软件设计最大的难题就是应对需求的变化,但是纷繁复杂的需求变化又是不可预料的,我们要为不可预料的变化做好准备,这本身是一件非常痛苦的事情,但好在有大师们已经给我们提出了非常好的六大设计原则和 23 种设计模式来“封装”未来的变化。
在软件设计上有一些经典的设计原则,其中包括,SOLID、KISS、YAGNI、DRY、LOD 等。这些设计原则,从字面上理解,都不难。你一看就感觉懂了,一看就感觉掌握了,但是用在项目中的时候就会发现,“看懂”和“会用”是两回事,而“用好”更是难上加难。我有时会对这些原则理解的不够透彻,导致在使用时过于教条注意,拿原则当真理,生搬硬套,适得其反。
下面我对这些原则做一些梳理和整理,确保自己理解的是正确的。
SOLID 原则并非单纯的 1 个原则,而是由 5 个设计原则组成的,它们分别是:
- 单一职责原则
- 开闭原则
- 里氏替换原则
- 接口隔离原则
- 依赖反转原则
分别对应 SOLID 的 S、O、L、I、D 这 5 个英文字母。
原则描述
单一职责原则和开闭原则的远离比较简单,但是想要在实践中用却比较难。但依赖反转原则却正好相反,这个原则用起来比较简单,但概念起来比较难。
依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。中文翻译有时候也叫依赖倒置原则。
1 | 高层模块不要依赖低层模块。高层模块和低层模块应该通过抽象来互相依赖。除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象。 |
所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的。实际上,这条原则主要还是用来指导框架层面的设计。
依赖反转的就是指高层模块和底层模块之间的依赖关系被反转了,高层低层模块都应该依赖抽象接口。
控制反转和依赖注入这两个概念和依赖反转是有区别的。
控制反转是一个比较笼统的设计,强调将程序的执行流程由框架来控制,预留扩展点给程序员填充业务逻辑。
而依赖注入是具体的编码技巧,将一个类所依赖的类通过构造函数的方式传入,而不是在构造函数里面硬编码确定所依赖类的类型。而且所依赖类的类型应该是一个接口,存在多种实现,这样就可以提高了代码的扩展性,可以灵活地替换依赖的类。
需要了解依赖反转原则的话,需要弄清楚以下问题:
- “依赖反转”这个概念指的是“谁跟谁”的“什么依赖”被反转了?“反转”两个字该如何理解?
- 业界还存在“控制反转”和“依赖注入”这两个概念,这两个概念跟“依赖反转”有什么区别和联系,它们说的是同一件事情吗?
控制反转
在讲“依赖反转原则”之前,先来看看“控制反转”。控制反转的英文翻译是 Inversion Of Control,缩写为 IOC。
这里的控制指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在业界有一些框架是可以实现反转的,也就是整个程序的执行流程可以通过框架来控制,流程的控制权从程序员“反转”到了框架。
实际上,实现控制反转的方法有很多,除了刚才例子中所示的类似于模板设计模式的方法之外,还有依赖注入。所以这里的控制反其实并不是一种具体的实现技巧,而是一种比较笼统的设计思想,一般用来知道框架层面的设计。
依赖注入
依赖注入跟控制反转恰恰相反,它是一种具体的编码技巧。
那到底什么是依赖注入呢?
1 | 不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或者叫注入)给类使用。 |
用代码来解释一下:
1 |
|
通过依赖注入的方式来将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类。
这一点在“开闭原则”的时候也提到过。当然,上面代码还有继续优化的空间,我们还可以把 MessageSender 定义成接口,基于接口而非实现编程。改造后的代码如下所示:
1 |
|