设计模式
设计模式
六大原则
开闭原则
(Open Close Principle)
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
里氏代换原则
(Liskov Substitution Principle)
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP 是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科
依赖倒转原则
(Dependence Inversion Principle)
这个是开闭原则的基础,具体内容:是对接口编程,依赖于抽象而不依赖于具体。
接口隔离原则
(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。
迪米特法则
(最少知道原则)(Demeter Principle)
为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
合成复用原则
(Composite Reuse Principle)
原则是尽量使用合成/聚合的方式,而不是使用继承。
创建型模式
这类模式提供创建对象的机制, 能够提升已有代码的灵活性和可复用性。
简单工厂模式 ⭐
(Simple Factory)
严格来说,简单工厂模式不是 GoF 总结出来的 23 种设计模式之一。
简单工厂方法模式在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。
工厂方法返回的对象通常被称作 “产品”。
所有产品都必须使用同一接口。
在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
适用场景
- 当一个类不知道它所必须创建的对象的类的时候。
- 当一个类希望由它的子类来指定它所创建的对象的时候。
- 当类将创建对象的职责委托给多个子类中的某一个。
实现方式
- 创建所有产品的弗雷,使所有产品都遵循同一接口。 该接口必须声明对所有产品都有意义的方法。
- 添加一个空的工厂方法。 该方法的返回类型必须遵循通用的产品接口。
- 在创建者代码中找到对于产品构造函数的所有引用。 将它们依次替换为对于工厂方法的调用, 同时将创建产品的代码移入工厂方法。
- 你可能需要在工厂方法中添加临时参数(比如一个代表产品名的字符串)来控制返回的产品类型。
- 工厂方法的代码看上去可能非常糟糕。 其中可能会有复杂的 switch分支运算符, 用于选择各种需要实例化的产品类。 我们将在工厂模式中修复这个问题。
示例
Python
|  |  | 
- Shape(父类 or 基类):提取出所有子类的重复方法代码作为接口
- Circle(Shape 子类 or 派生类):作用为画圆形
- Rectangle(Shape 子类 or 派生类):作用为画矩形
- ShapeFactory(新式类):该类作用为用户可根据该类对象创建指定的 Shape 子类对象(Circle or Rectangle)
特点
优点:客户端(调用时)不需要修改代码。
缺点: 当需要增加新的运算类的时候,不仅需新加运算类,还要修改工厂类,违反了开闭原则。
工厂方法模式 ⭐
(Factory Method,虚拟构造函数,Virtual Constructor)

简单工厂模式只有一个工厂,工厂方法模式对每一个产品都有相应的工厂
工厂方法模式是简单工厂模式的衍生,解决了许多简单工厂模式的问题。
- 首先完全实现‘开-闭 原则’,实现了可扩展。
- 其次更复杂的层次结构,可以应用于产品结果复杂的场合。
工厂方法模式的对简单工厂模式进行了抽象。有一个抽象的 Factory 类(可以是抽象类和接口),这个类将不在负责具体的产品生产,而是只制定一些规范,具体的生产工作由其子类去完成。在这个模式中,工厂类和产品类往往可以依次对应。即**一个抽象工厂对应一个抽象产品,一个具体工厂对应一个具体产品,**这个具体的工厂就负责生产对应的产品。
工厂方法模式是最典型的模板方法模式应用。
适用场景
- 当你在编写代码的过程中,如果无法预知对象确切类别及其依赖关系时,可使用工厂方法。
- 如果你希望用户能扩展你软件库或框架的内部组件, 可使用工厂方法。
- 如果你希望复用现有对象来节省系统资源, 而不是每次都重新创建对象, 可使用工厂方法。
实现方式
- 在简单工厂模式的基础上,为工厂方法中的每种产品编写一个创建者子类, 然后在子类中重写工厂方法, 并将基本方法中的相关创建代码移动到工厂方法中。 
- 如果应用中的产品类型太多, 那么为每个产品创建子类并无太大必要, 这时你也可以在子类中复用基类中的控制参数。 - 例如, 设想你有以下一些层次结构的类。 基类 - 邮件及其子类- 航空邮件和- 陆路邮件;- 运输及其子类- 飞机,- 卡车和- 火车。- 航空邮件仅使用- 飞机对象, 而- 陆路邮件则会同时使用- 卡车和- 火车对象。 你可以编写一个新的子类 (例如- 火车邮件) 来处理这两种情况, 但是还有其他可选的方案。 客户端代码可以给- 陆路邮件类传递一个参数, 用于控制其希望获得的产品。
- 如果代码经过上述移动后, 基础工厂方法中已经没有任何代码, 你可以将其转变为抽象类。 如果基础工厂方法中还有其他语句, 你可以将其设置为该方法的默认行为。 
示例
Python
|  |  | 
特点
优点:增加一个运算类(例如 N 次方类),只需要增加运算类和相对应的工厂,两个类,不需要修改工厂类。
缺点:增加运算类,会修改客户端代码,工厂方法只是把简单工厂的内部逻辑判断移到了客户端进行
抽象工厂模式 ⭐
(Abstract Factory)

每一个模式都是针对一定问题的解决方案。
抽象工厂模式与工厂方法模式的最大区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则需要面对多个产品等级结构。

假设一个子系统需要一些产品对象,而这些产品又属于一个以上的产品等级结构。那么为了将消费这些产品对象的责任和创建这些产品对象的责任分割开来,可以引进抽象工厂模式。这样的话,消费产品的一方不需要直接参与产品的创建工作,而只需要向一个公用的工厂接口请求所需要的产品。
抽象工厂模式可以创建出分属于不同产品等级结构的一个产品族中的所有对象。对应于每一个产品族都有一个具体工厂。而每一个具体工厂负责创建属于同一个产品族,但是分属于不同等级结构的产品。
通过使用抽象工厂模式,可以处理具有相同(或者相似)等级结构中的多个产品族中的产品对象的创建问题。
适用场景
- 如果代码需要与多个不同系列的相关产品交互, 但是由于无法提前获取相关信息, 或者出于对未来扩展性的考虑, 你不希望代码基于产品的具体类进行构建, 在这种情况下, 你可以使用抽象工厂。
- 如果你有一个基于一组抽象方法的类, 且其主要功能因此变得不明确, 那么在这种情况下可以考虑使用抽象工厂模式。
实现方式
- 以不同的产品类型与产品变体为维度绘制矩阵。
- 为所有产品声明抽象产品接口。 然后让所有具体产品类实现这些接口。
- 声明抽象工厂接口, 并且在接口中为所有抽象产品提供一组构建方法。
- 为每种产品变体实现一个具体工厂类。
- 在应用程序中开发初始化代码。 该代码根据应用程序配置或当前环境, 对特定具体工厂类进行初始化。 然后将该工厂对象传递给所有需要创建产品的类。
- 找出代码中所有对产品构造函数的直接调用, 将其替换为对工厂对象中相应构建方法的调用。
示例
Python
|  |  | 
特点
优点
- 确保同一工厂生成的产品相互匹配。
- 避免客户端和具体产品代码的耦合。
- 单一职责原则。 你可以将产品生成代码抽取到同一位置, 使得代码易于维护。
- 开闭原则。 向应用程序中引入新产品变体时, 你无需修改客户端代码。
缺点
- 由于采用该模式需要向应用中引入众多接口和类, 代码可能会比之前更加复杂。
建造者模式 ⭐
(Builder,生成器模式)

建造者模式能够分步骤步骤创建复杂对象。 它允许你使用相同的创建代码生成不同类型的对象。
它将一个复杂对象的构建与他的表示分离,使得同样的构建过程可以创建不同的表示。

如果为每种可能的对象都创建一个子类, 这可能会导致程序变得过于复杂。
生成器模式建议将对象构造代码从产品类中抽取出来, 并将其放在一个名为生成器的独立对象中。

你无须调用所有步骤
适用场景
- 使用生成器模式可避免 “重叠构造函数 (telescopic constructor)” 的出现。 - 假设你的构造函数中有十个可选参数, 那么调用该函数会非常不方便; 因此, 你需要重载这个构造函数, 新建几个只有较少参数的简化版。 但这些构造函数仍需调用主构造函数, 传递一些默认数值来替代省略掉的参数。 - 1 2 3 4 5- class Pizza { Pizza(int size) { ... } Pizza(int size, boolean cheese) { ... } Pizza(int size, boolean cheese, boolean pepperoni) { ... } // ...
- 生成器模式让你可以分步骤生成对象, 而且允许你仅使用必须的步骤。 应用该模式后, 你再也不需要将几十个参数塞进构造函数里了。 
 
- 当你希望使用代码创建不同形式的产品,而它们的制造过程相似且仅有细节上的差异时, 可使用生成器模式 
实现方式
- 清晰地定义通用步骤, 确保它们可以制造所有形式的产品。 否则你将无法进一步实施该模式。 
- 在基本生成器接口中声明这些步骤。 
- 为每个形式的产品创建具体生成器类, 并实现其构造步骤。 - 不要忘记实现获取构造结果对象的方法。 你不能在生成器接口中声明该方法, 因为不同生成器构造的产品可能没有公共接口, 因此你就不知道该方法返回的对象类型。 但是, 如果所有产品都位于单一类层次中, 你就可以安全地在基本接口中添加获取生成对象的方法。 
- 考虑创建主管类。 它可以使用同一生成器对象来封装多种构造产品的方式。 
- 客户端代码会同时创建生成器和主管对象。 构造开始前, 客户端必须将生成器对象传递给主管对象。 通常情况下, 客户端只需调用主管类构造函数一次即可。 主管类使用生成器对象完成后续所有制造任务。 还有另一种方式, 那就是客户端可以将生成器对象直接传递给主管类的制造方法。 
- 只有在所有产品都遵循相同接口的情况下, 构造结果可以直接通过主管类获取。 否则, 客户端应当通过生成器获取构造结果。 
示例
Python
|  |  | 
原型模式 ⭐
(Prototype,克隆,Clone)

原型模式使你能够复制已有对象, 而又无需使代码依赖它们所属的类。
它用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个 克隆 方法。
所有的类对 克隆 方法的实现都非常相似。 该方法会创建一个当前类的对象, 然后将原始对象所有的成员变量值复制到新建的类中。 你甚至可以复制私有成员变量, 因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量。
支持克隆的对象即为原型。 当你的对象有几十个成员变量和几百种类型时, 对其进行克隆甚至可以代替子类的构造。
适用场景
- 如果你需要复制一些对象, 同时又希望代码独立于这些对象所属的具体类, 可以使用原型模式。- 这一点考量通常出现在代码需要处理第三方代码通过接口传递过来的对象时。 即使不考虑代码耦合的情况, 你的代码也不能依赖这些对象所属的具体类, 因为你不知道它们的具体信息。
- 原型模式为客户端代码提供一个通用接口, 客户端代码可通过这一接口与所有实现了克隆的对象进行交互, 它也使得客户端代码与其所克隆的对象具体类独立开来。
 
- 如果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量。 别人创建这些子类的目的可能是为了创建特定类型的对象。- 在原型模式中, 你可以使用一系列预生成的、 各种类型的对象作为原型。
- 客户端不必根据需求对子类进行实例化, 只需找到合适的原型并对其进行克隆即可。
 
实现方式
- 创建原型接口, 并在其中声明 - 克隆方法。 如果你已有类层次结构, 则只需在其所有类中添加该方法即可。
- 原型类必须另行定义一个以该类对象为参数的构造函数。 构造函数必须复制参数对象中的所有成员变量值到新建实体中。 如果你需要修改子类, 则必须调用父类构造函数, 让父类复制其私有成员变量值。 - 如果编程语言不支持方法重载, 那么你可能需要定义一个特殊方法来复制对象数据。 在构造函数中进行此类处理比较方便, 因为它在调用 - new运算符后会马上返回结果对象。
- 克隆方法通常只有一行代码: 使用 - new运算符调用原型版本的构造函数。 注意, 每个类都必须显式重写克隆方法并使用自身类名调用- new运算符。 否则, 克隆方法可能会生成父类的对象。
- 你还可以创建一个中心化原型注册表, 用于存储常用原型。 - 你可以新建一个工厂类来实现注册表, 或者在原型基类中添加一个获取原型的静态方法。 该方法必须能够根据客户端代码设定的条件进行搜索。 搜索条件可以是简单的字符串, 或者是一组复杂的搜索参数。 找到合适的原型后, 注册表应对原型进行克隆, 并将复制生成的对象返回给客户端。 - 最后还要将对子类构造函数的直接调用替换为对原型注册表工厂方法的调用。 
示例
Python
|  |  | 
特点
优点
- 你可以克隆对象, 而无需与它们所属的具体类相耦合
- 你可以克隆预生成原型, 避免反复运行初始化代码
- 你可以更方便地生成复杂对象
- 你可以用继承以外的方式来处理复杂对象的不同配置
缺点
- 克隆包含循环引用的复杂对象可能会非常麻烦
单例模式 ⭐
(Singleton)
单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

单例模式同时解决了两个问题, 所以违反了_单一职责原则_:
- 保证一个类只有一个实例。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源的访问权限(例如数据库或文件) - 它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。
- 注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。
- 客户端甚至可能没有意识到它们一直都在使用同一个对象。
 
- 为该实例提供一个全局访问节点。 那些存储重要对象的全局变量在使用上十分方便,但同时也非常不安全,因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。 - 和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。
- 还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。
 
如今, 单例模式已经变得非常流行, 以至于人们会将只解决上文描述中任意一个问题的东西称为单例。

适用场景
- 如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。- 单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。该方法可以创建一个新对象, 但如果该对象已经被创建,则返回已有的对象。
 
- 如果你需要更加严格地控制全局变量,可以使用单例模式。- 单例模式与全局变量不同,它保证类只存在一个实例。除了单例类自己以外, 无法通过任何方式替换缓存的实例。
 
请注意,你可以随时调整限制并设定生成单例实例的数量,只需修改 获取实例方法,即 getInstance 中的代码即可实现。
实现方式
- 在类中添加一个私有静态成员变量用于保存单例实例。
- 声明一个公有静态构建方法用于获取单例实例。
- 在静态方法中实现 “延迟初始化”。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
- 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。
- 检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。
示例
Python
|  |  | 
结果
|  |  | 
结构型模式
这类模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。
行为模式
这类模式负责对象间的高效沟通和职责委派。