天天看点

设计模式-创建型模式 GoF

目录

GoF 23(分类)

OOP 七大原则

Creational Pattems

Singleton

背景

适用场景

实例-实现

理论-实现

与其他模式关系

Factory

与其他模式的关系

Abstract Factory

Builder

Prototype

阅读推荐:

设计模式-简单篇

设计模式-总篇

项目地址:https://gitee.com/zwtgit/gof23

学习网站推荐:

https://refactoringguru.cn/design-patterns/catalog

https://www.journaldev.com/1827/java-design-patterns-example-tutorial

设计模式是针对软件设计中常见问题的工具箱, 其中的工具就是各种经过实践验证的解决方案。 即使你从未遇到过这些问题, 了解模式仍然非常有用, 因为它能指导你如何使用面向对象的设计原则来解决各种问题。

算法更像是菜谱: 提供达成目标的明确步骤。 而模式更像是蓝图: 你可以看到最终的结果和模式的功能, 但需要自己确定实现步骤。

创建型模式:提供创建对象的机制, 增加已有代码的灵活性和可复用性。

结构型模式:介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。

行为模式:负责对象间的高效沟通和职责委派。

设计模式-创建型模式 GoF

创建型模式:

单例模式,工厂模式, 抽象工厂模式, 建造者模式, 原型模式

结构型模式:

适配器模式, 桥接模式, 装饰模式, 组合模式, 外观模式, 享元模式, 代理模式

行为型模式:

模板方法模式, 命令模式, 迭代器模式, 观察者模式, 中介者模式, 备忘录模式, 解释器模式, 状态模式, 策略模式, 职责链模式, 访问者模式

面向对象程序设计(Object Oriented Programming,OOP)。

1、开闭原则

对扩展开放, 对修改关闭。

2、单一职责原则

每个类应该实现单一的职责,不要存在多于一个导致类变更的原因,否则就应该把类拆分。该原则是实现高内聚、低耦合的指导方针。

3、里氏替换原则(Liskov Substitution Principle)

任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。

里氏替换原则是对开闭原则的补充。实现开闭原则的关键就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。

4、依赖倒转原则(Dependence Inversion Principle)

面向接口编程,依赖于抽象而不依赖于具体。用到具体类时,不与具体类交互,而与具体类的上层接口交互。

5、接口隔离原则(Interface Segregation Principle)

每个接口中不存在子类用不到却必须实现的方法,否则就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口中的方法聚合到一个的接口)要好。

6、迪米特法则(最少知道原则)(Demeter Principle)

一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过 public 方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。

7、合成复用原则(Composite Reuse Principle)

软件复用时,要先尽量使用组合或者聚合等关联关系实现,其次才考虑使用继承。即在一个新对象里通过关联的方式使用已有对象的一些方法和功能。

保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么, “某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。

如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。

如果你需要更加严格地控制全局变量,可以使用单例模式 。

Java 核心程序库中仍有相当多的单例示例:

<code>java.lang.Runtime#getRuntime()</code>

<code>java.awt.Desktop#getDesktop()</code>

<code>java.lang.System#getSecurityManager()</code>

基础单例(单线程)

基础单例(多线程)

相同的类在多线程环境中会出错。 多线程可能会同时调用构建方法并获取多个单例类的实例。

采用延迟加载的线程安全单例

为了解决这个问题, 你必须在创建首个单例对象时对线程进行同步。

Eager initialization

在预先初始化中,单例类的实例是在类加载时创建的,这是创建单例类的最简单方法,但它有一个缺点,即即使客户端应用程序可能不会使用它,也会创建实例。

如果您的单例类没有使用大量资源,则可以使用这种方法。但是在大多数情况下,Singleton 类是为文件系统、数据库连接等资源创建的。除非客户端调用该<code>getInstance</code>方法,否则我们应该避免实例化。此外,此方法不提供任何异常处理选项。

Static block initialization

Lazy Initialization

