# 设计模式
- 开发一款软件时,需要设计软件的架构、基本原理。对此,人们总结了一些常用的架构,称为设计模式(design pattern)。
- 一种设计模式,可能擅长处理某种问题,但不适合处理其它类型的问题。
- 学习、借鉴前人的设计模式,可以帮助自己开发出更好的软件。也可以自己总结出新的设计模式。
# 模块化
- 模块化(modularity),是将软件的每个主要功能,用一块独立的代码实现。
- 假设一个软件,将所有功能写在一个函数里。如果改为模块化,则编写多个函数,分别实现一个功能。
- 在模块化的同时,还应该追求以下效果:
- 单一职责原则(Single Responsibility Principle,SRP)
- :每个模块只负责实现一个功能,不多管闲事,从而简化每个模块包含的代码。
- 如果一个模块实现了多个功能,则建立拆分成多个模块。除非这个模块的代码量较少,容易被人看懂。
- 高内聚(High Cohesion)
- :将实现一个功能的相关代码,都写到同一个模块中,从而集中到同一个命名空间,方便相互调用。
- 假设实现功能 A 时,需要先后调用模块 1 、模块 2 ,并且两个模块只负责实现功能 A 。则这样属于单一职责、低内聚。
- 低耦合(Low Coupling)
- :减少各个模块之间的联系,使得每个模块可以独立运行,互不干扰。
- 假设一个模块函数,输入函数参数就可以让它独立运行,不管其它模块是什么状态,则耦合度低,甚至无耦合。
- 假设一个模块函数,运行时必须依赖另一个模块函数,则耦合度高。如果不能减少两者的耦合度,可以考虑将两者合并为一个模块。
- 单一职责原则(Single Responsibility Principle,SRP)
- 优点:
- 可以将一个复杂的软件,拆分成许多个简单模块,逐个开发,从而简化软件的开发过程。
- 开发了软件的第一个版本之后,还可以多次修改、发布新版本,可扩展性好。
- 想修改软件的某个功能时,只需修改实现该功能的那个模块,不需要改动大量模块。
- 想给软件增加、减少一个功能时,只需增加、减少一个模块,不需要改动大量模块。
- 模块之间可以隔离风险。一个模块故障时,其它模块可以继续正常工作,除非相互耦合。
# 面向对象
面向对象编程中,提倡 SOLID 五大原则:
- 单一职责原则
- :每个类只有一个职责。或者说只有一个主要职责,附加一些次要职责(比如打印日志)。
- 假设已经有一个类 File ,用于读写磁盘文件,专注于提供 read()、write() 方法,
- 如果想处理文件的路径,提供 split()、exists() 等方法,则应该新建一个类 Path ,而不是直接修改类 File 。
- 如果想解析 JSON 等格式的文件,提供 parse()、dump() 等方法,则应该新建一个类 Json ,而不是直接修改类 File 。
- 开闭原则(Open–Closed Principle)
- :每个模块正式发布之后,允许扩展其功能(持开放态度),不允许修改其源代码(持封闭态度)。
- 例如往软件中添加一个类 A ,被类 B 调用。
- 如果想扩展类 A 的功能,则不应该直接修改类 A ,否则可能影响类 B 的正常工作。
- 可以继承类 A ,定义一个子类,实现新功能。
- 理想的情况下,每个模块正式发布之后,永远不会修改其源代码,除非需要修复 bug 。
- 里氏替换原则
- :子类应该兼容父类的所有 API 。
- 假设执行代码
A.method1()
,调用对象 A 的一个方法。则将对象 A 改为子类对象时,这行代码也应该正常工作。 - 一般情况下,子类会继承父类的所有成员,因此符合该原则。如果子类重写了父类的某个方法,则可能违背该原则。
- 接口隔离原则
- :如果一个 API 实现了多种功能,则应该拆分成多个 API ,分别实现一种功能,从而进一步模块化。
- 依赖倒置原则
- :几个类之间相互调用时,尽量只调用抽象接口,而不是具体接口,从而方便未来进行扩展。
- 假设一个类,提供的接口全是具体接口,则可以将它改造为抽象类,然后在子类中编写具体接口。调用时,不直接调用子类的具体接口,而是调用父类的抽象接口。
- 单一职责原则
组合优于继承
- :组合使用多个类时,尽量减少它们之间的继承关系。
- 组合、继承都能提高代码的复用性,但组合能避免类之间的耦合。
- 继承会破坏子类和父类的封装(导致它们的成员混合在一起),导致父类与子类高度耦合。
# 中介模式
如果代码 A (比如一个函数、类),直接调用另一段代码 B ,则两者通常耦合。
- 优点:
- 代码逻辑简单,容易编写。
- 缺点:
- 如果很多段代码相互耦合,则工作流程会变得很复杂。
- 例如 5 段代码,需要两两相互调用,则有 20 种调用关系。这样的代码不方便阅读理解。
- 如果代码 B 发生异常(比如无响应、响应不符合预期),则代码 A 也会发生异常。
- 进一步地,可能引发级联故障。例如代码 A -> B -> C -> D 依次调用,如果代码 D 异常,则 A、B、C 也会发生异常。
- 如果很多段代码相互耦合,则工作流程会变得很复杂。
- 优点:
为了让两段代码解耦,常见的一种方案是,引入第三段代码,作为中介。让代码 A 调用中介,然后中介调用代码 B 。
- 优点:
- 让两段代码解耦。
- 例如 N 段代码,需要两两相互调用,引入中介之后,调用关系会减少到 N×2 种。
- N 越大,中介的好处越大。
- 让两段代码解耦。
- 缺点:
- 代码 A 调用代码 B ,存在一次通信的耗时。引入中介之后,存在两次通信的耗时,总耗时更久。
- 代码 A、B 分别与中介耦合。
- 编写中介的代码时,应该只提供抽象的 API ,刻意降低耦合度。
- 中介必须可靠地运行。如果中介发生异常,则会导致很多代码发生异常。
- 优点:
# 状态模式
- 有限状态机是一种常见的问题:这种对象,可以在多种状态之间切换,每种状态可能发生不同的行为。
- 为了处理这种问题,通常需要判断对象当前的状态,然后做出决策。如下:
class Test: def __init__(self, state): self.state = state def run(self): if self.state == 'A': print('A') elif self.state == 'B': print('B')
- 状态越多,上述代码的缺点越明显:
- 如果有 n 种状态,则需要编写 n 个 if 语句进行判断。
- 如果有 m 个方法,需要根据状态进行决策,则需要重复编写 if 语句进行判断,总共 n*m 个 if 语句。
- 如果新增、减少一种状态,则需要修改 m 个方法中的 if 语句。
- 为了简化代码,可以将每种状态,用一个类实现。这就是状态模式。如下:
from abc import ABCMeta, abstractmethod class State(metaclass=ABCMeta): # 定义一个抽象的状态类,声明各种行为方法 @abstractmethod def run(self): pass class StateA(State): # 定义一个具体的状态类,实现各种行为方法 def run(self): print('A') class StateB(State): def run(self): print('B') class Test: def __init__(self, state): self.state = StateA() def run(self): self.state.run() # 调用当前状态的行为方法
- 为了处理这种问题,通常需要判断对象当前的状态,然后做出决策。如下: