Design Pattern (1)

创建型模式是对类的实例化过程的抽象化。

  • 怎样创建对象?
  • 创建哪些对象?
  • 如何组合和表示这些对象?

创建模式描述了怎样构造和封装这些动态决定

Simple Factory 简单工厂

别名:Static Factory Method 静态工厂模式

目的:

  • 由一个工厂对象决定创建出哪一种产品类的实例

动机:

  • 负责将大量有共同接口的类实例化

示例

通过一个水果工厂产生有共同接口的水果类。

基本实现流程

  • 具体产品类:将需要创建的各种不同产品对象的相关代码封装到具体产品类中
  • 抽象产品类:将具体产品类公共的代码进行抽象和提取后封装在一个抽象产品类中
  • 工厂类:提供一个工厂类用于创建各种产品,在工厂类中提供一个创建产品的工厂方法,该方法可以根据所传入参数的不同创建不同的具体产品对象
  • 客户端:只需调用工厂类的工厂方法并传入相应的参数即可得到

定义

简单工厂模式(Simple Factory Pattern):定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类(或实现共同的接口)

  • 在简单工厂模式中用于创建实例的方法通常是静态方法,因此又被称为静态工厂方法(Static Factory Method)模式
  • 要点:如果需要什么,只需要传入一个正确的参数,就可以获取所需要的对象,而无须知道其创建细节。
  • Parameterized Factory Methods

结构与实现

结构

先来看看简单工厂模式的结构类图:

参与者:

  • Product:定义产品共同拥有的接口
  • Concrete Product:具体产品,实现Product接口
  • Creator(工厂类):静态方法含有与应用紧密相关的业务逻辑,在客户端的直接调用下创建产品对象

实现

典型的抽象产品类代码

// 也可以声明为接口
public abstract class Product {
// 所有产品类的公共业务方法
public void methodSame() {
// 公共方法的实现
}

public abstract void methodDiff();
}

典型的具体产品类的代码

public class ConcreteProduct extends Product {
// 实现业务方法
public void methodDiff() {
// 业务方法的实现
}
}

典型的工厂类代码

public class Factory {
// 静态工厂方法
public static Product getProduct(String arg) {
if (arg.equalsIgnoreCase("A")) {
return new ConcreteProductA();
}
if (arg.equalsIgnoreCase("B")) {
return new ConcreteProductB();
}

return null;
}
}

典型的客户端代码

public class Client {

public static void main(String[] args) {
// 通过工厂类创建产品对象
Product product = Factory.getProduct("A");
product.methodSame();
product.methodDiff();
}
}

我们也可以通过引入xml配置文件来做到开闭原则,即可以在不修改客户端代码的基础上修改配置文件来实现我们引入的产品对象的更改。

创建对象与使用对象

Java语言创建对象的几种方式

  1. 使用new关键字直接创建对象
  2. 通过反射机制创建对象
  3. 通过克隆方法创建对象
  4. 通过工厂类创建对象

将对象的创建与使用分离的其他好处

  • 防止用来实例化一个类的数据和代码在多个类中到处都是,可以将有关创建的知识搬移到一个工厂类中,解决代码重复、创建蔓延的问题[Move Creation Knowledge to Factory. Joshua Kerievsky, Refactoring to Patterns, 2004]
  • 构造函数的名字都与类名相同,从构造函数和参数列表中大家很难了解不同构造函数所构造的产品的差异 \rightarrow 将对象的创建过程封装在工厂类中,可以提供一系列名字完全不同的工厂方法,每一个工厂方法对应一个构造函数,客户端可以以一种更加可读、易懂的方式来创建对象

何时不需要工厂?

  • 无须为系统中的每一个类都配备一个工厂类
  • 如果一个类很简单,而且不存在太多变化,其构造过程也很简单,此时就无须为其提供工厂类,直接在使用之前实例化即可
  • 否则会导致工厂泛滥,增加系统的复杂度
    • 例如:java.lang.String

优缺点与使用环境

优点

  • 实现了对象创建和使用的分离
  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可
  • 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性

缺点

  • 工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受到影响
  • 增加系统中类的个数(引入了新的工厂类),增加了系统的复杂度和理解难度
  • 系统扩展困难,一旦添加新产品不得不修改工厂逻辑
  • 由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构,工厂类不能得到很好地扩展