在单线程环境下工作正常,但是当涉及到多线程系统时,如果多个线程同时处于 if 条件内,则可能会导致问题。它将破坏单例模式,两个线程将获得单例类的不同实例。

Thread Safe Singleton

但由于与同步方法相关的成本,它降低了性能,尽管我们只需要它用于可能创建单独实例的前几个线程(阅读:Java 同步)。为了每次都避免这种额外的开销,使用了双重检查锁定原则。在这种方法中,同步块在 if 条件中使用,并进行额外检查以确保仅创建单例类的一个实例。

Bill Pugh Singleton Implementation

在 Java 5 之前,java 内存模型有很多问题,并且在某些场景中,如果太多线程试图同时获取 Singleton 类的实例,上述方法会失败。

请注意包含单例类实例的私有内部静态类。当加载单例类时,<code>SingletonHelper</code>类不会加载到内存中,只有当有人调用getInstance方法时,才会加载这个类并创建单例类实例。

这是 Singleton 类最广泛使用的方法,因为它不需要同步。我在我的许多项目中都使用了这种方法,而且它也很容易理解和实现。

Using Reflection to destroy Singleton Pattern

反射可用于破坏上述所有单例实现方法。让我们用一个示例类来看看这个。

您运行上面的测试类时,您会注意到两个实例的 hashCode 不相同,这破坏了单例模式。反射非常强大,并在很多框架中使用,如 Spring 和 Hibernate。

Enum Singleton

Serialization and Singleton

有时在分布式系统中,我们需要在 Singleton 类中实现 Serializable 接口,以便我们可以将其状态存储在文件系统中并在以后的某个时间点检索它。这是一个也实现了 Serializable 接口的小型单例类。

序列化单例类的问题在于,无论何时反序列化它,它都会创建该类的一个新实例。让我们用一个简单的程序来看看它。

所以它破坏了单例模式,为了克服这种情况,我们需要做的就是提供<code>readResolve()</code>方法的实现。

外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。

如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。

只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。

单例对象可以是可变的。 享元对象是不可变的。

抽象工厂模式、 生成器模式和原型模式都可以用单例来实现。

工厂方法模式是一种创建型设计模式, 其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。

设计模式-创建型模式 GoF

调用工厂方法的代码 (通常被称为客户端代码) 无需了解不同子类返回实际对象之间的差别。 客户端将所有产品视为抽象的 <code>运输</code> 。 客户端知道所有运输对象都提供 <code>交付</code>方法, 但是并不关心其具体实现方式。

当你在编写代码的过程中, 如果无法预知对象确切类别及其依赖关系时, 可使用工厂方法。

如果你希望用户能扩展你软件库或框架的内部组件, 可使用工厂方法。

如果你希望复用现有对象来节省系统资源, 而不是每次都重新创建对象, 可使用工厂方法。

工厂方法模式在 Java 代码中得到了广泛使用。 当你需要在代码中提供高层次的灵活性时, 该模式会非常实用。

java.util.Calendar、ResourceBundle 和 NumberFormat<code>getInstance()</code>方法使用工厂模式。

<code>valueOf()</code> Boolean、Integer 等包装类中的方法。

核心 Java 程序库中有该模式的应用:

<code>java.util.Calendar#getInstance()</code>

<code>java.util.ResourceBundle#getBundle()</code>

<code>java.text.NumberFormat#getInstance()</code>

<code>java.nio.charset.Charset#forName()</code>

<code>java.net.URLStreamHandlerFactory#createURLStreamHandler(String)</code> (根据协议返回不同的单例对象)

<code>java.util.EnumSet#of()</code>

<code>javax.xml.bind.JAXBContext#createMarshaller()</code> 及其他类似的方法。

识别方法: 工厂方法可通过构建方法来识别, 它会创建具体类的对象, 但以抽象类型或接口的形式返回这些对象。

生成跨平台的 GUI 元素

buttons

buttons/Button.java: 通用产品接口

buttons/HtmlButton.java: 具体产品

buttons/WindowsButton.java: 另一个具体产品

