# 设计模式
- 开发一款软件时,需要设计软件的架构、基本原理。对此,人们总结了一些常用的架构,称为设计模式(design pattern)。
- 一种设计模式,可能擅长处理某种问题,但不适合处理其它类型的问题。
- 学习、借鉴前人的设计模式,可以帮助自己开发出更好的软件。也可以自己总结出新的设计模式。
# 模块化
- 模块化(modularity),是将软件的每个主要功能,用一块独立的代码实现。
- 假设一个软件,将所有功能写在一个函数里。如果改为模块化,则编写多个函数,分别实现一个功能。
- 在模块化的同时,还应该追求以下效果:
- 单一职责原则(Single Responsibility Principle,SRP)
- :每个模块只负责实现一个功能,不多管闲事,从而简化每个模块包含的代码。
- 如果一个模块实现了多个功能,则建立拆分成多个模块。除非这个模块的代码量较少,容易被人看懂。
- 高内聚(High Cohesion)
- :将实现一个功能的相关代码,都写到同一个模块中,从而集中到同一个命名空间,方便相互调用。
- 假设实现功能 A 时,需要先后调用模块 1 、模块 2 ,并且两个模块只负责实现功能 A 。则这样属于单一职责、低内聚。
- 低耦合(Low Coupling)
- :减少各个模块之间的联系,使得每个模块可以独立运行,互不干扰。
- 假设一个模块函数,输入函数参数就可以让它独立运行,不管其它模块是什么状态,则耦合度低,甚至无耦合。
- 假设一个模块函数,运行时必须依赖另一个模块函数,则耦合度高。如果不能减少两者的耦合度,可以考虑将两者合并为一个模块。
- 单一职责原则(Single Responsibility Principle,SRP)
- 优点:
- 可以将一个复杂的软件,拆分成许多个简单模块,逐个开发,从而简化软件的开发过程。
- 开发了软件的第一个版本之后,还可以多次修改、发布新版本,可扩展性好。
- 想修改软件的某个功能时,只需修改实现该功能的那个模块,不需要改动大量模块。
- 想给软件增加、减少一个功能时,只需增加、减少一个模块,不需要改动大量模块。
- 模块之间可以隔离风险。一个模块故障时,其它模块可以继续正常工作,除非相互耦合。
# 中介模式
如果代码 A (比如一个函数、类),直接调用另一段代码 B ,则两者通常耦合。
- 优点:
- 代码逻辑简单,容易编写。
- 缺点:
- 如果很多段代码相互耦合,则会很复杂,导致代码难以编写、阅读。
- 例如 5 段代码,需要两两相互调用,则有 20 种调用关系。
- 如果代码 B 发生异常(比如无响应、响应不符合预期),则会导致代码 A 也发生异常。
- 进一步地,可能引发级联故障。例如代码 A -> B -> C -> D 依次调用,如果代码 D 异常,则会导致 A、B、C 也发生异常。
- 如果很多段代码相互耦合,则会很复杂,导致代码难以编写、阅读。
- 优点:
为了让两段代码解耦,常见的一种方案是,引入第三段代码,作为中介。让代码 A 调用中介,然后中介调用代码 B 。
- 优点:
- 让两段代码解耦。
- 例如 N 段代码,需要两两相互调用,引入中介之后,调用关系会减少到 N×2 种。
- N 越大,中介的好处越大。
- 让两段代码解耦。
- 缺点:
- 代码 A 直接调用代码 B ,存在一次通信的耗时。而代码 A 调用中介,然后中介调用代码 B ,存在两次通信的耗时。
- 代码 A、B 分别与中介耦合。
- 为了降低耦合度,编写中介的代码时,应该只提供抽象的 API 。
- 还应该保证中介的稳定运行,能随时被其它代码调用。
- 优点:
# 命令模式
- 命令模式是中介模式的一种。
- 特点:代码 A 不直接调用代码 B 的 API ,而是发送一段字符串,表示代码 A 的需求,然后代码 B 会读取、理解该字符串,做出正确的行为。
- 例如:代码 B 可能存在几十个版本,API 经常变化。代码 A 不必考虑代码 B 的 API 怎么调用,只需发送
get 1.txt
字符串,代码 B 就会给出名为1.txt
的文件。 - 优点:
- 解耦。
- 可以实现命令式编程,或者声明式编程。
- 缺点:
- 需要花时间设计命令的语法,可以参考 SQL、Shell 等编程语言。
# 适配器模式
- 适配器模式是中介模式的一种。
- 假设代码 A 原本能调用代码 B 的 API ,但是代码 B 发布新版本之后,API 变化,不能直接被代码 A 调用。
- 此时,可以增加代码 C 作为适配器,一方面提供代码 B 的旧版 API ,另一方面调用代码 B 的新版 API 。调用流程变为代码 A -> C -> B
- 优点:
- 在不修改已有代码的情况下,让程序顺利工作。
- 缺点:
- 需要编写、维护适配器,增加了工作量。如果修改已有代码的成本很大,才值得使用适配器。
- 假设代码 A 原本能调用代码 B 的 API ,但是代码 B 发布新版本之后,API 变化,不能直接被代码 A 调用。
# 面向对象
面向对象编程中,提倡 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 ,分别实现一种功能,从而进一步模块化。
- 依赖倒置原则
- :几个类之间相互调用时,尽量只调用抽象接口,而不是具体接口,从而方便未来进行扩展。
- 假设一个类,提供的接口全是具体接口,则可以将它改造为抽象类,然后在子类中编写具体接口。调用时,不直接调用子类的具体接口,而是调用父类的抽象接口。
- 单一职责原则
组合优于继承
- :组合使用多个类时,尽量减少它们之间的继承关系。
- 组合、继承都能提高代码的复用性,但组合能避免类之间的耦合。
- 继承会破坏子类和父类的封装(导致它们的成员混合在一起),导致父类与子类高度耦合。
# 工厂模式
- 假设你为一家宠物店开发网站,定义了 Cat、Dog 等多个类,用于表示不同类型的宠物。
- 每次创建一个宠物对象时,需要判断该宠物的类型,然后调用对应的类,创建对象。代码示例:
if pet_type == 'cat': prepare_for_cat() cat = Cat() elif pet_type == 'dog': prepare_for_dog() dog = Dog()
- 每次创建一个宠物对象时,需要判断该宠物的类型,然后调用对应的类,创建对象。代码示例:
- 上述代码,可以改写为一个方法:
class PetFactory: @staticmethod def new(pet_type): if pet_type == 'cat': prepare_for_cat() return Cat() elif pet_type == 'dog': prepare_for_dog() return Dog() pet = PetFactory.new('cat')
- 这样的方法,像一个工厂,可以创建不同类型的对象。
- 特点:
- 存在多个相似的 class 时,用户不直接实例化这些 class ,而是通过一个工厂,间接实例化这些 class 。
- 这样也符合中介模式,由工厂担任中介。
- 优点:
- 用户不必关注具体的 class 有哪些、如何实例化(可能存在复杂的准备工作),只需调用工厂的简单 API ,即可创建一个对象。
- class 越多、实例化过程越复杂,工厂模式的优点越大。
- 缺点:
- 每次增加一个新的 class ,就需要修改上述 if 语句,不符合开闭原则。
- 为了符合开闭原则,可以将 if 语句改写为多个方法,每个方法用于实例化一个具体的 class :
class PetFactory: @staticmethod def new_cat(): prepare_for_cat() return Cat() @staticmethod def new_dog(): prepare_for_dog() return Dog()
- 或者为每个具体的 class ,分别定义一个工厂:
from abc import ABCMeta, abstractmethod # 这里存在多个相似的工厂,因此建议继承自同一个抽象的工厂类,提供相同的 API class PetFactory(metaclass=ABCMeta): @abstractmethod def new(self): pass class CatFactory(PetFactory): def new(self): prepare_for_cat() return Cat() class DogFactory(PetFactory): def new(self): prepare_for_dog() return Dog() pet = CatFactory().new()
# 建造者模式
C++、Java 等语言支持方法重载,可以定义多个构造函数,以多种方式来创建对象。
- 代码示例:
class Date { Date(int day) { ... } Date(int day, int month) { ... } Date(int day, int month, int year) { ... } };
- C++ 不支持构造函数之间相互调用,因此多个构造函数容易包含重复代码。
- Java 支持构造函数之间相互调用。
- Python 不支持方法重载。
- 代码示例:
如果构造函数较多、形参较多,则代码较复杂,可以改用建造者模式(builder pattern),又称为生成器模式。
- 原理:定义另一个类,专门负责初始化当前类的对象。
- 代码示例:
class DateBuilder { // 新建 Date 对象 reset() { this.date = new Date(); } // 定义多个方法,用于不同的初始化步骤 set_day(int day) { ... } set_month(int month) { ... } set_year(int year) { ... } }; // 执行一组初始化步骤 CarBuilder builder = new CarBuilder() builder.reset() builder.set_day(1) builder.set_month(1)
# 原型模式
假设你需要克隆某个对象,拷贝它的所有成员(包括变量、方法),通常会遇到以下问题:
- 每个成员的名称是什么?
- 每个成员都需要拷贝吗?
- 每个成员都允许拷贝吗?(比如私有成员,不一定能从外部访问)
上述问题,外部难以解答,而当前对象自己容易解答,因此可以让当前对象,自愿提供一个 clone() 方法,用于将自己克隆一份。
- 此时,当前对象称为一个原型(prototype),可以克隆生成其它对象。
- 代码示例:
class Pet: def __init__(self, name): self.name = name def clone(self): return self.__class__(self.name) pet = Pet('test') pet.clone()
# 单例模式
单例模式是指,某个类只存在一个实例对象。每次调用该类创建对象,都会返回同一个对象。
- 例如,假设执行代码
connect = ConnectDatabase()
可以连接数据库。采用单例模式的情况下,如果已经存在对数据库的 TCP 连接,则可以复用该连接,而不是创建新连接。 - 多线程运行代码时,每个线程会分别创建一个单例对象,需要注意区别,或者通过线程间通信保持一致。
- 例如,假设执行代码
Python 中,如果让
__new__()
总是返回同一个实例,则可以实现单例模式。>>> class Test: ... def __new__(cls): ... if not hasattr(cls, 'instance'): ... cls.instance = super().__new__(cls) ... return cls.instance ... >>> Test() <__main__.Test object at 0x00000140699F54C0> >>> Test() <__main__.Test object at 0x00000140699F54C0> >>> Test() is Test() True
# 流程控制
# 责任链模式
假设你在开发一个 Web 网站,每次收到一个 HTTP 请求,就需要执行多个步骤。
- 可能包含很多步骤,比如身份认证、检查权限、读取数据库、修改数据库。
- 如果将所有步骤,写在同一个函数中,则该函数会很臃肿、复杂。
可以将每个步骤分别用一个函数实现,每个函数执行完之后,将 HTTP 请求交给下一个函数处理。
- 优点:
- 模块化设计。按顺序执行多个函数,组成一条责任链。
- 可以实现灵活的处理流程。比如从第一个函数直接跳转最后一个函数。
- 缺点:
- 处理流程不固定,可能产生死循环。
- 代码示例:
def step1(data): handle(data) if ...: step2(data) elif ...: step3(data)
- 优点:
# 状态模式
- 某些对象,可能在多种状态之间切换,每种状态可能执行不同的行为,称为有限状态机。
- 处理这种问题时,通常需要判断对象当前的状态,然后做出决策。如下:
class Pet: 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 Pet: def __init__(self, state): self.state = StateA() def run(self): self.state.run() # 调用当前状态的行为方法
# 消息队列
- 消息队列的几种设计模式 (opens new window)
- 观察者模式
- 发布/订阅模式
- 生产/消费模式