面向对象的七大原则
本文最后更新于 453 天前,其中的信息可能已经有所发展或是发生改变。

说到面向对象,很多人应该都想到了封装、继承和多态,这是面向对象的三大特征。

但是,面向对象还有七大原则,如果你不了解的话,其实也并不影响你正常开发,但是有可能会造成不可预料的后果。掌握面向对象的七大原则,可以帮助你更好的设计规划,提高代码的可维护性。

开闭原则 (Open Close Principle)

Software entities like classes, modules and functions should be open for extension but closed for modifications.

简而言之,开闭原则就是要让类具有可拓展性且避免修改父类方法,下面举一个简单的例子来帮助理解。

假设我们现在需要写一个图形绘制类,要求是既可以画圆也可以画正方形,下面看看反例如何:

static class Graph {
    void draw(Shape shape) {
        switch (shape.type) {
            case Circle:
                drawCircle((Circle) shape);
                break;
            case Square:
                drawSquare((Square) shape);
                break;
            default:
                break;
        }
    }
    void drawCircle(Circle circle) {}
    void drawSquare(Square square) {}
}

static class Shape {
    public final Type type;
    public Shape(Type type) {
        this.type = type;
    }
    enum Type {
        Circle, Square
    }
}

如上所示,如果还需要再增加几种图形,那么就必须要再对Graph做出修改,同时新增几个新的绘制方法,这样就显得很复杂,并且维护性也会下降。因为在实际开发中,Graph可能由其他人共同完成,但形状只由一个人负责,因此最佳的解决方案应该是将draw()方法交给子类完成,Graph只需要调用Shape.draw()即可。

下面是改进后的代码:

static class Graph {
    void drawShape(Shape shape) {
        shape.draw();
    }
}

static abstract class Shape {
    abstract void draw();
}

static class Circle extends Shape {
    @Override
    void draw() {
        // How to draw a circle
    }
}

static class Square extends Shape {
    @Override
    void draw() {
        // How to draw a square
    }
}

里氏代换原则 (Liskov Substitution Principle)

Likov’s Substitution Principle states that if a program module is using a Base class, then the reference to the Base class can be replaced with a Derived class without affecting the functionality of the program module.

里氏替换原则就是说子类可以拓展父类的功能,但不能替换,还包含如下几点:

  • 子类可以增加自己独有的方法
  • 子类实现父类的接口时,返回类型应该比父类更加严格
  • 子类重载父类的方法时,参数类型应该比父类更加宽松

也就是说,父类可以用子类来替换。

下面举一个经典的例子来理解一下里氏替换原则,正方形不是长方形,但正方形确实属于长方形中的一种。

static class RectAngle {
    int width, height;

    public void setWidth(int width) {
        this.width = width;     
    }

    public void setHeight(int height) {
        this.height = height;
    }
}

static class Square extends RectAngle {
    public void setWidth(int newWidth) {
        super.width = newWidth;
        super.height = newWidth;
    }

    public void setHeight(int newHeight) {
        super.width = newHeight;
        super.height = newHeight;
    }
}

如上所示,Square类继承了RectAngle类,并且无论对正方形设置宽度还是高度,最终的宽高都必须相同。

子类Square没有覆盖父类中的setWidth()和setHeight(),符合里氏替换原则。

下面再来看看子类重载父类的方法时,参数类型应该比父类更加宽松是怎么理解的。

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.function(new HashMap<>());
    }

    static class Parent {
        void function(HashMap<?,?> map) {
            System.out.println("Parent function");
        }
    }

    static class Child extends Parent {
        void function(Map<?, ?> map) {
            System.out.println("Child function");
        }
    }
}

子类重载了父类的function()方法,并且接收比父类HashMap更宽范围的Map类型的参数,最终的结果应该是执行父类的function()方法。

相反地,如果把父类方法参数类型放宽而缩小子类方法的参数类型,最终的结果应该是Child function被执行,这不符合里氏替换原则。

接口隔离原则 (Interface Segregation Principle)

The Interface Segregation Principle states that clients should not be forced to implement interfaces they don’t use. Instead of one fat interface many small interfaces are preferred based on groups of methods, each one serving one submodule.
Clients should not be forced to depend upon interfaces that they don’t use.

接口隔离原则很好理解,目的是要让实现接口的类只实现它们需要的方法。

static class Worker implements IWorker {
    @Override
    public void work() {}

    @Override
    public void eat() {}

    @Override
    public void sleep() {}
}

static class RobotWorker implements IWorker {
    @Override
    public void work() {}

    @Override
    public void eat() {
        // Robot cannot eat as human
    }

    @Override
    public void sleep() {
        // Robot cannot sleep as human
    }
}

interface IWorker {
    void work();
    void eat();
    void sleep();
}

上面我们对工厂进行抽象,这个工厂既有工人也有AI,IWorker中有eat()和sleep()方法,显然对Robot不适用,所以我们需要将IWorker分成两个接口。

事实上,我们完全可以对RobotWorker中的eat()和sleep()不做任何处理,但是这样不仅让类变得复杂且无意义,还可能造成其他的一些问题:Robot虽然不能eat(),但是可能在当天午餐的人数统计中被计入在内,根据接口隔离原则,就必须将IWorker拆分了。

static class Worker implements IWorker, IHuman {
    @Override
    public void work() {}

    @Override
    public void eat() {}

    @Override
    public void sleep() {}
}

static class RobotWorker implements IWorker {
    @Override
    public void work() {
}
}

interface IWorker {
    void work();
}

interface IHuman {
    void eat();
    void sleep();
}

