0%

设计原则

1. 面向对象设计原则

对于面向对象软件系统设计而言, 在支持 可维护性 的同时, 提高系统的 可复用性 是一个至关重要的问题.
常见的面向对象设计原则有以下 7 种, 遵循这些设计原则可以有效的提高系统的可维护性可复用性.

1.1. 开闭原则

Open-Closed Principle

软件实体应对扩展开放, 对修改关闭.

换言之, 软件实体应尽量在不修改原有代码的情况下进行扩展. 此处软件实体可以指一个软件模块, 一个由多个类组成的局部结构或一个独立的类.

应用开闭原则可扩展已有的软件系统, 并为之提供新的行为, 以满足对软件的新需求, 使变化中的软件系统具有一定的 适应性灵活性.
对于已有的软件模块, 特别是最重要的抽象层模块不能再修改, 这就使变化中的软件系统具有一定的 稳定性延续性, 这样的系统同时满足了 可复用性可扩展性.

实现开闭原则的关键就是抽象化, 并且从抽象化导出具体化实现. 在面向对象设计中, 开闭原则一并通过在原有模块中添加抽象层 (如接口或抽象类等) 来实现, 它也是其他面向对象设计原则的基础.

开闭原则是面向对象设计的目标.

1.2. 里氏替换原则

Liskov Substitution Principle

所有引用基类的地方能够透明地使用其子类对象.

其严格描述: 如果对每一个类型为 T1 的对象 O1, 都有类型为 T2 的对象 O2, 使得以 T1 定义的所有程序 P 在所有的对象 O1 都代表为 O2 时, 程序 P 的行为没有变化, 那么类型 T2 是类型 T1 的子类型.
换言之, 一个软件实体如果使用的是一个基类对象的话, 那么一定适用于其子类型对象, 而且觉察不出基类对象和子类对象的区别, 即把基类都替换成它的子类, 程序不会出错. 反过来则不一定成立, 如果一个软件实体使用的是一个子类对象的话, 那么它不一定适用于基类对象.

在运用里氏替换原则时, 尽量将一些需要扩展的类或者存在变化的类设计为抽象类或者接口, 并将其作为基类, 在程序中尽量针对抽象编程.
由于子类继承基类并覆盖基类的方法, 在程序运行时, 子类对象可以替换基类对象, 如果需要对类的行为进行修改, 可以通过扩展基类, 或增加新的子类, 而无需修改使用该基类对象的代码.

为了确保该原则的应用, 一个具体类应当只实现接口和抽象类中声明过的方法, 而不要给出多余的方法, 否则将无法调用到子类中增加的新方法.

1.3. 依赖倒转原则

Dependence Inversion Principle

抽象不应该依赖于细节, 细节应该依赖于抽象.

换言之, 要针对抽象编程, 而不是针对实现编程. 在程序代码中, 传递参数时或在关联关系中, 尽量引用层次高的抽象层类, 即使用接口和抽象类进行变量类型声明, 参数类型声明, 方法返回类型声明, 以及数据类型的转换等, 而不是使用具体类来做这类事情.
使用依赖倒转原则, 由类图可以看到一个向上依赖的倒置结构, 故以此命名.

在引入抽象层后, 系统将具有极好的灵活性, 在程序中尽量使用抽象层进行编程, 而将具体一来作为配置. 这样一来, 如果系统行为需要发生变化, 只需对抽象层进行扩展, 并修改相关配置, 而尽可能的减少修改代码, 甚至不修改代码的情况下改变系统的功能.

依赖倒转原则是以开闭原则为目标, 以里氏替换原则作为基础, 更深层次的应用, 是面向对象设计的主要机制.

1.4. 单一职责原则

Single Responsibility Principle

一个类只负责一个功能领域中的相应职责.
对于一个类而言, 应该只有一个引起它变化的原因.

在一个软件系统中, 一个类 (或者大到模块, 小到方法) 承担的职责越多, 它被复用的可能性就越小.
而且一个类承担的职责过多, 就相当于将这些职责耦合在一起, 当其中一个职责变化时, 可能影响其他职责的运作.
因此要将这些职责进行分离, 将不同的职责封装在不同的类中, 即将不同的变化原因封装在不同的类中, 如果多个职责总是同时发生改变则可将它们封装在同一个类中.

单一职责原则是实现 高内聚, 低耦合 的指导方针.

1.5. 接口隔离原则

Interface Segregation Principle

使用多个专门的接口, 而不使用单一的总接口.

这里的接口往往有 2 种含义, 这 2 种不同的含义, 在 ISP 中的表达方式以及含义都有所不同:

  1. 是指一个类型所具有的方法特征的集合, 仅仅是一种逻辑上的的抽象.

此时可以把接口理解成为角色, 一个接口就只代表一种角色, 此时这个原则可以叫做 角色隔离原则.

  1. 是指某种语言具体的接口定义, 有严格的定义和结构, 比如 Java 语言里的 Interface.