适用环境

  • 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂
  • 客户端只知道传入工厂类的参数,对于如何创建对象并不关心

Factory Pattern 工厂方法

别名:virtual constructor 虚构造器

目的:定义一个用于创建对象的接口,让子类决定实例化哪一个
类。Factory Method使一个类的实例化延迟到其子类。

工厂方法模式:

  • 不再提供一个工厂类来统一负责所有产品的创建,而是将具体对象的创建过程交给专门的工厂子类去完成
  • 如果出现新的类型,只需要为这种新类型定义一个具体的工厂类就可以创建该新类型的实例。

定义

工厂方法模式:定义一个用于创建对象的接口,但是让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类

  • 简称为工厂模式 (Factory Pattern)
  • 又可称作虚拟构造器模式 (Virtual Constructor Pattern)多态工厂模式 (Polymorphic Factory Pattern)
  • 工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象
  • 目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

结构与实现

结构

工厂方法模式包含以下4个角色:

  • Product(抽象产品)
  • ConcreteProduct(具体产品)
  • Factory(抽象工厂)
  • ConcreteFactory(具体工厂)

实现

典型的抽象工厂类代码

public interface Factory [
public Product factoryMethod();
]

典型的具体工厂类代码

public class ConcreteFactory implements Factory {

public Product factoryMethod() {
return new ConcreteProduct();
}
}

典型的客户端代码片段

...
Factory factory = new ConcreteFactory(); // 可以通过xml配置文件或者反射机制来实现
Product product = factory.factoryMethod();
...

实例

优缺点与适用环境

优点

  • 工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节
  • 能够让工厂自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部
  • 在系统中加入新产品时,完全符合开闭原则

缺点

  • 系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,会给系统带来一些额外的开销
  • 增加了系统的抽象性和理解难度

适用环境

  • 客户端不知道它所需要的对象的类(客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体产品对象由具体工厂类创建)
  • 抽象工厂类通过其子类来指定创建哪个对象

Abstract Factory Pattern 抽象工厂模式

别名:Kit

目的:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类 - 客户端不必指定产品的具体类型,创建多个产品族中的产品对象。

产品等级结构与产品族

工厂方法模式:每个具体工厂只有一个或者一组重载的工厂方法,只能生产一种产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销

抽象工厂模式:一个工厂可以生产一系列产品(一族产品),极大减少了工厂类的数量

概念

产品等级结构:产品等级结构即产品的继承结构

产品族:产品族是指由同一个工厂产生的,位于不同产品等级结构中的一组产品。

概述

模式动机

  • 当系统所提供的的工厂生产的具体产品并不是一个简单的对象,而是多个位于不同产品等级结构、属于不同类型的具体产品时就可以使用抽象工厂模式。
  • 抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形式

定义

抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

  • 又称为工具(Kit)模式
  • 抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品
  • 当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、更有效率。

结构与实现

结构

抽象工厂模式包含以下四个角色:

  • AbstractFactory(抽象工厂)
  • ConcreteFactory(具体工厂)
  • AbstractProduct(抽象产品)
  • ConcreteProduct(具体产品)

实现

典型的抽象工程类代码

public interface AbtractFactory {
// 工厂方法一
AbstractProductA createProductA();
// 工厂方法二
AbstractProductB createProductB();
...
}

典型的具体工厂类代码

public class ConcreteFactory1 implements AbstractFactory {

// 工厂方法一
public AbstractProductA createProductA() {
return new ConcreteProductA1();
}

// 工厂方法二
public AbstractProductB createProductB() {
return new ConcreteProductB1();
}
}

实例

开闭原则的倾斜性

  • 增加产品族:对于增加新的产品族,抽象工厂模式很好地支持了开闭原则,只需要增加具体产品并对应增加一个新的具体工厂,对已有代码无须做任何修改

  • 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了开闭原则

    即需要在某个工厂中多生产一类产品会违背开闭原则。而如果只是单纯增加已有类型的产品,则不会违背

优缺点与适用环境

优点

  • 隔离了具体类的生成,使得客户端并不需要知道什么被创建
  • 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象
  • 增加新的产品族很方便,无须修改已有系统,符合开闭原则

缺点

  • 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了开闭原则。

模式适用环境

  • 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节
  • 系统中有多于一个的产品族,但每次只使用其中某一产品族
  • 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来
  • 产品等级结构稳定,在设计完成之后不会向系统中增加新的产品等级结构或者删除已有的产品等级结构