改进之后,我们把IWorker拆分成了IWorker和IHuman两个接口,IWorker只负责工作相关的方法,而IHuman则只属于Worker而非RobotWorker,这样就避免让RobotWorker类实现不必要的方法。

依赖倒转原则 (Dependence Inversion Principle)

  • 上级模块不应该依赖低级模块,二者都应该依赖抽象
  • 抽象不依赖细节,细节取决于抽象

看完上面的描述,你可能还是一脸懵逼,下面举两个简单的例子来理解一下。

首先还是一个反例,假设我们对人读报纸进行抽象,可以有如下写法:

static class Person {
    void readNewspaper(Newspaper newspaper) {
        System.out.println(newspaper.content);
    }
}

static class Newspaper {
    String content;
}

很快我们就可以写出这样的方法,但如果现在要再增加几种读物呢?

static class Person {
    void readNewspaper(Newspaper newspaper) {
        System.out.println(newspaper.content);
    }
    void readBook(Book book) {
        System.out.println(book.content);
    }
    
    void readMagazine(Magazine magazine) {
        System.out.println(magazine.content);
    }
}

static class Newspaper {
    String content;
}

static class Book {
    String content;
}

static class Magazine {
    String content;
}

可以看到,随着读物类型的增加,Person类逐渐复杂,因为Person需要负责处理各种读物的阅读方法。

这就是一个典型的由下级到上级,上级依赖下级的例子,因为要读就必须有读物,读的内容来自读物,这样随着类型的增加,可维护性也会降低。

那么根据依赖倒转原则,我们可以降低类之间的耦合性,以抽象建立起类之间的联系,这样的抽象要比具体实现更加稳定,下面看看好的例子是如何实现上述需求的:

interface Readable {
    String getContent();
}

static class Person {
    void read(Readable readable) {
        System.out.println(readable.getContent());
    }
}

static class Newspaper implements Readable {
    String content;

    @Override
    public String getContent() {
        return content;
    }
}

static class Book implements Readable {
    String content;

    @Override
    public String getContent() {
        return content;
    }
}

static class Magazine implements Readable {
    String content;

    @Override
    public String getContent() {
        return content;
    }
}

经过修改,观察发现我们把读物抽象成了Readable,并且Person中的read()方法只关心传入的参数是否为Readable类型,而不管具体的读物类型。

迪米特法则 (Law of Demeter)

迪米特原则又称知道最少原则(The Least Knowledge Principle),它有一个形象的解释:talk only to your immediate friends

那么什么是直接的朋友呢?一个类中的成员变量、方法参数与返回值的类成为直接朋友,而仅出现在局部变量中的类称为间接朋友。

简单来说,就是保存一个对象对另一个对象的最低了解,举一个简单的例子。

我们在关闭计算机时,通常需要先关闭正在运行的程序与服务,然后才执行关机程序。

static class Person {
    void closeComputer(Computer computer) {
        computer.closeAllServices();
        computer.powerDown();
        computer.close();
    }
}

static class Computer {
    void closeAllTasks() {}
    void closeAllServices(){}
    void powerDown() {}

    void close() {
        closeAllTasks();
        closeAllServices();
        powerDown();
    }
}

如上所示,如果Person要关闭计算机,只需要调用Computer.close()方法即可,但是Computer还有其他的方法暴露给了Person,这样就造成了混乱。

因此,在Computer类中,只有close()方法是有必要开放的,其他方法只在内部调用,这样就避免了方法调用混乱,也符合知道最少原则,也就是迪米特原则。

public class Computer {
    private void closeAllTasks() {}
    private void closeAllServices(){}
    private void powerDown() {}
    public void close() {
        closeAllTasks();
        closeAllServices();
        powerDown();
    }
}

经过如上修改,现在closeAllTasks()等Computer的内部方法变得不可访问,Person只能调用close()方法来关闭计算机。

合成复用原则 (Composite Reuse Principle)

CRP的定义是尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的。

例如我们现在对花进行抽象,花有许多种类:玫瑰、郁金香、紫荆、康乃馨等,并且有不同的颜色。

假设我们对玫瑰进行抽象,玫瑰有红色也有白色,所以抽象成两个类RedRose和WhiteRose,并让它们都继承Rose类,Rose再继承Flower类,这样就完成了对玫瑰的抽象,好像并没有什么问题。

但随着花色的增加,需要抽象出的类也逐渐增多,于是我们便可以抽象出颜色,让它变得可复用。

static class Flower {
    final Color color;

    public Flower(Color color) {
        this.color = color;
    }
}

static class Rose extends Flower {
    public Rose(Color color) {
        super(color);
    }   
}

enum Color {
    White, Red
}

单一职责原则 (Single Responsibility Principle)

A class should have a single responsibility, where a responsibility is nothing but a reason to change.

很多时候,随着开发的进行,我们会无意识中给一个类添加本不属于它的职责的方法,如果后续要再修改,由于这个类有许多职责,所以导致这个类修改的原因也有很多。例如Login类本只需要负责登录接口的调用并且返回数据,但在开发中还对它添加了一些方法,如注册新账户、更新本地数据库等,这样如果要对更新数据库的逻辑进行修改时,就要修改Login类,这就导致Login类被修改的原因有很多。

因此,把注册与数据库更新的方法分离出去是必要的,只让Login处理登录的逻辑,也可以提供代码的可维护性。

未经允许禁止转载本站内容,经允许转载后请严格遵守CC-BY-NC-ND知识共享协议4.0,代码部分则采用GPL v3.0协议

评论

  1. Hakadao
    Windows Edge
    1 年前
    2023-1-30 1:31:16

    感謝大佬總結😀

    • Hakadao
      博主
      Windows Edge
      1 年前
      2023-1-30 23:08:39

      |´・ω・)ノ

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