# 设计模式

  • 开发一款软件时,需要设计软件的架构、基本原理。对此,人们总结了一些常用的架构,称为设计模式(design pattern)。
    • 一种设计模式,可能擅长处理某种问题,但不适合处理其它类型的问题。
    • 学习、借鉴前人的设计模式,可以帮助自己开发出更好的软件。也可以自己总结出新的设计模式。

# 模块化

  • 模块化(modularity),是将软件的每个主要功能,用一块独立的代码实现。
    • 假设一个软件,将所有功能写在一个函数里。如果改为模块化,则编写多个函数,分别实现一个功能。
  • 在模块化的同时,还应该追求以下效果:
    • 单一职责原则(Single Responsibility Principle,SRP)
      • :每个模块只负责实现一个功能,不多管闲事,从而简化每个模块包含的代码。
      • 如果一个模块实现了多个功能,则建立拆分成多个模块。除非这个模块的代码量较少,容易被人看懂。
    • 高内聚(High Cohesion)
      • :将实现一个功能的相关代码,都写到同一个模块中,从而集中到同一个命名空间,方便相互调用。
      • 假设实现功能 A 时,需要先后调用模块 1 、模块 2 ,并且两个模块只负责实现功能 A 。则这样属于单一职责、低内聚。
    • 低耦合(Low Coupling)
      • :减少各个模块之间的联系,使得每个模块可以独立运行,互不干扰。
      • 假设一个模块函数,输入函数参数就可以让它独立运行,不管其它模块是什么状态,则耦合度低,甚至无耦合。
      • 假设一个模块函数,运行时必须依赖另一个模块函数,则耦合度高。如果不能减少两者的耦合度,可以考虑将两者合并为一个模块。
  • 优点:
    • 可以将一个复杂的软件,拆分成许多个简单模块,逐个开发,从而简化软件的开发过程。
    • 开发了软件的第一个版本之后,还可以多次修改、发布新版本,可扩展性好。
      • 想修改软件的某个功能时,只需修改实现该功能的那个模块,不需要改动大量模块。
      • 想给软件增加、减少一个功能时,只需增加、减少一个模块,不需要改动大量模块。
    • 模块之间可以隔离风险。一个模块故障时,其它模块可以继续正常工作,除非相互耦合。

# 中介模式

  • 如果代码 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
    • 优点:
      • 在不修改已有代码的情况下,让程序顺利工作。
    • 缺点:
      • 需要编写、维护适配器,增加了工作量。如果修改已有代码的成本很大,才值得使用适配器。

# 面向对象

  • 面向对象编程中,提倡 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()  # 调用当前状态的行为方法
    

# 消息队列