面向对象设计原则总结:SOLID/LKP/DRY/KISS…

目录

  • 封装变化
  • 针对接口编程,不针对实现编程
  • 多用组合(has-a),少用继承(is-a)
  • 为交互对象之间的松耦合设计而努力
  • 最少知识原则 LKP / 迪米特法则 Law of Demeter
  • 好莱坞原则
  • SOLID 原则
    • 单一职责原则 SRP
    • 开放关闭原则 OCP
    • 里氏替代原则 LSP
    • 接口隔离原则 ISP
    • 依赖倒置原则 DIP
  • DRY 原则
  • KISS 原则

封装变化

找出应用中需要经常变化的部分,把他们独立出来,改变这部分代码不影响其他部分。这几乎是每个设计模式背后的精神所在,即系统中某部分的改变不影响其他部分。

针对接口编程,不针对实现编程

针对接口编程,关键在于多态。变量/成员/形参的声明应该是抽象类/接口类/父类,即所有的代码操作的都是父类/接口类/抽象类(如 Animal),只会在一处会涉及到具体类(如 Cat 或 Dog),那就是在用 new 实例化具体子类对象时,而这部分代码最好也用工厂封装起来,这样甚至可以在运行时动态实例化不同的子类对象。

针对实现编程 👎 针对接口编程 👍 针对接口编程 + 工厂 👍👍
Dog d = new Dog();
d.bark();
Animal a = new Dog();
a.makeSound();
Animal a = getAnimal();
a.makeSound();

多用组合(has-a),少用继承(is-a)

继承和组合都可以实现代码复用,但组合比继承更灵活,可运行时改变行为/属性。例如下面的游戏战车,采用组合可以在运行时动态地通过 setWeapon() 来切换武器,而继承是在编译时静态决定子类行为,则没有这种灵活性。

继承(is-a) 👎 组合(has-a) 👍
is-a has-a

为交互对象之间的松耦合设计而努力

例如观察者模式使得观察者(Listener)和被观察者(Subject)之间是松耦合的。被观察者(Subject)不知道观察者的细节,只知道观察者实现了 Listener 接口(如 update()),只要他们之间的接口不变,就可以自由地改变一方而不影响另一方。

最少知识原则 / 迪米特法则

最少知识原则(LKP, Least Knowledge Principle)也被称为迪米特法则(Law of Demeter)。其目的是减少对象之间的交互和类之间的依赖,不要让太多的类耦合在一起,避免修改一部分影响到另一部分。

为了避免依赖过多的类,在 Foo 类的方法 func(Bar bar) 中只应该调用:

  • Foo 类自身的方法/成员函数
  • Foo 类成员的方法
  • 参数 bar 的方法
  • func()中创建的对象的方法

应避免调用由另一个方法返回的对象的方法,即避免像下面这样连续的函数调用:

// 👎 依赖 Station、Thermometer 类
float getTemp() {
  return station.getThermometer().getTemperature();
}

// 👍 只依赖 Station 类
float getTemp() {
  return station.getTemperature();
}

外观模式是该原则的体现。Facade 封装了复杂的子系统,提供了统一、简单的高层接口, Client 只和 Facade 进行交互,避免了 Client 和子系统的紧耦合。

缺点:需要额外的包装类,增加复杂度,降低运行时性能。

好莱坞原则

允许低层组件将自己挂钩(hook)到系统上,但是高层组件决定什么时候和怎样使用这些低层组件(即高层组件对待低层组件的方式是:Don't call me, I'll call you)。一个典型例子是模版方法模式:

class Drink {
public:
    void makeDrink() final
    {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }
    
    void boilWater() {...}
    virtual void brew() = 0;
    void pourInCup() {...}
    virtual void addCondiments() = 0;
};

高层组件 Drink 的 makeDrink() 方法控制着饮料制作方法流程。低层组件 Tea/Coffee 通过 override brew()/addCondiments() 方法可以将自己 hook 到系统上,从而改变最终的行为。高层组件控制着何时、如何让低层组件参与,低层组件不可以直接调用高层组件。客人只依赖 Drink 类,而不依赖具体的 Tea/Coffee 类。