factory

factory/Dialog.java: 基础创建者

factory/HtmlDialog.java: 具体创建者

factory/WindowsDialog.java: 另一个具体创建者

Demo.java: 客户端代码

OutputDemo.txt: 执行结果 (Html­Dialog)

OutputDemo.png: 执行结果 (Windows­Dialog)

设计模式-创建型模式 GoF

在许多设计工作的初期都会使用工厂方法模式 (较为简单, 而且可以更方便地通过子类进行定制), 随后演化为使用抽象工厂模式、 原型模式或生成器模式 (更灵活但更加复杂)。

抽象工厂模式通常基于一组工厂方法, 但你也可以使用原型模式来生成这些类的方法。

你可以同时使用工厂方法和迭代器模式来让子类集合返回不同类型的迭代器, 并使得迭代器与集合相匹配。

原型并不基于继承, 因此没有继承的缺点。 另一方面, 原型需要对被复制对象进行复杂的初始化。 工厂方法基于继承, 但是它不需要初始化步骤。

工厂方法是模板方法模式的一种特殊形式。 同时, 工厂方法可以作为一个大型模板方法中的一个步骤。

抽象工厂模式是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。

如果代码需要与多个不同系列的相关产品交互, 但是由于无法提前获取相关信息, 或者出于对未来扩展性的考虑, 你不希望代码基于产品的具体类进行构建, 在这种情况下, 你可以使用抽象工厂。

如果你有一个基于一组抽象方法的类, 且其主要功能因此变得不明确, 那么在这种情况下可以考虑使用抽象工厂模式。

使用示例: 抽象工厂模式在 Java 代码中很常见。 许多框架和程序库会将它作为扩展和自定义其标准组件的一种方式。

以下是来自核心 Java 程序库的一些示例:

<code>javax.xml.parsers.DocumentBuilderFactory#newInstance()</code>

<code>javax.xml.transform.TransformerFactory#newInstance()</code>

<code>javax.xml.xpath.XPathFactory#newInstance()</code>

识别方法: 我们可以通过方法来识别该模式——其会返回一个工厂对象。 接下来, 工厂将被用于创建特定的子组件。

跨平台 GUI 组件系列及其创建方式

在本例中, 按钮和复选框将被作为产品。 它们有两个变体: macOS 版和 Windows 版。

抽象工厂定义了用于创建按钮和复选框的接口。 而两个具体工厂都会返回同一变体的两个产品。

客户端代码使用抽象接口与工厂和产品进行交互。 同样的代码能与依赖于不同工厂对象类型的多种产品变体进行交互。

buttons: 第一个产品层次结构

buttons/Button.java

buttons/MacOSButton.java

buttons/WindowsButton.java

checkboxes: 第二个产品层次结构

checkboxes/Checkbox.java

checkboxes/MacOSCheckbox.java

checkboxes/WindowsCheckbox.java

factories

factories/GUIFactory.java: 抽象工厂

factories/MacOSFactory.java: 具体工厂 ( mac­OS)

factories/WindowsFactory.java: 具体工厂 (Windows)

app

app/Application.java: 客户端代码

Demo.java: 程序配置

生成器重点关注如何分步生成复杂对象。 抽象工厂专门用于生产一系列相关对象。 抽象工厂会马上返回产品, 生成器则允许你在获取产品前执行一些额外构造步骤。

当只需对客户端代码隐藏子系统创建对象的方式时, 你可以使用抽象工厂来代替外观模式。

你可以将抽象工厂和桥接模式搭配使用。 如果由桥接定义的抽象只能与特定实现合作, 这一模式搭配就非常有用。 在这种情况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代码隐藏其复杂性。

抽象工厂、 生成器和原型都可以用单例模式来实现。

亦称:建造者模式、Builder

使用生成器模式可避免 “重叠构造函数 (telescopic constructor)” 的出现。

当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时, 可使用生成器模式。

使用生成器构造组合树或其他复杂对象。