Builder 建造者模式

概述

建造者模式可以将部件本身和它们的组装过程分开,关注如何一步步创建一个包含多个组成部分的复杂对象,用户只需要指定复杂对象的类型即可得到该对象,而无须知道其内部的具体构造细节。

定义

建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

  • 将客户端与包含多个部件的复杂对象的创建过程分离,客户端无须知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可。
  • 关注如何逐步创建一个复杂的对象,不同的建造者定义了不同的创建过程。

结构与实现

结构

建造者模式包含以下4个角色

  • Builder(抽象建造者)
  • ConcreteBuilder(具体建造者)
  • Product(产品)
  • Director(指挥者)

实现

典型的复杂对象类代码

@Data
// lombok插件,提供Getter,Setter
public class Product {
// 定义部件,部件可以是任意类型,包括值类型和引用类型
private String partA;
private String partB;
private String partC;
}

典型的抽象建造者类代码

public abstract class Builder {
protected Product product = new Product();

public abstract void buildPartA();
public abstract void buildPartB();
public abstract void buildPartC();

// 返回产品对象
public Product getResult() {
return product;
}
}

典型的具体建造者类代码

public class ConcreteBuilder1 extends Builder {
public void buildPartA() {
product.setPartA("A1");
}

public void buildPartB() {
product.setPartA("B1");
}

public void buildPartC() {
product.setPartA("C1");
}
}

典型的指挥者类代码

@Setter
@AllArgsConstructor
// lombok插件,提供setter和构造器
public class Director {

private Builder builder;

public Product construct() {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
return builder.getResult();
}
}

客户类代码片段

...
Builder builder = new ConcreteBuilder1(); // 通过配置文件实现
Director director = new Director(builder);
Product product = director.construct();
...

实例

指挥者类的深入讨论

优缺点与适用环境

优点

  • 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
  • 每一个具体建造者都相对独立,与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,扩展方便,符合开闭原则
  • 可以更加精细地控制产品的创建过程

缺点

  • 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,不适合使用建造者模式,因此其使用范围受到一定的限制
  • 如果产品的内部变化复杂,可能会需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,增加了系统的理解难度和运行成本

适用环境

  • 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员变量
  • 需要生成的产品对象的属性相互依赖,需要指定其生成顺序
  • 对象的创建过程独立于创建该对象的类。在建造者模式中通过引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类和客户类中
  • 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品

Prototype 原型模式

概述

通过复制一个原型对象得到多个与原型对象一模一样的新对象

定义

原型模式:使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。

  • 工作原理:将一个原型对象传给要发动创建的对象(即客户端对象),这个要发动创建的对象通过请求原型对象复制自己来实现创建过程
  • 创建新对象(也称为克隆对象)的工厂就是原型类自身,工厂方法由负责复制原型对象的克隆方法来实现
  • 通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,每一个克隆对象都是独立的
  • 通过不同的方式对克隆对象进行修改以后,可以得到一系列相似但不完全相同的对象。

结构与实现

结构

原型模式包含以下3个角色:

  • Prototype(抽象原型类)
  • ConcretePrototype(具体原型类)
  • Client(客户类)

浅克隆与深克隆

浅克隆(Shallow Clone):当原型对象被复制时,只复制它本身和其中包含的值类型的成员变量,而引用类型的成员变量并没有复制。

深克隆(Deep Clone):除了对象本身被复制外,对象所包含嗯对所有成员变量也将被复制。、

实现

  • Java语言中的clone()方法和Cloneable接口

    在Java语言中,提供了一个clone()方法用于实现浅克隆,直接调用super.clone()方法用于克隆

实例

原型管理器

定义

原型管理器(Prototype Manager)将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,如果需要某个原型对象的一个克隆,可以通过复制集合中对应的原型对象来获得。

结构

代码

import java.util.*;
public class PrototypeManager {
/**
* 用Hashtable存储原型对象
*/
private Hashtable prototypeTable=new Hashtable();

public PrototypeManager() {
prototypeTable.put("A", new ConcretePrototypeA());
prototypeTable.put("B", new ConcretePrototypeB());
}

public void add(String key, Prototype prototype) {
prototypeTable.put(key,prototype);
}

public Prototype get(String key) {
Prototype clone = null;
// 通过克隆方法创建新对象
clone = ((Prototype) prototypeTable.get(key)).clone();

return clone;
}
}

