面向对象设计原则总结: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) 👍 |
---|---|
为交互对象之间的松耦合设计而努力
例如观察者模式使得观察者(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 doen'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 保持简单。不要本末倒置,简单的事情简单做!不要过度设计,不要把简单的事情搞复杂!
本文作者:Zijian/TENG(微信公众号:好记性如烂笔头),转载请注明原文链接:https://www.cnblogs.com/tengzijian/p/17608351.html
热门相关:峡谷正能量 不科学御兽 异世修真邪君 不科学御兽 未来兽世:买来的媳妇,不生崽