跳到主要内容

SOLID 原则

SOLID 原则主要讲的是,如何将数据和函数组织成类,如何将这些类链接起来成为程序,主要聚焦于类的层面。

在这一层次,即软件构建中层,架构设计的目标是:

  • 使软件可容忍被改动
  • 使软件更容易被理解
  • 构建可在多个软件系统中复用的组件
备注

中层指的是,SOLID 原则应紧贴于具体的代码逻辑,用于定义架构中的组件和模块,而组件和模块的整合则是下一章的内容

SRP: 单一职责原则

任何一个软件模块都应该只对某一类有共同需求的人负责

在这里,这一类有共同需求的人指的是他们希望对系统进行的变更是相似的。一个模块不应是一部分人想变更,而另一部分人不想变更的。

软件系统的最佳结构高度依赖于开发这个系统的组织的内部结构,这样每个软件模块都有且只有一个需要被改变的理由,即组织结构决定架构结构

提示

在组件层面,SRP 可以被称为共同闭包原则(Common Closure Principle)
在软件架构层面,SRP 则是用于奠定架构边界的变更轴心(Axis of Change)

OCP: 开闭原则

设计良好的程序应该易于扩展,同时抗拒修改。

可以根据需求,将代码分组(SRP),然后再调整分组间的依赖关系(DIP)。模块间的依赖关系应该为单向依赖,将不想更改的组件(上层组件)作为依赖的目标。高阶组件不会因为低阶组件被修改而受到影响。

提示

抗拒修改指的是程序本身的特性,在好的设计下,在需要扩展时,已有组件不需要扩展。但在实践中,如果发现现有程序无法满足需求(设计得不够好),还是应该积极重构

备注

在此图中,Controller 会直接使用 Interactor 中的数据结构,导致 Controller 对 Interactor 的结构过于依赖,应该添加一个 Requester 接口,用于 Controller 仅获取其需要的数据。

LSP: 里氏替换原则

如果想用可替换的组件来构建系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换

如果对于每个类型是 Child 的对象都存在一个类型为 Parent 的对象,能使操作 Parent 类型的程序 P 在用 Parent 对象替换 Child 对象时行为保持不变,则可称 Child 为 Parent 的子类型。这种替换关系,是使用继承的目的。

一旦违背了可替换性,系统架构就不得不为此添加大量复杂的应对机制。

违反 LSP 的设计案例——正方形/长方形问题

在这个例子中,正方形的 H 与 W 只能同时变化,但对用户来说,却有setH()setW()两个可以分别设置的函数,容易带来混淆与潜在的错误。故在考虑 LSP 原则时,不仅要从类的意义来看,更要从使用者的角度考虑

ISP: 接口隔离原则

在设计中避免不必要的依赖

User1 仅需要使用 OPS 中的 op1(),不需要其他op2()op3()方法,但对他们的更改却会导致 User1 也需要被重新编译和部署。而到了软件架构中,如果依赖不必要的模块,这些无关模块的改变也会导致软件需要重新编译和部署。同时无关模块中的无关错误,也可能产生影响。因此,无论是源代码级的设计,还是架构设计中都应避免依赖不需要的模块。

应改为下图的模式,添加接口。

DIP: 依赖反转原则

高层策略性代码不应该依赖底层实现细节代码,恰恰相反,底层实现细节代码应该依赖高层策略性代码

如果想设计一个灵活的系统,在源代码依赖关系中就应该多引用抽象类型,而非具体实现。即在使用use, import, include这些语句时,应该只引用包含接口,抽象类或其他抽象声明的源文件,而不应该引用任何具体实现。更具体的,应该是避免引用那些经常变动的具体实现。

抽象层是稳定的

需要修改抽象接口的时候,对应的具体实现肯定需要修改,但反之修改实现时,抽象接口一般不会去修改。因此接口的设计很重要,其有以下几个原则

  • 在代码中多使用抽象接口,尽量避免使用多变的具体实现类。同时,对象的创建也应该受到严格控制,通常使用抽象工厂(abstract factory)这一设计模式
  • 不要在具体实现类上创建衍生类。继承关系是一切源代码中依赖关系最强的、最难被修改的。对继承的使用应该格外小心。
  • 不要覆盖(override)包含具体实现的函数。调用包含具体实现的函数通常就意味着引入了源代码级别的依赖,即使覆盖了这些函数,也无法消除其中的依赖。正确的做法是创建一个抽象函数,然后再为该函数提过多种具体实现。
  • 避免再代码中写入与任何实现相关的名字,或者是其他容易变动的事物名字

工厂模式

创建对象不可避免地需要再源代码层次上依赖对象的具体实现。因此有必要对易变对象的创建做一些特殊处理。一种方式就是抽象工厂。

这种模式将软件架构划分为了抽象层与具体实现层,所有跨越这层组织边界的依赖关系都是单向的,即具体实现依赖抽象层。

抽象层中包含了应用的高阶业务规则,而具体实现中则包括了所有这些业务规则所需要做的具体操作及细节信息。

所谓的依赖反转(DIP)指的是源代码依赖永远是控制流方向的反转,即图中的闭合箭头(源代码级依赖,即继承关系)与非闭合箭头(控制流)相反。

提示

跨越组织边界的、朝向抽象层的单向依赖关系是一个抽象守则——依赖守则

具体实现组件

在图中的实现组件中,有一条 create 关系,它是违反 DIP 原则的,但这其实很常见,在软件系统中不可能完全消除违反 DIP 的情况,通常只需要将它们集中于少部分具体实现组件中,与其他部分隔离。