软件构建中层结构的设计原则--SOLID
SOLID是五条原则的英文首字母拼接,这五条原则指的是:
-
SRP:
单一职责原则
一个软件系统的最佳结构高度依赖于开发这个系统的组织的内部结构。这样,每个软件模块都有且只有一个需要被改变的理由。
-
OCP:
开闭原则
核心要素是:如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。
-
LSP:
里氏替换原则
如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。
-
ISP:
接口隔离原则
这项设计原则主要告诫软件设计师应该在设计中避免不必要的依赖。
-
DIP:
依赖反转原则
该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。
SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序。下面会分别介绍这五条设计的原则,以及给出相应的代码示例。
SRP:单一职责原则
SRP的一个描述是:类发生更改的原因应该只有一个, 更简单的描述是:
任何一个软件模块都应该只对某一类行为者负责。
软件模块可能是一个源代码文件,也可能指的是一组紧密相关的函数和数据结构。
《架构整洁之道》中举了一个例子描述违反SRP原则的例子:

- calculatePay()函数是由财务部门制定的,他们负责向CFO汇报。
- reportHours()函数是由人力资源部门制定并使用的,他们负责向COO汇报。
- save()函数是由DBA制定的,他们负责向CTO汇报。
这三个函数被放在同一个源代码文件,即同一个Employee类中,程序员这样做实际上就等于使三类行为者的行为耦合在了一起,这有可能会导致CFO团队的命令影响到COO团队所依赖的功能。
比如现在有另外一个Utils的工具类,被calculatePay()函数和reportHours()函数调用来计算工作时长,假设CFO团队需要修改正常工作时数的计算方法,而COO带领的HR团队不需要这个修改,因为他们对数据的用法是不同的,如果这个修改最终上线,CFO团队的修改最终会导致COO带领的HR团队的数据报表计算出错。
一个推荐的修改方法是采用Facade
设计模式:
Facade
(外观)模式:一个复杂的子系统,它由许多类和接口组成。这些类和接口之间的交互可能相当复杂,对于使用该子系统的客户端来说,直接与这些细节进行交互可能会很困难。Facade设计模式通过提供一个简单的接口,封装了底层复杂的子系统,使客户端与子系统的交互变得更加简单。它相当于一个外观,隐藏了子系统的复杂性,使客户端只需要与Facade接口进行交互,而不需要了解底层子系统的细节。

OCP:开闭原则
OCP 描述的是这样一个原则:
设计良好的计算机软件应该易于扩展,同时抗拒修改。
OCP是研究软件架构的根本目的,一个设计良好的计算机系统应该在不需要修改的前提下就可以轻易被扩展。
OCR在软件架构层次上应用的一个例子是组件结构,软件架构师可以根据相关函数被修改的原因、修改的方式及修改的时间来对其进行分组隔离,并将这些互相隔离的函数分组整理成组件结构,使得高阶组件不会因低阶组件被修改而受到影响。如果A组件不想被B组件上发生的修改所影响,那么就应该让B组件依赖于A组件。下面是一个例子:

图中的Interactor组件包含了其最高层次的应用策略。其他组件都只是负责处理周边的辅助逻辑,只有Interactor才是核心组件。是整个系统中最符合OCP的。发生在Database、Controller、Presenter甚至View上的修改都不会影响到Interactor。
LSP:里氏替换原则
里氏替换原则是用来指导我们如何判定两个类应该设计成继承关系。即如何判定一个类是另外一个类的子类型。
如果对于每个类型是S的对象o1都存在一个类型为T的对象o2,能使操作T类型的程序P在用o2替换o1时行为保持不变,我们就可以将S称为T的子类型。

一个例子是:假设一个License类,该类中有一个名为calcFee()的方法,该方法将由Billing应用程序来调用。而License类有两个“子类型”: PersonalLicense与BusinessLicense,这两个类会用不同的算法来计算授权费用。上述设计是符合LSP原则的,因为Billing应用程序的行为并不依赖于其使用的任何一个衍生类。License类对象可以用来替换两个衍生类的对象,而保持行为不变。
《架构整洁之道》中提到一个经典的违反LSP原则的例子:

上图中当用户调用Rectangle的设置宽、高函数,并不能正确为Square设置边长,因为正方形要求边长相等,用户调用setH()和setW()设置不相等时,最后会返回的不正确正方形面积。因为Rectangle的实例替换Square的实例并不带来相同的表现,所以Rectangle并不是Square的子类型。
ISP:接口隔离原则
在一般情况下,任何层次的软件设计如果依赖于不需要的东西,都会是有害的。从源代码层次来说,这样的依赖关系会导致不必要的重新编译和重新部署,对更高层次的软件架构设计来说,问题也是类似的。这个问题可以通过将不同的操作隔离成接口来解决,如下图左一改为下图左二。


DIP:依赖反转原则
依赖反转原则(DIP)主要想告诉我们的是,如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。当然这条原则被严格执行是不现实的,因为软件系统在实际构造中不可避免地需要依赖到一些具体实现。但如果依赖的具体类足够稳定,或者这些实现来自稳定的操作系统或者平台设施,一般情况下会认为这些系统接口很少会有变动,我们主要应该关注的是软件系统内部那些会经常变动的(volatile)具体实现模块,对于这些模块,我们可以提供一个比较稳定的抽象接口层。针对依赖反转原则,以下有几条具体的编码守则:
- 应该花费更大的精力来设计接口,以减少未来对其进行改动。
- 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
- 不要在具体实现类上创建衍生类。
- 不要覆盖(override)包含具体实现的函数。
- 应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。
依赖反转原则(DIP)的一个常用的设计模式是抽象工厂模式。
抽象工厂模式是一种创建型设计模式,它提供了一种创建一系列相关或相互依赖对象的方法,而无需指定具体的类。
在下图中Application类是通过Service接口来使用ConcreteImpl类的,然而,Application类还是必须要构造ConcreteImpl类实例,这意味着在源代码层次上引入对ConcreteImpl 类具体实现的依赖,如左下图所示。我们可以使用抽象工厂再封装一层稳定的接口来解决该问题,如右下图所示。


在右上图中,考虑接口 Service Facroty
和实现类Service Facroty impl
,它们之间的边界代表了软件架构中的抽象层与具体实现层的边界。可以发现这里的控制流跨越架构边界的方向与源代码依赖关系跨越该边界的方向正好相反。
源代码依赖方向永远是控制流方向的反转——这就是DIP被称为依赖反转原则的原因。
评论