接口仅仅提供客户端需要的行为, 客户端不需要的行为则隐藏起来, 应当为客户端提供尽可能小的单独的接口, 而不要提供大的总接口.
为了使接口的职责单一, 需要将大接口中的方法根据职责不同拆分成不同的小接口, 以降低耦合度, 提高灵活性.

1.6. 迪米特法则

Law of Demeter
也称最少知识原则, Least Knowledge Principle

一个软件实体应当尽可能少的与其他实体发生相互作用.

这样在修改时, 就会尽量少地影响其他的模块, 扩展相对容易. 这是对软件实体之间通信的限制, 它要求限制软件实体之间通信的宽度和深度.

迪米特法则可分狭义法则和广义法则.
在狭义的迪米特法则中, 如果两个类之间不必彼此直接通信, 那么这两个类就不应当发生直接的相互作用, 如果其中的一个类需要调用另一个类的某一个方法的话, 可以通过第三方转发这个调用. 狭义的迪米特法则可以降低类之间的耦合度, 但是会在系统中增加大量的小方法并散落在系统的各个角落, 它可以使一个系统的局部设计简化, 因为每一个局部都不会和远距离的对象有直接的关联, 但是也会造成系统的不同模块之间的通信效率降低, 使得系统的不同模块之间不容易协调.
广义的迪米特法则就是指对象之间的信息流量, 流向以及信息的影响的控制, 主要是对信息隐藏的控制. 信息的隐藏可以使各个子系统之间解耦, 从而使它们可以被独立地开发, 优化, 使用和修改, 同时可以促进软件的复用, 由于每一个模块都不依赖于其它模块而存在, 因此每一个模块都可以被独立地在其它地方被使用. 一个系统的规模越大, 信息隐藏的重要性就越明显.

迪米特法则主要用于控制信息的过载. 在迪米特法则运用时到系统设计时, 需要注意以下的几点:
在类的划分上, 应当尽量创建松耦合的类;
在类的结构上, 每一个类应当尽量降低其成员变量和成员函数的访问权限;
在类的设计上, 只要有可能, 一个类应当设计成不变类;
在对其他类的引用上, 一个对象对其他对象的引用应当降到最低.

1.7. 合成复用原则

Composite Reuse Principle
也称组合/聚合复用原则, Composition / Aggregate Reuse Principle

复用时, 尽量使用对象组合, 而不使用继承来达到复用的目的.

在一个新的对象里通过关联关系 (包括组合和聚合关系) 来使用一些已有的对象, 使之成为新对象的一部分; 新对象通过委派调用已有对象的方法达到复用已有功能的目的.

在面向对象设计中, 可以通过 2 种基本方法在不同的环境中复用已有的设计和实现, 即通过组合/聚合关系或通过继承. 但首先应该考虑使用组合/聚合, 组合/聚合可以使系统更加灵活, 类之间的耦合度更低; 其次才考虑使用继承, 使用继承时应严格遵循里氏替换原则. 有效的使用继承会有助于对问题的理解, 降低复杂度, 而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度.

通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性, 因为继承会将基类的实现细节暴露给子类.
由于基类的内部细节对于子类来说是可见的, 所以这种复用有身为 白箱 复用.
如果基类发生改变, 那么子类的实现也不得不发生改变; 从基类继承而来的实现是静态的, 不可能在运行时发生改变, 没有足够的灵活性; 而且继承只能在有限的环境中使用, 如类未声明为不能被继承.

由于组合或聚合关系可以将已有的对象 (也称成员对象) 纳入到新对象中, 使之成为新对象的一部分, 因此新对象可以调用已有对象的功能, 这样做可以使得成员对象的内部实现细节对新对象是不可见的, 所以这种复用又称 黑箱 复用.
相对继承而言, 其耦合度相对较低; 成员对象的变化对新对象的影响不大, 可以在新对象中根据实际需要有选择性地调用成员对象的操作; 合成复用可以在运行时动态地进行, 新对象可以动态的引用于成员对象类型相同的其他对象.

2. 其他设计原则

2.1. SOLID

SOLID 是以上其中 5 种设计原则的单词缩写, 比较好记.

  • S 单一职责原则 SRP
  • O 开闭原则 OCP
  • L 里氏替换原则 LSP
  • I 接口分离原则 ISP
  • D 依赖倒转原则 DIP

2.2. DRY

Don’t Repeat Yourself
每一个知识或逻辑必须在一个系统中有一个单一的, 明确的表示.

2.3. KISS

Keep It Simple, Stupid
每个方法应该只解决一个小问题, 而不是实现很复杂的功能. 如果你在方法中有很多条件, 把它们分解成更小的单独的方法. 它不仅更易于阅读和维护, 而且可以更快地发现bug.

2.4. YAGNI

You Ain’t Gonna Need It
只需要将应用程序必需的功能包含进来, 而不要试图添加任何其他你认为可能需要的功能.

当你准备列出一个项目清单时, 试着考虑以下问题:

  • 通过降低抽象的层级, 来实现低复杂度
  • 根据特性将功能独立出来
  • 适度接受非功能性需求
  • 识别耗时的任务, 并摆脱它们