生成器在 Java 核心程序库中得到了广泛的应用:

<code>java.lang.StringBuilder#append()</code> ( <code>非同步</code> )

<code>java.lang.StringBuffer#append()</code> ( <code>同步</code> )

<code>java.nio.ByteBuffer#put()</code> (还有 <code>Char­Buffer</code>、 <code>Short­Buffer</code>、 <code>Int­Buffer</code>、 <code>Long­Buffer</code>、 <code>Float­Buffer</code> 和 <code>Double­Buffer</code>)

<code>javax.swing.GroupLayout.Group#addComponent()</code>

<code>java.lang.Appendable</code>的所有实现

识别方法: 生成器模式可以通过类来识别, 它拥有一个构建方法和多个配置结果对象的方法。 生成器方法通常支持链式编程 (例如 <code>someBuilder-&gt;setValueA(1)-&gt;setValueB(2)-&gt;create()</code> )。

分步制造汽车

在本例中, 生成器模式允许你分步骤地制造不同型号的汽车。

示例还展示了生成器如何使用相同的生产过程制造不同类型的产品 (汽车手册)。

主管控制着构造顺序。 它知道制造各种汽车型号需要调用的生产步骤。 它仅与汽车的通用接口进行交互。 这样就能将不同类型的生成器传递给主管了。

最终结果将从生成器对象中获得, 因为主管不知道最终产品的类型。 只有生成器对象知道自己生成的产品是什么。

builders

builders/Builder.java: 通用生成器接口

builders/CarBuilder.java: 汽车生成器

builders/CarManualBuilder.java: 汽车手册生成器

cars

cars/Car.java: 汽车产品

cars/Manual.java: 手册产品

cars/CarType.java

components

components/Engine.java: 产品特征 1

components/GPSNavigator.java: 产品特征 2

components/Transmission.java: 产品特征 3

components/TripComputer.java: 产品特征 4

director

director/Director.java: 主管控制生成器

OutputDemo.txt: 执行结果

你可以在创建复杂组合模式树时使用生成器, 因为这可使其构造步骤以递归的方式运行。

你可以结合使用生成器和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。

原型模式是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类。

原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个 <code>克隆</code>方法。

所有的类对 <code>克隆</code>方法的实现都非常相似。 该方法会创建一个当前类的对象, 然后将原始对象所有的成员变量值复制到新建的类中。 你甚至可以复制私有成员变量, 因为绝大部分编程语言都允许对象访问其同类对象的私有成员变量。

支持克隆的对象即为原型。 当你的对象有几十个成员变量和几百种类型时, 对其进行克隆甚至可以代替子类的构造。

其运作方式如下: 创建一系列不同类型的对象并不同的方式对其进行配置。 如果所需对象与预先配置的对象相同, 那么你只需克隆原型即可, 无需新建一个对象。

如果你需要复制一些对象, 同时又希望代码独立于这些对象所属的具体类, 可以使用原型模式。

如果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量。 别人创建这些子类的目的可能是为了创建特定类型的对象。

使用示例: Java 的 <code>Cloneable</code> (可克隆) 接口就是立即可用的原型模式。

任何类都可通过实现该接口来实现可被克隆的性质。

java.lang.Object#clone() (类必须实现 <code>java.lang.Cloneable</code> 接口)

识别方法: 原型可以简单地通过 <code>clone</code>或 <code>copy</code>等方法来识别。

复制图形

让我们来看看在不使用标准 <code>Cloneable</code>接口的情况下如何实现原型模式。

shapes: 形状列表

shapes/Shape.java: 通用形状接口

shapes/Circle.java: 简单形状

shapes/Rectangle.java: 另一个形状

Demo.java: 克隆示例

原型可用于保存命令模式的历史记录。

大量使用组合模式和装饰模式的设计通常可从对于原型的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。

有时候原型可以作为备忘录模式的一个简化版本, 其条件是你需要在历史记录中存储的对象的状态比较简单, 不需要链接其他外部资源, 或者链接可以方便地重建。