设计模式(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 种设计模式。
TODO
使用 Python 语言实现以下设计模式
# 设计模式原则
提到设计模式,不可避免地要谈谈设计模式的六大原则:
- 单一职责原则
- 里式替换原则
- 依赖倒置原则
- 接口隔离原则
- 迪米特法则
- 开闭原则
# 单一职责原则(Single Responsibility Principle, SRP)
定义:不要存在多于一个导致类变更的原因。
其实不光是类,对于接口、类和方法都要遵循单一职责原则。单一职责原则从字面上很好理解,一个接口、类和方法只做一件事。
单一职责原则(SRP)表明一个类有且只有一个职责。一个类就像容器一样,它能添加任意数量的属性、方法等。然而,如果你试图让一个类实现太多,很快这个类就会变得笨重。任意小的改变都将导致这个单一类的变化。当你改了这个类,你将需要重新测试一遍。如果你遵守 SRP,你的类将变得简洁和灵活。每一个类将负责单一的问题、任务或者它关注的点,这种方式你只需要改变相应的类,只有这个类需要再次测试。SRP 核心是把整个问题分为小部分,并且每个小部分都将通过一个单独的类负责。
假设你在构建一个应用程序,其中有个模块是根据条件搜索顾客并以 Excel 形式导出。随着业务的发展,搜索条件会不断增加,导出数据的分类也会不断增加。如果此时将搜索与数据导出功能放在同一个类中,势必会变的笨重起来,即使是微小的改动,也可能影响其他功能。所以根据单一职责原则,一个类只有一个职责,故创建两个单独的类,分别处理搜索以及导出数据
遵循单一职责原则有如下好处:
- 每个接口、类和方法只承担一项职责,意义更清晰。
- 使代码可读性、可维护性和可扩展性更佳。
- 当需求有变化需要修改代码时,只需要修改相应功能的代码,不会对其他功能的代码造成影响。
# 里式替换原则(Liskov Substitution Principle, LSP)
定义:如果对每一个类型为 S 的对象 o1,都有类型为 T 的对象 o2,使得以 T 定义的所有程序 P 在所有的对象 o1 代换 o2 时,程序 P 的行为没有变化,那么类型 S 是类型 T 的子类型。
上面的定义是严格的定义,比较通俗的定义是:所有引用基类(父类)的地方必须能透明地使用其子类的对象。
里氏替换原则指出,派生的子类应该是可替换基类的,也就是说任何基类可以出现的地方,子类一定可以出现。值得注意的是,当你通过继承实现多态行为时,如果派生类没有遵守 LSP,可能会让系统引发异常。所以请谨慎使用继承,只有确定是“is-a”的关系时才使用继承。
里式替换原则包含如下几个含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类已经实现的方法(即非抽象方法)。
- 子类可以增加自己特有的方法。
- 当子类重载父类的方法时,方法的前置条件(入参)要比父类更宽松。
- 当子类实现父类的抽象方法时,方法的后置条件(出参)要比父类更严格。
# 依赖倒置原则(Dependence Inversion Principle, DIP)
定义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象。
- 抽象不应该依赖细节。
- 细节应该依赖抽象。
依赖倒置原则(DIP)表明高层模块不应该依赖低层模块,相反,他们应该依赖抽象类或者接口。这意味着你不应该在高层模块中使用具体的低层模块。因为这样的话,高层模块变得紧耦合低层模块。如果明天,你改变了低层模块,那么高层模块也会被修改。根据 DIP 原则,高层模块应该依赖抽象(以抽象类或者接口的形式),低层模块也是如此。通过面向接口(抽象类)编程,紧耦合被移除。
那么什么是高层模块,什么是低层模块呢?通常情况下,我们会在一个类(高层模块)的内部实例化它依赖的对象(低层模块),这样势必造成两者的紧耦合,任何依赖对象的改变都将引起类的改变。
设想一个情景,工厂生产零件,其中有工厂类 Factory、螺丝类 Screw、钉子类 Nail 等。如果 Factory 直接依赖于某个零件类,当需要生产新的零件时,势必要修改 Factory,这样一来 Factory 和各个零件类的耦合就非常大。为了解决这个问题,我们可以引入一个接口IProduce
,所有零件类都实现这个接口,Factory 直接使用 IProduce 接口来进行生产,之后如果有新的零件类加入进来,只需要实现 IProduce 接口,Factory 不需要做任何修改。
这个例子中,Factory 类是高层模块,具体的零件类是低层模块,引入的 IProduce 接口就是抽象,其中,Factory 使用 IProduce 接口生产零件,具体零件类实现了 IProduce 接口,这满足定义中的第一点。同时 IProduce 接口不依赖于具体零件类,它是一个独立的存在,这满足定义中的第二点。各个具体的零件类实现依赖于 IProduce 接口,这满足定义中的第三点。
依赖导致原则的好处非常明显,它降低了模块间的耦合性,提高系统的稳定性、灵活性、可维护性和可扩展性,降低了修改代码的风险和成本。
# 接口隔离原则(Interface Segregation Principle, ISP)
定义:
- 客户端不应该依赖它不需要的接口。
- 一个类对另一个类的依赖应该建立在最小的接口上。
假设有一个接口 I,定义了一些方法,类 A 通过接口 I 依赖于类 C,类 B 通过接口 I 依赖于类,但类 Ahead 类 B 都只依赖接口 I 的其中一部分方法。也就是说,接口 I 不是类 A 和类 B 的最小接口。这种情况下,类 C 和类 D 为了实现接口,就要实现那些类 A 和类 B 不需要的方法,如果后续还对接口 I 添加方法或还有其他类依赖于接口 I,就会造成接口 I 过于臃肿,这是不好的设计。
解决方案是遵循接口隔离原则,该原则实际上是指导我们面向接口编程。应该分析类 A 和类 B 对接口的需求,将接口 I 拆分成多个小接口,客户类和实现类根据拆分后的接口做调整。
接口隔离原则(ISP)表明类不应该被迫依赖他们不使用的方法,也就是说一个接口应该拥有尽可能少的行为,它是精简的,也是单一的。
接口隔离原则要注意以下几点:
- 拆分和细化接口,不要让接口过于臃肿,另外也要注意接口细化的粒度,拆分地太细会导致复杂度升高,要适度拆分。
- 接口即契约,对一个模块制定接口时,只暴露出需要的功能,接口中不应有多余的功能存在。
- 设计模块时,要尽量提高模块的内聚性,尽量减少模块对外的依赖关系。
# 迪米特法则(Least Knowledge Principle, LKP, 最小知道原则)
定义:一个对象应该对其他对象保持最少的了解。
迪米特法则又叫作最少知道原则。软件工程提倡模块化,一个模块要低耦合,高内聚。当两个类彼此之间互相了解得越多,它们的耦合就越大,当其中一个类变化时,另一个类受的影响就越大。迪米特法则说的就是要尽量降低类与类之间的耦合。
类和类之间完全没有耦合是不可能的,如果完全不耦合,程序就没办法工作了。但是必须要保证是适当的耦合度,。个类互相耦合时,最好是将这个耦合度控制在类级别或方法级别,应避免互相去了解类的实现细节。
# 开闭原则(Open Closed Principle, OCP)
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
开放封闭原则(OCP)指出,一个类应该对扩展开放,对修改关闭。这意味一旦你创建了一个类并且应用程序的其他部分开始使用它,你不应该修改它。为什么呢?因为如果你改变它,很可能你的改变会引发系统的崩溃。如果你需要一些额外功能,你应该扩展这个类而不是修改它。使用这种方式,现有系统不会看到任何新变化的影响。同时,你只需要测试新创建的类。
开闭原则告诉我们当软件的需求有变化时,应尽量通过扩展软件的功能来响应变化,而不是通过修改已有的代码来响应变化。实际上开闭原则是其他五个原则的归纳汇总,其他五个原则都在教我们怎么做,其中都在阐述开闭原则这个核心思想。
开闭原则给我们的启示是:用抽象构建功能框架,用实现扩展功能细节。这是构建高可维护性、高可扩展性、高灵活性和高稳定性系统的最佳方式。
I might say that I have a visitor pattern, but in any language with first class functions it will be just a function taking a function. Instead of factory class I usually have just a factory function. I might say I have an interface, but then it's just a couple of methods marked with comments, because there wouldn't be any other implementation (of course in python an interface is always just comments, because it's duck-typed). I still speak of the code as using the pattern, because it's a useful way to think about it, but don't actually type in all the stuff until I really need it.