优缺点与适用环境

优点

  • 简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率
  • 扩展性较好
  • 提供了简化的创建结构,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品
  • 可以使用深克隆的方式保存对象的状态,以便在需要的时候使用,可辅助实现撤销操作

缺点

  • 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了开闭原则
  • 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦

适用环境

  • 创建新对象成本较大,新对象可以通过复制已有对象来获得,如果是相似对象,则可以对其成员变量稍作修改
  • 系统要保存对象的状态,而对象的状态变化很小
  • 需要避免使用分层次的工厂类来创建分层次的对象
  • Ctrl + C \rightarrow Ctrl + V

Singleton 单例模式

概述

如何保证一个类只有一个实例并且这个实例易于被访问:

  1. 全局变量:可以确保对象随时可以被访问,但不能防止创建多个对象
  2. 让类自身负责创建和保存它的唯一实例,并保证不能创建其他实例,它还提供一个访问该实例的方法

定义

概述:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

  1. 某个类只能有一个实例
  2. 必须自行创建这个实例
  3. 必须自行向整个系统提供这个实例

结构与实现

结构

单例模式只包含一个单例角色:

  • Singleton(单例)

实现

  • 私有构造函数
public class Singleton {
// 静态私有成员变量
private static Singleton instance = null;

// 私有构造函数
private Singlton() {

}

// 静态公有工厂方法,返回唯一实例
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}

return instance;
}
}

实例

饿汉式单例与懒汉式单例

饿汉式单例

Eager Singleton:在类定义时就初始化instance,而不是等到调用getInstance()才初始化instance

结构

实现
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();

private EagerSingleton() {

}

public static EagerSingleton getInstance() {
return instance;
}
}

懒汉式单例

Lazy Singleton:将单例的初始化延迟到调用getInstance方法中,但有可能引起多线程问题,需要加锁。(双重加锁锁定)

实现
public class LazySingleton {
private volatile static LazySingleton instance = null;

private LazySingleton() {

}

public static LazySingleton getInstance() {
if (instance == null) {
// 锁定代码块
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
}
}

比较

  • 饿汉式单例类:无须考虑多个线程同时访问的问题;调用速度和反应时间优于懒汉式单例;资源利用效率不及懒汉式单例;系统加载时间可能会比较长。
  • 懒汉类单例类:实现了延迟加载;必须处理好多个线程同时访问的问题;需通过双重检查锁定等机制进行控制,将导致系统性能受到一定影响

Java – 静态内部类

// initialization on demand holder
public class Singleton {

private Singleton() {

}

// 静态内部类
private static class HolderClass {
private final static Singleton instance = new Singleton();
}

public static Singleton getInstance() {
return HolderClass.instance;
}
}

优缺点与适用环境

优点

  • 提供了对唯一实例的受控访问
  • 可以节约系统资源,提高系统的性能
  • 允许可变数目的实例(多例类)

缺点

  • 扩展困难(缺少抽象层)
  • 单例类的职责过重、一定程度上违背单一职责原则
  • 由于自动垃圾回收机制,可能会导致共享的单例对象的状态丢失

适用环境

  • 系统只需要一个实例对象,或者因为资源消耗太大而只允许创建一个对象
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例

Monostate

目标:另外一种获取对象单一性的方法

动机:无论创建了多少Monostate的实例,它们都表现得像一个对象一样,甚至把当前的所有实例都销毁或者解除职责,也不会丢失数据。

解决方式/实现:将所有变量都设置为静态的。

解决效果

优点
  • 透明性:使用Monostate对象和使用常规对象没有什么区别,使用者不需要知道对象是Monostate
  • 可派生性:派生类都是Monostate
  • 多态性:由于方法不是静态的,所以可以在派生类中override。因此不同的派生类可以基于同样的静态变量表现出不同的行为
缺点
  • 不可转换性:不能透过派生把常规类转换成Monostate类
  • 内存占用:即使从未使用Monostate,它的变量也要占用内存空间

Reference

  1. Java设计模式 – 刘伟,清华大学出版社
  2. 面向对象设计方法 – 南京大学计算机科学与技术系2022春季课程
  3. Design Pattern – GoF 23种设计模式
文章作者: ZY
文章链接: https://zyinnju.com/2022/03/31/Design-Pattern-1/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ZY in NJU