此外,工厂方法、观察者等模式也遵循了该原则:如工厂方法子类决定如何创建具体对象、低层的 Listener 把自己添加到高层被观察者的 Listener 列表中等。特别地,标准库中的很多算法也是该原则的体现:例如用标准库 sort 算法对自定义类的对象进行排序,需要传入一个可调用对象(比较函数/lambda)。

思考:和依赖倒置原则的关系?

SOLID 原则

这是最著名的 5 个设计原则,在《整洁架构之道》的第三部分的学习笔记中再次详细介绍这 5 个原则。

  • 单一职责原则 SRP
  • 开放关闭原则 OCP
  • 里氏替代原则 LSP
  • 接口隔离原则 ISP
  • 依赖倒置原则 DIP

单一职责原则 SRP

Just because you can doesn't mean you should.

类应该只有一个改变的理由。这背后的原因是每次修改代码可能产生潜在的错误。一个类只做一件事的时候,更容易实现高内聚;反之,如果一个类支持多个不相关的功能时,通常是低内聚的。当项目中经常由于不同的原因修改一个类的时候,或者负责不同业务的两个团队经常需要修改同一个文件,则是时候考虑单一职责原则了。

开放关闭原则 OCP

类应该对扩展开放,对修改关闭。扩展新功能只需新增代码,而不需要修改现有代码,减少 code review 的工作量及意外引入 bug 的可能性。

例子

  • 观察者模式中,增加新的观察者不需要改变被观察者代码
  • 除了观察者模式之外,装饰者模式也是该原则的体现
  • 一个比较典型的违反 OCP 的例子是 switch/case:代码中有多处相同的 switch,每当需要增加一个新的 case 时,需要到处修改

注意

  • 开放关闭原则需要引入新的抽象层,增加代码的复杂度
  • 没有必要(也做不到)让系统的每个部分都遵循开放关闭原则,这是一种浪费
  • 应该把精力放在最可能改变的地方

里氏替代原则 LSP

需要父类的地方都可以用子类替代,背后的原因是希望实现同一接口的组件之间可以任意替换。

接口隔离原则 ISP

一个接口中有几十个方法,但客户 A。可能只需要其中的几个方法,而另一个客户 B 可能需要另外几个方法。臃肿的接口使得两个客户之间产生耦合,为客户 A 修改的某些方法也会影响到客户 B,即使客户 B 并没有用到这些方法。

依赖倒置原则 DIP

依赖倒置原则的另一种表达方式是:要依赖抽象,不要依赖具体类。和“针对接口编程, 不针对实现编程”很相似,但这里更强调抽象。高层策略性代码不应该依赖低层实现细节代码,两者都应该依赖抽象。抽象也可以看作是高层策略的一部分,即低层细节实现应该依赖高层。控制流方向往往都是从高层到低层,而依赖倒置,是将源码的依赖关系反转,在源码级别上使得低层代码依赖高层策略。这一点对于软件架构来说尤为重要,《架构整洁之道》的第三部分深入地讨论了这一点。

依赖倒置指导方针

  • 变量不可以持有具体类的引用——改用工厂,避免直接使用 new 持有具体类的引用(new 具体类的操作都封装到工厂中)
  • 不要让类派生自具体类——派生自抽象类或接口,这样就不依赖具体类了
  • 不要覆盖基类中已经实现的方法——如果这样,说明不是一个真正适合被继承的抽象

DRY 原则

Don't Repeat Yourself 不要重复。重复是一切邪恶的根源。许多原则、最佳实践都是为了消除重复。

KISS 原则

Keep It Simple and Stupid 保持简单。不要本末倒置,简单的事情简单做!不要过度设计,不要把简单的事情搞复杂!

posted @ 2023-08-05 18:04  Zijian/TENG  阅读(335)  评论(0编辑  收藏  举报