设计模式(0)——设计模式和设计模式原则

本文介绍了什么是设计模式、设计模式列表和设计模式六大原则。

什么是设计模式

在《设计模式:可复用面向对象软件的基础》(以下都简称《设计模式》)一书中提到:

Christopher Alexander说过:“每一个模式描述了一个在我们周围不断发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。”

Christopher Alexander是一位建筑师,这是在他的著作中对建筑模式的总结。对应到软件工程,在长期的实践和积累中,人们总结了很多场景下提高代码的可复用性、可扩展性、可维护性和降低模块间耦合的经验,提炼成一个个框架供以后使用。

设计模式列表

《设计模式》一书介绍了23种设计模式:

模式英文名 模式中文名
Abstract Factory 抽象工厂模式
Adapter 适配器模式
Bridge 桥接模式
Builder 建造者模式
Chain of Responsibility 责任链模式
Command 命令模式
Composite 组合模式
Decorator 装饰器模式
Facade 外观模式
Factory Method 工厂方法模式
Flyweight 享元模式
Interpreter 解释器模式
Iterator 迭代器模式
Mediator 中介者模式
Memento 备忘录模式
Observer 观察者模式
Prototype 原型模式
Proxy 代理模式
Singleton 单例模式
State 状态模式
Strategy 策略模式
Template Method 模板方法模式
Visitor 访问者模式

模式依据其目的可以分为创建型、结构型和行为型三种,下面是《设计模式》一书中对23种设计模式的分类:

设计模式分类

设计模式之前的关系:

设计模式之间的关系

在后续文章中,将使用Java演示和分析这23种设计模式。

设计模式原则

提到设计模式,不可避免地要谈谈设计模式的六大原则:

  1. 单一职责原则
  2. 里式替换原则
  3. 依赖倒置原则
  4. 接口隔离原则
  5. 迪米特法则
  6. 开闭原则

单一职责原则(Single Responsibility Principle, SRP)

定义:不要存在多于一个导致类变更的原因。

其实不光是类,对于接口、类和方法都要遵循单一职责原则。单一职责原则从字面上很好理解,一个接口、类和方法只做一件事。

遵循单一职责原则有如下好处:

  1. 每个接口、类和方法只承担一项职责,意义更清晰。
  2. 使代码可读性、可维护性和可扩展性更佳。
  3. 当需求有变化需要修改代码时,只需要修改相应功能的代码,不会对其他功能的代码造成影响。

里式替换原则(Liskov Substitution Principle, LSP)

定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。

上面的定义是严格的定义,比较通俗的定义是:所有引用基类(父类)的地方必须能透明地使用其子类的对象。

里式替换原则包含如下几个含义:

  1. 子类可以实现父类的抽象方法,但不能覆盖父类已经实现的方法(即非抽象方法)。
  2. 子类可以增加自己特有的方法。
  3. 当子类重载父类的方法时,方法的前置条件(入参)要比父类更宽松。
  4. 当子类实现父类的抽象方法时,方法的后置条件(出参)要比父类更严格。

依赖倒置原则(Dependence Inversion Principle, DIP)

定义:

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象。
  2. 抽象不应该依赖细节。
  3. 细节应该依赖抽象。

设想一个情景,工厂生产零件,其中有工厂类Factory、螺丝类Screw、钉子类Nail等。如果Factory直接依赖于某个零件类,当需要生产新的零件时,势必要修改Factory,这样一来Factory和各个零件类的耦合就非常大。为了解决这个问题,我们可以引入一个接口IProduce,所有零件类都实现这个接口,Factory直接使用IProduce接口来进行生产,之后如果有新的零件类加入进来,只需要实现IProduce接口,Factory不需要做任何修改。

这个例子中,Factory类是高层模块,具体的零件类是低层模块,引入的IProduce接口就是抽象,其中,Factory使用IProduce接口生产零件,具体零件类实现了IProduce接口,这满足定义中的第一点。同时IProduce接口不依赖于具体零件类,它是一个独立的存在,这满足定义中的第二点。各个具体的零件类实现依赖于IProduce接口,这满足定义中的第三点。

依赖导致原则的好处非常明显,它降低了模块间的耦合性,提高系统的稳定性、灵活性、可维护性和可扩展性,降低了修改代码的风险和成本。

接口隔离原则(Interface Segregation Principle, ISP)

定义:

  1. 客户端不应该依赖它不需要的接口。
  2. 一个类对另一个类的依赖应该建立在最小的接口上。

假设有一个接口I,定义了一些方法,类A通过接口I依赖于类C,类B通过接口I依赖于类,但类Ahead类B都只依赖接口I的其中一部分方法。也就是说,接口I不是类A和类B的最小接口。这种情况下,类C和类D为了实现接口,就要实现那些类A和类B不需要的方法,如果后续还对接口I添加方法或还有其他类依赖于接口I,就会造成接口I过于臃肿,这是不好的设计。

解决方案是遵循接口隔离原则,该原则实际上是指导我们面向接口编程。应该分析类A和类B对接口的需求,将接口I拆分成多个小接口,客户类和实现类根据拆分后的接口做调整。

接口隔离原则要注意以下几点:

  1. 拆分和细化接口,不要让接口过于臃肿,另外也要注意接口细化的粒度,拆分地太细会导致复杂度升高,要适度拆分。
  2. 接口即契约,对一个模块制定接口时,只暴露出需要的功能,接口中不应有多余的功能存在。
  3. 设计模块时,要尽量提高模块的内聚性,尽量减少模块对外的依赖关系。

迪米特法则(Least Knowledge Principle, LKP, 最小知道原则)

定义:一个对象应该对其他对象保持最少的了解。

迪米特法则又叫作最少知道原则。软件工程提倡模块化,一个模块要低耦合,高内聚。当两个类彼此之间互相了解得越多,它们的耦合就越大,当其中一个类变化时,另一个类受的影响就越大。迪米特法则说的就是要尽量降低类与类之间的耦合。

类和类之间完全没有耦合是不可能的,如果完全不耦合,程序就没办法工作了。但是必须要保证是适当的耦合度,。个类互相耦合时,最好是将这个耦合度控制在类级别或方法级别,应避免互相去了解类的实现细节。

开闭原则(Open Closed Principle, OCP)

定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

开闭原则告诉我们当软件的需求有变化时,应尽量通过扩展软件的功能来响应变化,而不是通过修改已有的代码来响应变化。实际上开闭原则是其他五个原则的归纳汇总,其他五个原则都在教我们怎么做,其中都在阐述开闭原则这个核心思想。

开闭原则给我们的启示是:用抽象构建功能框架,用实现扩展功能细节。这是构建高可维护性、高可扩展性、高灵活性和高稳定性系统的最佳方式。