# 设计模式

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

# 模块化

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

# 面向对象

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