简单梳理下二十三种设计模式,在使用设计模式的时候,不仅要对其分类了然于胸,还要了解每个设计模式的应用场景、设计与实现,以及其优缺点。同时,还要能区分功能相近的设计模式,避免出现误用的情况。
设计模式是一套被反复使用的、多人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。其目的是为了提高代码的可重用性、代码的可读性和代码的可靠性。
设计模式的本质是面向对象设计原则的实际运用,是对类的封装、继承和多态以及类的关联关系和组合关系的充分理解。正确使用设计模式具有以下优点:
(1) 可以提高程序员的思维能力、编程能力和设计能力;
(2) 可以使程序设计更加标准化、代码编制更加工程化,从而提高软件开发效率,缩短软件的开发周期;
(3) 可以使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
建筑大师Christopher Alexander说过“每一个模式描述了一个不断重复发生的问题,以及该问题的解决方案的核心”。这种思想适用于面向对象设计领域。一般而言,一个模式有四个基本要素:
(1) 模式名称(Pattern Name)
一个助记名,用来描述模式的问题、解决方案和效果。模式名称(Pattern Name)有助于我们理解和记忆该模式,也方便我们来讨论自己的设计(相同的设计模式语言)。
(2) 问题(Problem)
问题描述了该模式的应用环境,即何时使用该模式。它解释了设计问题和问题存在的前因后果,以及必须满足的一系列先决条件。
(3) 解决方案(Solution)
解决方案描述设计的组成成分、它们之间的相互关系及各自的职责和协作方式。因为模式就像一个模板,可应用于多种不同场合,所以解决方案并不描述一个特定而具体的设计或实现,而是提供设计问题的抽象描述和如何用一个具有一般意义的元素组合(类或对象的 组合)来解决这个问题。
(4) 效果(Consequences)
描述模式的应用效果以及使用该模式应该权衡的问题,即模式的优缺点。主要是对时间和空间的衡量,以及该模式对系统的灵活性、扩充性、可移植性的影响,也考虑其实现问题。显式地列出这些效果(Consequence)对理解和评价这些模式有很大的帮助。
设计模式的数量有很多,这里仅以经典的GoF总结的设计模式进行分类。根据服务的范围是类还是对象,以及根据设计模式使用的目的,将二十三个设计模式进行如下划分:
- | - | 创建型(5个) | 结构型(7个) | 行为型(11个) |
---|---|---|---|---|
范围 | 类 | 工厂方法 | 适配器模式(类) | 解释器、模板方法 |
范围 | 对象 | 抽象工厂、建造器、原型模式、单例 | 适配器模式(对象)、桥接模式、组合模式、装饰器模式、外观模式、享元模式、代理模式 | 职责链模式、命令模式、迭代器、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、访问者模式 |
创建型模式对类的实例化过程进行了抽象,能够将对象的创建与对象的使用过程分离。
创建型模式,抽象了实例化的过程。它帮助一个系统独立于如何创建、组合和表示它的那些对象。一个类创建模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象(实例化对象)。
创建型模式主要关注点是“怎样创建对象”,它的主要特点是“对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节,对象的创建由相关的类来完成。
创建型模式分为以下5种:
(1) 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
(2) 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
(3) 工厂方法(FactoryMethod)模式:定义一个用于创建产品的接口,由子类决定生产什么产品。
(4) 抽象工厂(AbstractFactory)模式:提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
(5) 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
简单工厂模式,又称为静态工厂方法(Static Factory Method)模式,是一种创建型设计模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。简单工厂模式不属于GoF的23个设计模式,可看成是工厂方法模式的退化实现。
在以下情况可以使用考虑简单工厂模式:
(1) 工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
(2) 客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。
简单工厂模式定义一个工厂类来负责创建具体产品,且每个产品都拥有共同的父类,具体来说,包含如下角色:
Factory,工厂类,负责实现创建所有实例的内部逻辑。
Product,抽象产品,用来定义工厂所创建的对象的接口。
ConcreteProduct,具体产品,用来实现Product的接口。
简单工厂模式类图表示如下:
简单工厂实现的示例代码如下:
// 1.1 定义产品(Product)抽象类(也可以是基类),对产品接口进行声明
public abstract class Product {
abstract void function();
}
// 1.2 定义具体产品(Concrete Products),产品接口的不同实现
public class ConcreteProductA extends Product {
@Override
public void function() {
System.out.println("---------do some thing in a ConcreteProductA instance---------");
}
}
public class ConcreteProductB extends Product {
@Override
public void function() {
System.out.println("---------do some thing in a ConcreteProductB instance---------");
}
}
// 2、产品工厂(Product Factory),定义返回产品对象的静态工厂方法。该方法的返回对象类型是产品的基类(Product)。
// 该类最主要的职责就是根据入参的不同创建不同的产品。
public class ProductFactory {
/**
* 根据条件生产产品
* 注意:这里以产品名作为入参,具体场景还应灵活处理
*/
public static Product create(String productName) {
if (productName.contains("ConcreteProductA")) {
return new ConcreteProductA();
}
if (productName.contains("ConcreteProductB")) {
return new ConcreteProductB();
}
return null;
}
}
// 3、客户端调用
public class FactoryMethodClient {
public void test() {
// (1) 通过工厂创建产品
Product productA = ProductFactory.create(ConcreteProductA.class.getName());
// (2) 使用产品
productA.function();
Product productB = ProductFactory.create(ConcreteProductB.class.getName());
productB.function();
}
}
简单工厂模式有以下优点:
(1) 责任分离。工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,仅需要使用产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象。
(2) 使用简单。客户端在使用产品时,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
(3) 提高系统灵活性。通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
但是简单工厂模式也存在以下缺点:
(1)违反开闭原则。工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则是相违背的。如果产品数量过多,则会容易引入上帝类。
(2) 可扩展性差。一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展。另外,简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
工厂方法模式是一种创建型设计模式,就是定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法模式将类的实例化(具体产品的创建)延迟到工厂类的子类(具体工厂)中完成,即由子工厂类来决定该实例化哪一个类。
在以下情况可以考虑使用工厂方法模式:
(1) 如果无法预知对象确切类别及其依赖关系时,可使用工厂方法。
工厂方法将创建产品的代码与实际使用产品的代码分离,从而能在不影响其他代码的情况下扩展产品创建部分代码。
例如,如果需要添加一种新产品,则只需要开发新的产品工厂子类,然后重写其工厂方法即可。
(2) 如果希望用户能扩展软件库或框架的内部组件,可使用工厂方法。
继承是扩展软件库或框架默认行为的最简单方法。但是,当使用子类替代标准组件时,框架如何辨识出该子类?解决方案是将各框架中构造组件的代码集中到单个工厂方法中,并在继承该组件之外允许任何人对该方法进行重写。
(3) 如果希望复用现有对象来节省系统资源,而不是每次都重新创建对象,可使用工厂方法。
在处理大型资源密集型对象(如数据库连接、文件系统和网络资源等)时, 经常需要复用现有对象已节省资源。
正常情况下,为实现现有对象的复用,其处理流程一般如下:
首先,创建存储空间来存放经创建的对象。
然后,在外部请求对时,将优先在对象池中搜索可用对象。如果存在则立即返回该对象。
如果没有可用对象,则创建并返回该新对象(并将其添加到对象池中)。
为避免上述代码不会因为重复而污染程序(对象的创建场景可能很多),可以将这些代码放在试图重用的对象类的构造函数中。
但是从定义上来讲,构造函数始终返回的是新对象,其无法返回现有实例。针对这种既需要一个能够创建新对象,又可以重用现有对象的场景,工厂方法是最佳选择。
工厂方法模式包含如下角色:
Product,抽象产品,用来定义工厂方法所创建的对象的接口。
ConcreteProduct,具体产品,用来实现Product的接口。
Factory,抽象工厂,负责声明工厂方法,该方法返回一个Product类型的对象。
ConcreteFactory,具体工厂,用来实现Factory声明的工厂方法。
工厂方法模式类图表示如下:
工厂方法模式实现的示例代码如下:
// 1、定义产品(Product)抽象类(也可以是基类),对产品接口进行声明
public abstract class Product {
abstract void function();
}
// 2、定义具体产品(Concrete Products),产品接口的不同实现
public class ConcreteProductA extends Product {
@Override
public void function() {
System.out.println("---------do some thing in a ConcreteProductA instance---------");
}
}
public class ConcreteProductB extends Product {
@Override
public void function() {
System.out.println("---------do some thing in a ConcreteProductB instance---------");
}
}
// 3、产品工厂(Product Factory)抽象类或基类,声明返回产品对象的工厂方法。该方法的返回对象类型必须与产品接口相匹配。
// 可以将工厂方法声明为抽象方法,强制要求每个子类以不同方式实现该方法。或者,在基础工厂方法中返回默认产品类型。
// 注意,该类最主要的职责并不是创建产品。 一般来说, 产品工厂类包含一些与产品相关的核心业务逻辑。工厂方法将这些逻辑处理从具体产品类中分离出来。
public abstract class ProductFactory {
/**
* 生产产品
*/
abstract Product create();
}
// 4、具体产品工厂(Concrete Product Factories)重写基础工厂方法, 使其返回不同类型的产品。
// 注意, 并不一定每次调用工厂方法都会创建新的实例。工厂方法也可以返回缓存、对象池或其他来源的已有对象。
public class ProductFactoryA extends ProductFactory {
@Override
public Product create() {
System.out.println("create ProductA");
return new ConcreteProductA();
}
}
public class ProductFactoryB extends ProductFactory {
@Override
public Product create() {
System.out.println("create ProductB");
return new ConcreteProductB();
}
}
// 5、客户端调用
public class FactoryMethodClient {
public void foo() {
// (1) 实例化产品工厂
ProductFactory productFactoryA = new ProductFactoryA();
// (2) 生产产品
Product productA = productFactoryA.create();
// (3) 使用产品
productA.function();
ProductFactory productFactoryB = new ProductFactoryB();
Product productB = productFactoryB.create();
productB.function();
}
}
工厂方法模式有以下优点:
(1) 将创建产品的代码与实际使用产品的代码分离(解耦),避免产品创建工厂和实际产品之间的紧耦合。
(2) 单一职责原则。产品创建代码放在单独的类里,从而使得代码更容易维护。
(3)开闭原则。无需更改客户端调用代码, 就可以在程序中引入新的产品类型。
但是工厂方法模式也存在以下缺点:
(1) 应用工厂方法模式需要引入许多新的子类,代码会因此变得更复杂。最好的情况是将该模式引入产品类的现有层次结构中(将工厂类组合到产品类里)。
(2) 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到反射等技术,增加了系统的实现难度。
其实,上述两个缺点也是设计模式的固有缺陷,并不是工厂方法模式独有的问题。
抽象工厂模式是一种创建型设计模式,该模式提供一个创建一组相关或相互依赖的对象的接口,而无须指定它们具体的类,每个子类可以生产一系列相关的产品。
在以下情况可以考虑使用抽象工厂模式:
(1) 如果需要与多个不同系列的相关产品交互,但无法预知对象确切类别及其依赖关系时,可使用抽象工厂。
抽象工厂将创建产品的代码与实际使用产品的代码分离,从而能在不影响其他代码的情况下扩展产品创建部分代码。
(2) 如果希望用户能扩展软件库或框架的内部组件,可使用抽象工厂。
继承是扩展软件库或框架默认行为的最简单方法。但是,当使用子类替代标准组件时,框架如何辨识出该子类?解决方案是将各框架中构造组件的代码集中到单个工厂方法中,并在继承该组件之外允许任何人对该方法进行重写。
(3) 如果存在一个基于一组抽象方法的类,且其主要功能因此变得不明确,可使用抽象工厂。
在设计良好的程序中, 每个类仅负责一件事。如果一个类与多种类型产品交互,就可以考虑将工厂方法抽取到独立的工厂类或具备完整功能的抽象工厂类中。
抽象工厂模式包含如下角色:
AbstractFactory,抽象工厂,负责声明一组产品创建的工厂方法,每个方法返回一个AbstractProduct类型的对象。
ConcreteFactory,具体工厂,用来实现AbstractFactory声明的工厂方法,以创建具有特定实现的产品对象。
AbstractProduct,抽象产品,用来声明一组不同但相关的产品声明的接口。
ConcreteProduct,具体产品,用来实现AbstractProduct的接口,以定义特定的产品。
抽象工厂模式类图表示如下:
接下来将使用代码介绍下抽象工厂模式的实现。
(1) 定义一组产品(ProductA、ProductB、…)抽象类(也可以是基类),对产品接口进行声明。然后定义具体产品(Concrete Products),实现产品声明的接口。
public abstract class ProductA {
abstract void function();
}
public class ConcreteProductA1 extends ProductA {
@Override
public void function() {
System.out.println("---------do some thing in a ConcreteProductA1 instance---------");
}
}
public class ConcreteProductA2 extends ProductA {
@Override
public void function() {
System.out.println("---------do some thing in a ConcreteProductA2 instance---------");
}
}
public abstract class ProductB {
abstract void foo();
}
public class ConcreteProductB1 extends ProductB {
@Override
public void foo() {
System.out.println("---------do some thing in a ConcreteProductB1 instance---------");
}
}
public class ConcreteProductB2 extends ProductB {
@Override
public void foo() {
System.out.println("---------do some thing in a ConcreteProductB2 instance---------");
}
}
(2) 定义产品工厂(Product Factory)抽象类或基类,声明返回一组产品对象的工厂方法。每个方法的返回对象类型必须与产品接口相匹配。然后定义具体产品工厂(Concrete Product Factories)重写基础工厂方法, 使其返回不同类型的产品。
public abstract class ProductFactory {
/**
* 生产产品A
*/
abstract ProductA createProductA();
/**
* 生产产品B
*/
abstract ProductB createProductB();
}
public class ProductFactory1 extends ProductFactory {
@Override
public ProductA createProductA() {
System.out.println("create ProductA");
return new ConcreteProductA1();
}
@Override
public ProductB createProductB() {
System.out.println("create ProductB");
return new ConcreteProductB1();
}
}
public class ProductFactory2 extends ProductFactory {
@Override
public ProductA createProductA() {
System.out.println("create ProductB");
return new ConcreteProductA2();
}
@Override
public ProductB createProductB() {
System.out.println("create ProductB");
return new ConcreteProductB2();
}
}
(3) 客户端调用。支持多种调用方式,如:直接在方法中实例化工厂子类、提供静态方法,支持方法参数中传递工厂对象、提供对象方法,使用构造函数中传入的工厂对象。
public class AbstractFactoryClient {
private ProductFactory factory;
public AbstractFactoryClient(ProductFactory factory) {
this.factory = factory;
}
// 调用方式一:方法中实例化工厂子类
public void test() {
// (1) 实例化产品工厂
ProductFactory productFactory1 = new ProductFactory1();
// (2) 生产产品
ProductA productA1 = productFactory1.createProductA();
// (3) 使用产品
productA1.function();
// 下同
ProductB productB1 = productFactory1.createProductB();
productB1.foo();
ProductFactory productFactory2 = new ProductFactory2();
ProductA productA2 = productFactory2.createProductA();
productA2.function();
ProductB productB2 = productFactory2.createProductB();
productB2.foo();
}
// 调用方式二:工厂对象作为方法参数(工具类方法)
public static void foo(ProductFactory factory) {
ProductA productA1 = factory.createProductA();
productA1.function();
ProductB productB1 = factory.createProductB();
productB1.foo();
}
// 调用方式三:使用构造函数中传入的工厂(工厂一旦绑定,无法修改)
public void foo() {
ProductA productA1 = this.factory.createProductA();
productA1.function();
ProductB productB1 = this.factory.createProductB();
productB1.foo();
}
}
抽象工厂模式有以下优点:
(1) 将创建产品的代码与实际使用产品的代码分离(解耦),避免产品创建工厂和实际产品之间的紧耦合。
(2) 单一职责原则。产品创建代码放在单独的类里,从而使得代码更容易维护。
(3)开闭原则。无需更改客户端调用代码, 就可以在程序中引入新的产品类型。
但是抽象工厂模式也存在以下缺点:
(1) 应用抽象工厂模式需要引入许多新的子类,代码会因此变得更复杂。最好的情况是将该模式引入产品类的现有层次结构中(将工厂类组合到产品类里)。
(2) 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到反射等技术,增加了系统的实现难度。
单例模式是一种创建型设计模式,就是保证一个类仅有一个实例,并提供一个全局访问点来访问它。
在以下情况可以考虑使用单例模式:
(1) 如果系统只需要一个实例对象,则可以考虑使用单例模式。如提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象(如数据库连接等)。
(2) 如果需要调用的实例只允许使用一个公共访问点,则可以考虑使用单例模式。
(3) 如果一个系统只需要指定数量的实例对象,则可以考虑扩展单例模式。如可以在单例模式中,通过限制实例数量实现多例模式。
单例模式只有一个角色–单例类,用来保证实例唯一并提供一个全局访问点。为实现访问点全局唯一,可以定义一个静态字段,同时为了封装对该静态字段的访问,可以定义一个静态方法。
为了保证实例唯一,这个类还需要在内部保证实例的唯一。基于以上思考,单例模式的类图表示如下:
单例模式的实现方式有很多种,主要的实现方式有以下五种:饿汉方式、懒汉方式、线程安全实现方式、双重校验方式、惰性加载方式。
饿汉方式就是在类加载的时候就创建实例,因为是在类加载的时候创建实例,所以实例必唯一。由于在类加载的时候创建实例,如果实例较复杂,会延长类加载的时间。
// 1. 定义单例类,提供全局唯一访问点,保证实例唯一
public class HungrySingleton {
// (1) 声明并实例化静态私有成员变量(在类加载的时候创建静态实例)
private static final HungrySingleton instance = new HungrySingleton();
// (2) 私有构造方法
private HungrySingleton() {
}
// (3) 定义静态方法,提供全局唯一访问点
public static HungrySingleton getInstance() {
return instance;
}
public void foo() {
System.out.println("---------do some thing in a HungrySingleton instance---------");
}
}
// 2. 客户端调用
public class HungrySingletonClient {
public void test() {
// (1) 获取实例
HungrySingleton singleton = HungrySingleton.getInstance();
// (2) 调用实例方法
singleton.foo();
}
}
懒汉方式就是在调用实例获取(如getInstance())接口时,再创建实例,这种方式可避免在加载类的时候就初始化实例。
// 1. 定义单例类,提供全局唯一访问点,保证实例唯一
public class LazySingleton {
// (1) 声明静态私有成员变量
private static LazySingleton instance;
// (2) 私有构造方法
private LazySingleton() {
}
// (3) 定义静态方法,提供全局唯一访问点
public static LazySingleton getInstance() {
// 将实例的创建延迟到第一次获取实例
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
public void foo() {
System.out.println("---------do some thing in a LazySingleton instance---------");
}
}
// 2. 客户端调用
public class LazySingletonClient {
public void test() {
// (1) 获取实例
LazySingleton instance = LazySingleton.getInstance();
// (2) 调用实例方法
instance.foo();
}
}
需要说明的是,对多线程语言来说(如java语言),懒汉方式会带来线程不安全问题。如果在实例前执行判空处理时,至少两个线程同时进入这行代码,则会创建多个实例。
所以,对于多线程语言来说,为了保证代码的正确性,还需在实例化的时候,保证线程安全。
为保证线程安全,可以在实例判空前,进行线程同步处理,如添加互斥锁。
// 1. 定义单例类,提供全局唯一访问点,保证实例唯一
public class ThreadSafeSingleton {
// (1) 声明静态私有成员变量
private static ThreadSafeSingleton instance;
// (2) 私有构造方法
private ThreadSafeSingleton() {
}
// (3) 定义静态方法,提供全局唯一访问点
public static ThreadSafeSingleton getInstance() {
// 使用synchronized方法,保证线程安全
synchronized (ThreadSafeSingleton.class) {
if (Objects.isNull(instance)) {
instance = new ThreadSafeSingleton();
}
return instance;
}
}
public void foo() {
System.out.println("---------do some thing in a ThreadSafeSingleton instance---------");
}
}
// 2. 客户端调用
public class ThreadSafeSingletonClient {
public void test() {
// (1) 获取实例
ThreadSafeSingleton instance = ThreadSafeSingleton.getInstance();
// (2) 调用实例方法
instance.foo();
}
}
但是这种方式,会因线程同步而带来性能问题。因为大多数场景下,是不存在并发访问。
为避免每次创建实例时加锁带来的性能问题,引入双重校验方式,即在加锁前额外进行实例判空校验,这样就可保证非并发场景下仅在第一次实例化时,去加锁并创建实例。
// 1. 定义单例类,提供全局唯一访问点,保证实例唯一
public class DoubleCheckSingleton {
// (1) 声明静态私有成员变量
private static volatile DoubleCheckSingleton instance;
// (2) 私有构造方法
private DoubleCheckSingleton() {
}
// (3) 定义静态方法,提供全局唯一访问点
public static DoubleCheckSingleton getInstance() {
// 在加锁之前,先执行判空检验,提高性能
if (Objects.isNull(instance)) {
// 使用synchronized方法,保证线程安全
synchronized (DoubleCheckSingleton.class) {
if (Objects.isNull(instance)) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
public void foo() {
System.out.println("---------do some thing in a DoubleCheckSingleton instance---------");
}
}
// 2. 客户端调用
public class DoubleCheckSingletonClient {
public void test() {
// (1) 获取实例
DoubleCheckSingleton instance = DoubleCheckSingleton.getInstance();
// (2) 调用实例方法
instance.foo();
}
}
注意,使用双重校验方式时,需明确语言是否支持指令重排序。以Java语言为例,实例化一个对象的过程是非原子的。具体来说,可以分为以下三步:(1) 分配对象内存空间;(2)将对象信息写入上述内存空间;(3) 创建对上述内存空间的引用。其中(2)和(3)的顺序不要求固定(无先后顺序),所以存在实例以分配内存空间但还未初始化的情况。如果此时存在并发线程使用了该未初始化的对象,则会导致代码异常。为避免指令重排序,Java语言中可以使用 volatile 禁用指令重排序。更多细节可以参考java单例模式一文。
由于加锁会带来性能损耗,最好的办法还是期望实现一种无锁的设计,且又能实现延迟加载。对Java语言来说,静态内部类会延迟加载(对C#语言来说,内部类会延迟加载)。可以利用这一特性,实现单例。
// 1. 定义单例类,提供全局唯一访问点,保证实例唯一
public class LazyLoadingSingleton {
// (2) 私有构造方法
private LazyLoadingSingleton() {
}
// (3) 定义静态方法,提供全局唯一访问点
public static LazyLoadingSingleton getInstance() {
// 第一调用静态类成员或方法时,才加载静态内部类,实现了延迟加载
return Holder.instance;
}
public void foo() {
System.out.println("---------do some thing in a LazyLoadingSingleton instance---------");
}
// (1) 声明私有静态内部类,并提供私有成员变量
private static class Holder {
private static LazyLoadingSingleton instance = new LazyLoadingSingleton();
}
}
// 2. 客户端调用
public class LazyLoadingSingletonClient {
public void test() {
// (1) 获取实例
LazyLoadingSingleton instance = LazyLoadingSingleton.getInstance();
// (2) 调用实例方法
instance.foo();
}
}
单例模式模式有以下优点:
(1) 提供了对唯一实例的受控访问。
因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
(2) 节约系统资源。由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
(3) 允许可变数目的实例。可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
但是单例模式模式也存在以下缺点:
(1) 违反了单一职责原则。单例类的职责过重,既充当工厂角色,提供了工厂方法,同时又充当产品角色,包含一些业务方法,将产品的创建和产品本身的功能融合到一起,在一定程度上违背了单一职责原则。
(2) 单例类扩展困难。由于单例模式中没有抽象层,且继承困难,所以单例类的扩展有很大的困难。
(3) 滥用单例模式带来一些负面问题,如过多的创建单例,会导致这些单例类一直无法释放且占用内存空间,另外对于一些不频繁使用的但占用内存空间较大的对象,也不宜将其创建为单例。而且现在很多面向对象语言(如Java、C#)都提供了自动垃圾回收的技术。
原型模式是一种创建型设计模式,使调用方能够复制已有对象,而又无需使代码依赖它们所属的类。当有一个类的实例(原型),并且想通过复制原型来创建新对象时,通常会使用原型模式。
在以下情况可以考虑使用原型模式:
(1) 如果需要复制一些对象,同时又希望代码独立于这些对象所属的具体类,可以使用原型模式。
例如,代码里需要处理第三方接口传递过来的对象时,即使不考虑代码耦合的情况, 调用方的代码也不能依赖这些对象所属的具体类,因为无法知道它们的具体信息。
原型模式,可以为调用方提供一个通用接口,调用方可通过这一接口与所有实现了克隆的对象进行交互,它也使得调用方与其所克隆的对象具体类独立开来。
(2) 如果子类的区别仅在于其对象的初始化方式,那么可以使用原型模式来减少子类的数量。
在原型模式中,可以使用一系列预生成的、各种类型的对象作为原型(原型对象池)。客户端不必根据需求对子类进行实例化,只需找到合适的原型并对其进行克隆即可。
原型模式就是定义一个原型类,用来声明克隆接口,然后集成该类,以实现克隆接口。包含如下角色:
Prototype,原型类,用来声明克隆方法。在绝大多数情况下,只会有一个名为 clone 的方法。
ConcretePrototype,具体原型类,用来实现克隆方法。除了将原始对象的数据复制到克隆体中之外,该方法有时还需处理克隆过程中的极端情况, 例如克隆关联对象和梳理递归依赖等等。
原型模式类图表示如下:
接下来将使用代码介绍下原型模式的实现:
// 1.定义原型接口,用来声明克隆方法
public interface Prototype {
/**
* 复制对象
*
* @return 复制后的对象
*/
Prototype clone();
}
// 2、定义具体原型类(ConcretePrototype),用来实现克隆方法
public class ConcretePrototype implements Prototype {
private String field;
public ConcretePrototype() {
}
public String getField() {
return field;
}
public ConcretePrototype(String field) {
this.field = field;
}
@Override
public Prototype clone() {
ConcretePrototype concretePrototype = new ConcretePrototype();
concretePrototype.field = this.field;
return concretePrototype;
}
}
// 3、客户端调用
public class PrototypeClient {
public PrototypeClient() {
}
// 调用方式:调用具体原型实例的克隆方法
public void test() {
Prototype concretePrototype = new ConcretePrototype("foo");
ConcretePrototype clonedPrototype = (ConcretePrototype) concretePrototype.clone();
System.out.println(clonedPrototype.getField());
}
}
需要说明的是,对于Java语言来说,Object基类已经提供了一个clone的保护方法,用于实现对象的浅复制。注意,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。更多clone方法的介绍,可以参考笔者之前的文章。
所以,在Java中实现原型模式,并不需要单独定义声明克隆方法的接口,直接继承Object基类并实现Cloneable接口接口。
原型模式有以下优点:
(1) 将对象克隆与对象所属的具体类分离(解耦),避免克隆对象与具体类的紧耦合。
(2) 简化了复杂对象的初始化代码。对一些复杂对象,可以通过提供克隆方法来简化调用方的使用。
(3) 提供了除继承以外的方式来实现复杂对象的初始化。继承建立了子类与父类的强耦合,如果可以,尽量不要使用继承。
但是原型模式也存在以下缺点:
(1) 在重写克隆方法时,对于复杂对象的关联对象或递归依赖等处理相比麻烦一些。
建造器模式,也称建造者模式、生成器模式,是一种创建型模式,用于将一个复杂对象的构建与类的声明分离,使得同样的构建过程可以创建不同类型和形式的对象。建造器模式隐藏了复杂对象的创建过程,允许用户只通过指定复杂对象的类型和内容就可以构建它们,而不需要知道类型的具体构建细节。
在以下情况下可以考虑使用建造器模式:
(1) 如果可能出现"重叠构造函数(telescoping constructor)",可以考虑使用建造器模式重构。例如,当前类的构造函数有十多个可选参数,那么调用该构造函数会非常不方便。因此,需要重载这个构造函数,新建几个只有较少参数的简化版。但这些构造函数仍需调用主构造函数,传递一些默认数值来替代省略掉的参数(仅会出现在支持方法重载的语言,如Java、C++、C#等)。建造器模式可以分步骤生成对象,而且允许仅使用必须的步骤。应用该模式后,可以不需要将十多个参数塞进构造函数里。
(2) 如果需要创建的各种形式的产品,其制造过程相似且仅有细节上的差异,可以考虑使用生成器模式。此时,基本建造器定义所有可能的制造步骤,具体生成器将实现这些步骤来制造特定形式的产品。同时,指挥者类负责管理制造步骤的顺序。
(3) 如果需要生成的产品对象的属性相互依赖,需要指定其生成顺序,可以考虑使用建造器模式。例如,产品的各部分存在依赖顺序,则可以使用建造器模式,并在指挥者类中指定制造步骤的顺序。
(3) 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中。
(4) 如果需要隔离复杂对象的创建和使用,可以考虑使用建造器模式。如需要构造组合树或其他复杂对象,可以使用建造器模式分步骤构造产品。如可以延迟执行某些步骤而不会影响最终产品,或者递归调用这些步骤,以方便创建对象树。
建造器模式通过定义建造者来分担对象的创建职责,具体来说,包含如下角色:
Builder:抽象建造者,用于声明产品构造步骤。
ConcreteBuilder:具体建造者,提供构造步骤的不同实现。
Director:指挥者,定义调用构造步骤的顺序,实现创建和复用特定的产品配置。
Product:产品,最终生成的对象。
建造器模式类图表示如下:
接下来将使用代码介绍下建造器模式的实现。
// 1.定义产品,表示最终生成的对象
public class Product {
private String partA;
private String partB;
private String partC;
public void setPartA(String partA) {
this.partA = partA;
}
public void setPartB(String partB) {
this.partB = partB;
}
public void setPartC(String partC) {
this.partC = partC;
}
public void function() {
System.out.printf("partA %s, partB %s, partC %s", partA, partB, partC);
}
}
// 2、定义抽象建造者,用于声明产品构造步骤
public abstract class Builder {
public abstract void buildPartA();
public abstract void buildPartB();
public abstract void buildPartC();
public abstract Product getProduct();
}
// 3、定义具体建造者,用于实现构造步骤
public class ConcreteBuilder extends Builder {
private Product product = new Product();
@Override
public void buildPartA() {
product.setPartA("A");
}
@Override
public void buildPartB() {
product.setPartB("B");
}
@Override
public void buildPartC() {
product.setPartC("C");
}
@Override
public Product getProduct() {
return this.product;
}
}
// 4、定义指挥者,用于定义调用构造步骤的顺序,实现创建和复用特定的产品配置
public class Director {
public Director(Builder builder) {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
}
}
// 5、客户端调用
public class BuilderClient {
public void test() {
// (1) 实例化建造器
Builder builder = new ConcreteBuilder();
// (2) 基于建造器,实例化指挥者
new Director(builder);
// (3) 从建造器中获取产品对象
Product product = builder.getProduct();
// (4) 使用产品
product.function();
}
}
建造器模式有以下优点:
(1) 符合单一职责原则。建造者模式将复杂的产品创建代码从产品的业务逻辑中分离出来,实现了解耦,简化了产品创建的细节。
(2) 符合开闭原则。增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类实现,系统扩展方便。
(3) 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便控制创建过程。
但是建造器模式也存在以下缺点:
(1) 建造者模式创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
(2) 如果产品内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
结构型模式描述如何将类或对象按某种布局组成更大的结构。
结构型模型按照适用于对象或类可细分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式分为以下 7 种:
代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
适配器(Adapter)模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
桥接(Bridge)模式:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
装饰(Decorator)模式:动态地给对象增加一些职责,即增加其额外的功能。
外观(Facade)模式:为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
享元(Flyweight)模式:运用共享技术来有效地支持大量细粒度对象的复用。
组合(Composite)模式:将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
以上 7 种结构型模式,除了适配器模式分为类结构型模式和对象结构型模式两种,其他的全部属于对象结构型模式。
代理模式是一种结构型设计模式,让开发者能够提供对象的替代品或其占位符。代理对象控制着对于原对象的访问,并允许在将请求提交给原对象前后进行一些处理。代理模式为原对象提供一种代理以控制对这个对象的访问,并由代理对象控制对原对象的引用。
在以下情况下可以考虑使用代理模式:
(1) 远程代理。本地执行远程服务,适用于服务对象位于服务器(本地服务器或远程服务器)上的情形。在这种情形中,代理通过网络传递客户端请求,负责处理所有与网络相关的复杂细节。
(2) 虚拟代理。如果需要创建一个资源消耗较大的对象,一直保持该对象运行会消耗系统资源。可以先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。虚拟代理是一种延迟初始化实现,无需在程序启动时就创建该对象,可将对象的初始化延迟到真正有需要的时候。
(3) Copy-on-Write代理。它是虚拟代理的一种实现,把克隆对象的操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个开销较大的操作,Copy-on-Write代理可以让这个操作延迟,只有对象被用到的时候才被克隆。
(4) 保护代理。该代理控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。如果只希望特定用户使用服务对象,可考虑使用代理模式。
(5) 缓存代理。为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。
(6) 日志记录代理。当需要保存对于服务对象的请求历史记录时,代理可以在向服务传递请求前进行记录。
(7) 智能引用。当一个对象被引用时,提供一些额外的操作,如将此对象被调用的次数记录下来,在没有客户端使用某个重量级对象时立即销毁该对象等。
(8) 图片代理。当需要对大图浏览进行控制时,可以考虑使用代理模式。用户通过浏览器访问网页时先不加载真实的大图,而是通过代理对象的方法来进行处理,在代理对象的方法中,先使用一个线程向客户端浏览器加载一个小图片,然后在后台使用另一个线程来调用大图片的加载方法将大图片加载到客户端。当需要浏览大图片时,再将大图片在新网页中显示。如果用户在浏览大图时加载工作还没有完成,可以再启动一个线程来显示相应的提示信息。通过代理技术结合多线程编程将真实图片的加载放到后台来操作,不影响前台图片的浏览。
(9) 动态代理。动态代理是一种较为高级的代理模式,它的典型应用就是Spring AOP。在传统的代理模式中,客户端通过Proxy调用RealSubject类的request()方法,同时还在代理类中封装了其他方法(如preRequest()和postRequest()),可以处理一些其他问题。如果按照这种方法使用代理模式,那么真实主题角色必须是事先已经存在的,并将其作为代理对象的内部成员属性。如果一个真实主题角色必须对应一个代理主题角色,这将导致系统中的类个数急剧增加,因此需要想办法减少系统中类的个数,此外,如何在事先不知道真实主题角色的情况下使用代理主题角色,这都是动态代理需要解决的问题。
为使用代理对象控制对某个对象(Real Subject)的访问,可以创建一个代理(Proxy)并封装对源对象的访问。为保证代理和这个对象的接口一致,还需提取公共接口(Subject),这样在任何需要控制这个对象的访问的地方,都可使用代理实现。代理模式包含如下角色:
Subject,抽象对象,声明了对象接口。代理必须遵循该接口才能伪装成原对象。
Real Subject,真实对象,实现了对象接口。
Proxy,代理类,包含一个指向服务对象的引用成员变量。代理完成其任务(如延迟初始化、记录日志、访问控制和缓存等)后会将请求传递给服务对象。
代理模式类图表示如下:
接下来将使用代码介绍下代理模式的实现。
// 1、抽象对象,对接口进行声明
public interface Subject {
void operation();
}
// 2、真实对象,实现了接口
public class RealSubject implements Subject {
@Override
public void operation() {
System.out.println("---------do some thing in a real subject instance---------");
}
}
// 3、代理类,包含一个指向代理对象的引用成员变量
public class Proxy implements Subject {
private RealSubject realSubject = new RealSubject();
@Override
public void operation() {
preOperation();
realSubject.operation();
afterOperation();
}
public void preOperation() {
System.out.println("pre operation in the proxy");
}
public void afterOperation() {
System.out.println("after operation in the proxy");
}
}
// 4、客户端调用
public class ProxyClient {
public void test(){
// (1) 声明接口并实例化代理类
Subject subjectProxy = new Proxy();
// (2) 调用对象接口
subjectProxy.operation();
}
}
注意:
(1) 尽管可以提供一个公共接口供代理和对象使用,但是更多的情况是这个对象的实现和代理的实现是两个不同的人或部门开发。一种可能的情况是开发代理类的是客户端开发人员,而开发服务器端类的服务器端开发人员。所以代理和这个对象的公共接口可能并不会被创建。(无法完全做到面向接口编程)
(2) 如果Proxy不需要知道待控制访问的对象的类型,则可使用统一的接口处理代理,而不需要为每个待控制访问的对象创建Proxy。
代理模式,在访问对象时引入了一定程度的间接性。代理模式有以下优点:
(1) 符合开闭原则。可以在不对服务或客户端做出修改的情况下创建新代理。
(2) 隐藏一个对象存在于不同地址空间的事实。如客户端调用服务器端方法,使用代理后,客户端像调用本地方法一样,调用服务器端方法。
(3) 允许在访问一个对象时,进行一些额外的处理。如将组合后的数据返回给调用者,延迟对象的创建时间,对对象进行生命周期管理等。
但是代理模式也存在以下缺点:
(1) 服务响应可能会延迟。由于在客户端和真实对象之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
(2) 代码复杂度上升。实现代理模式需要额外的工作,有些代理模式的实现非常复杂(如动态代理)。
适配器模式是一种结构型设计模式,用于将一个接口转换成用户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
在以下情况下可以考虑使用适配器模式:
(1) 需要使用某个现有类,但是这些类的接口不符合系统的需要,可以考虑使用适配器。如以下场景:
适配器模式允许创建一个中间层类, 其可作为代码与遗留类、第三方类或提供接口的类之间的转换器。
如果需要复用这样一些类,他们处于同一个继承体系,并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。
扩展每个子类,将缺少的功能添加到新的子类中,且无法将功能提取到父类。但是, 必须在所有新子类中重复添加这些代码,这样会使得代码有坏味道。
(2) 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类一起工作,可以考虑使用适配器。
(3) 将缺失功能添加到一个适配器类中是一种优雅的解决方案。 在这方案中,开发者可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模
式非常相似。
适配器模式包含如下角色:
Target:目标类,描述了其他类与客户端代码合作时必须遵循的协议,简单来说,就是客户端使用的目标接口。
Adaptee:适配者类,待适配的类。客户端与其接口不兼容,因此无法直接调用其功能。
Adapter:适配器类,适配器(Adapter)是一个可以同时与客户端和服务交互的类:它在实现客户端接口的同时封装了服务对象。适配器接受客户端通过适配器接口发起的调用,并将其转换为适用于被封装服务对象的调用。
Client:客户端类,客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此,开发者可以向程序中添加新类型的适配器而无需修改已有代码。这在服务类的接口被更改或替换时很有用:开发者无需修改客户端代码就可以创建新的适配器类。
适配器模式有对象适配器和类适配器两种实现:
接下来将使用代码介绍下适配器模式的实现。首先是对象适配器模式的实现:
// 1、目标类,对客户端需要使用的目标接口进行声明
public interface Target {
void request();
}
// 2、适配者类,待适配类,其声明的接口无法被客户端直接调用
public class Adaptee {
public void specialRequest() {
System.out.println("---------do some thing in an adaptee instance---------");
}
}
// 3、适配器类,适配器可以接受客户端通过适配器接口发起的调用,并将其转换为适用于被封装服务对象的调用。
// 对象适配器通过组合适配者实例并实现目标类来完成适配
public class Adapter implements Target {
private Adaptee adaptee = new Adaptee();
@Override
public void request() {
adaptee.specialRequest();
}
}
// 4、客户端调用
public class AdapterClient {
public void test() {
Target adapter = new Adapter();
adapter.request();
}
}
其次是类适配器模式的实现:
// 1、目标类,对客户端需要使用的目标接口进行声明
public interface Target {
void request();
}
// 2、适配者类,待适配类,其声明的接口无法被客户端直接调用
public class Adaptee {
public void specialRequest() {
System.out.println("---------do some thing in an adaptee instance---------");
}
}
// 3、适配器类,适配器可以接受客户端通过适配器接口发起的调用,并将其转换为适用于被封装服务对象的调用。
// 类适配器通过继承适配者实例并实现目标类来完成适配
public class Adapter extends Adaptee implements Target {
@Override
public void request() {
specialRequest();
}
}
// 4、客户端调用
public class AdapterClient {
public void test() {
Target adapter = new Adapter();
adapter.request();
}
}
从上面的实现不难发现,类适配器是基于继承实现,而对象适配器是基于组合关系实现。由于对象适配器是通过关联关系进行耦合的,因此在设计时更灵活,而类适配器就只能通过重写Adaptee的方法进行扩展。
适配器让接口不兼容的对象可以相互合作。适配器模式有以下优点:
(1) 单一职责。可以将接口或数据转换代码从程序主要业务逻辑中分离。
(2) 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
(3) 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端来说是透明的,而且提高了适配者的复用性。
(4) 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
类适配器模式还具有如下优点:
由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
对象适配器模式还具有如下优点:
一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。
但是适配器模式也存在以下缺点:
(1) 代码整体复杂度增加, 因为开发者需要新增一系列接口和类。有时直接更改服务类使其与其他代码兼容会更简单。
类适配器模式还具有如下缺点:
对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。
对象适配器模式还具有如下缺点:
与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。
享元模式是一种结构型设计模式,主要通过共享技术有效地减少大量细粒度对象的复用,以减少内存占用和提高性能。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式。
在以下情况下可以考虑使用享元模式:
(1) 一个系统有大量相同或者相似的对象,由于这类对象的大量使用,造成内存的大量耗费。
应用该模式所获的收益大小取决于使用它的方式和情景。它在下列情况中最有效:
程序需要生成数量巨大的相似对象
这将耗尽目标设备的所有内存
对象中包含可抽取且能在多个对象间共享的重复状态
(2) 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
使用享元模式需要维护一个存储享元对象的享元池,而这需要耗费资源,因此,应当在多次重复使用享元对象时才值得使用享元模式。
适配器模式包含如下角色:
Flyweight: 抽象享元类,包含原始对象中部分能在多个对象中共享的状态。 同一享元对象可在许多不同情景中使用。 享元中存储的状态被称为 “内在状态”。 传递给享元方法的状态被称为 “外在状态”。
ConcreteFlyweight: 具体享元类,实现享元接口,并为内部状态(如果存在的话)增加存储空间。
UnsharedConcreteFlyweight: 非共享具体享元类,抽象享元类使共享成为可能,但并不强制共享。
FlyweightFactory: 享元工厂类, 享元工厂会对已有享元的缓存池进行管理。 有了工厂后, 客户端就无需直接创建享元, 它们只需调用工厂并向其传递目标享元的一些内在状态即可。 工厂会根据参数在之前已创建的享元中进行查找, 如果找到满足条件的享元就将其返回; 如果没有找到就根据参数新建享元。
享元模式类图表示如下:
接下来将使用代码介绍下享元模式的实现。
// 1、抽象享元类,存储的状态被称为 "内在状态"。 传递给享元方法的状态被称为 "外在状态"
public abstract class Flyweight {
private String intrinsic;
protected String extrinsic;
public Flyweight(String extrinsic) {
this.extrinsic = extrinsic;
}
public abstract void operation(String extrinsic);
}
// 2、具体享元类,实现享元接口
public class ConcreteFlyweight extends Flyweight {
public ConcreteFlyweight(String extrinsic) {
super(extrinsic);
}
@Override
public void operation(String extrinsic) {
System.out.println("do some thing in the concrete flyweight instance");
}
}
// 3、非共享具体享元类,抽象享元类使共享成为可能,但并不强制共享
public class UnsharedConcreteFlyweight extends Flyweight {
public UnsharedConcreteFlyweight(String extrinsic) {
super(extrinsic);
}
@Override
public void operation(String extrinsic) {
System.out.println("do some thing in the unshared concrete flyweight instance");
}
}
// 4、享元工厂类, 享元工厂会对已有享元的缓存池进行管理。 有了工厂后, 客户端就无需直接创建享元,
// 它们只需调用工厂并向其传递目标享元的一些状态即可
public class FlyweightFactory {
private static final Map<String, Flyweight> pool = new HashMap<>();
public static Flyweight getFlyweight(String extrinsic) {
Flyweight flyweight = pool.get(extrinsic);
if (flyweight == null) {
flyweight = new ConcreteFlyweight(extrinsic);
pool.put(extrinsic, flyweight);
System.out.println("put a fly weight instance to the pool");
}
return flyweight;
}
}
// 5、客户端调用
public class FlyweightClient {
public void test() {
// 从享元工厂获取享元类
Flyweight flyweight1 = FlyweightFactory.getFlyweight("one");
// 执行享元方法
flyweight1.operation("one");
// 从享元工厂获取重复享元类(直接从缓存池获取)
Flyweight flyweight2 = FlyweightFactory.getFlyweight("one");
flyweight2.operation("one");
// 获取非共享享元子类
Flyweight unsharedConcreteFlyweight = new UnsharedConcreteFlyweight("two");
unsharedConcreteFlyweight.operation("two");
}
}
享元模式有以下优点:
(1) 节省内存。享元模式的优点在于它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。如果程序中有很多相似对象,那么可以考虑使用该模式
(2) 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享
但是享元模式也存在以下缺点:
(1) 可能需要牺牲执行速度来换取内存,为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
(2) 代码会变得更加复杂。 需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
外观模式是一种结构型设计模式,用来给子系统中的一组接口提供一个一致的界面。当外部与一到多个子系统的通信必须通过一个统一的外观对象进行时,可以通过外观模式定义一个高层接口,该接口使得这些子系统更加容易使用。外观模式又称为门面模式。
在以下情况可以考虑使用外观模式:
(1) 当要为一个复杂子系统提供一个简单接口时可以使用外观模式。该接口可以满足大多数用户的需求,而且用户也可以越过外观类直接访问子系统。
子系统通常会随着时间的推进变得越来越复杂。 为了解决这个问题,外观将会提供指向子系统中最常用功能的快捷方式,能够满足用户的大部分需求。
(2) 客户程序与多个子系统之间存在很大的依赖性。引入外观类将子系统与客户以及其他子系统解耦,可以提高子系统的独立性和可移植性。
(3) 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
创建外观类来定义子系统中各层次的入口。 可以要求子系统仅使用外观来进行交互,以减少子系统之间的耦合。
外观模式包含如下角色:
Facade: 外观类,提供了一种访问特定子系统功能的便捷方式,将客户的请求代理给合适的子系统对象。
Subsystem: 子系统类,实现子系统的功能,子系统类不会意识到外观的存在,它们在系统内运作并且相互之间可直接进行交互。
外观模式类图表示如下:
接下来将使用代码介绍下外观模式的实现。
// 1、外观类,提供了一种访问特定子系统功能的便捷方式,将客户的请求代理给合适的子系统对象。
public class Facade {
private SystemA systemA = new SystemA();
private SystemB systemB = new SystemB();
private SystemC systemC = new SystemC();
public void operationA() {
systemA.operationA();
}
public void operationB() {
systemB.operationB();
}
public void operationC() {
systemC.operationC();
}
}
// 2、子系统A,实现子系统A的功能
public class SystemA {
public void operationA() {
System.out.println("do some thing in the systmA instance");
}
}
// 3、子系统B,实现子系统B的功能
public class SystemB {
public void operationB() {
System.out.println("do some thing in the SystemB instance");
}
}
// 4、子系统C,实现子系统C的功能
public class SystemC {
public void operationC() {
System.out.println("do some thing in the SystemC instance");
}
}
// 5、客户端调用
public class DecoratorClient {
public void test() {
// (1) 实例化外观类
Facade facade = new Facade();
// (2) 调用外观类接口
facade.operationA();
facade.operationB();
facade.operationC();
}
}
外观模式有以下优点:
(1) 符合"迪米特法则"。对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。通过引入外观模式,客户代码将变得很简单,与之关联的对象也很少。
(2) 实现了子系统与客户之间的松耦合关系,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可。
但是外观模式也存在以下缺点:
(1) 外观类可能成为与程序中所有类都耦合的上帝类。
(2) 不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性。
(3) 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了"开闭原则"。
装饰器模式是一种结构型设计模式,用来动态地给一个对象增加一些额外的职责。就增加对象功能来说,装饰器模式比生成子类实现更为灵活。装饰器模式的别名为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。
在以下情况可以考虑使用装饰器模式:
(1) 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
(2) 需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
装饰器能将业务逻辑组织为层次结构,开发者可为各层创建一个装饰,在运行时将各种不同逻辑组合成对象。由于这些对象都遵循通用接口,客户端代码能以相同的方式使用这些对象。
(3) 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类定义不能继承(如final类)
说明:一般有两种方式可以实现给一个类或对象增加行为:
(1) 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。
(2) 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)
装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。这就是装饰模式的模式动机。
装饰器模式包含如下角色:
Component: 抽象构件,定义一个对象接口,用来动态地给一个对象增加一些额外的职责
ConcreteComponent: 具体构件,实现一个对象接口
Decorator: 抽象装饰类,拥有一个指向被封装对象的引用成员变量。装饰类会将所有操作委派给被封装的对象。
ConcreteDecorator: 具体装饰类,定义了可动态添加到抽象构件的具体行为。具体装饰类会重写装饰基类的方法,并在调用父类方法之前或之后进行额外的行为。
装饰器模式类图表示如下:
接下来将使用代码介绍下装饰器模式的实现。
// 1、抽象构件,定义一个对象接口,用来动态地给一个对象增加一些额外的职责
public interface Component {
void operation();
}
// 2、具体构件,实现一个对象接口
public class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("do some thing in the concrete component instance");
}
}
// 3、抽象装饰类,拥有一个指向被封装对象的引用成员变量。装饰类会将所有操作委派给被封装的对象
public class Decorator implements Component {
private Component component = null;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
this.component.operation();
}
}
// 4、具体装饰类,定义了可动态添加到抽象构件的具体行为。具体装饰类会重写装饰基类的方法,并在调用父类方法之前或之后进行额外的行为
public class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component component) {
super(component);
}
@Override
public void operation() {
super.operation();
afterOperation();
}
private void afterOperation() {
System.out.printf("do some thing after operation in the concrete decorator");
}
}
// 5、客户端调用
public class DecoratorClient {
public void test() {
// (1) 声明接口并实例化组件
Component component = new ConcreteComponent();
// (2) 调用组件方法(装饰前)
component.operation();
// (3) 实例化具体装饰器并对组件进行装饰
component = new ConcreteDecorator(component);
// (4) 调用组件方法(装饰后)
component.operation();
}
}
装饰器模式有以下优点:
(1) 符合开闭原则。具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。
(2) 符合单一职责原则。开发者可以将实现了许多不同行为的一个大类拆分为多个较小的类。
(3) 提高了代码的可扩展性和灵活性。装饰器模式与继承关系的目的都是要扩展对象的功能,但是装饰器模式可以提供比继承更多的灵活性,可以在不创建新子类的前提下,扩展对象的行为。
(4) 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的装饰器,从而实现不同的行为。
(5) 通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合。可以使用多个具体装饰类来装饰同一对象,得到功能更为强大的对象。
但是装饰器模式也存在以下缺点:
(1) 代码复杂度上升。使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
(2) 装饰器模式比继承更加灵活,也同时意味着装饰器模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
桥接模式是一种结构型设计模式, 又称为柄体(Handle and Body)模式或接口(Interface)模式。桥接模式可将抽象部分与它的实现部分分离,使它们都可以独立地变化。如将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构,从而在开发时分别使用。
在以下情况可以考虑使用桥接模式:
(1) 如果想要拆分或重组一个具有多重功能的庞杂类(例如需要与多个数据库进行交互的类,上帝类),可以考虑使用桥接模式。
桥接模式可以将庞杂类拆分为几个类层次结构。 此后, 就可以修改任意一个类层次结构而不会影响到其他类层次结构。 这种方法可以简化代码的维护工作, 并将修改已有代码的风险降到最低。
(2) 如果希望在几个独立维度上扩展一个类, 可以考虑使用桥接模式。
桥接建议将每个维度抽取为独立的类层次。 初始类将相关工作委派给属于对应类层次的对象, 无需自己完成所有工作。
(3) 如果需要在运行时切换不同实现方法, 可以考虑使用桥接模式。
当然并不是说一定要实现这一点, 桥接模式可替换抽象部分中的实现对象, 具体操作就和给成员变量赋新值一样简单。
顺便提一句, 最后一点是很多人混淆桥接模式和策略模式的主要原因。 记住, 设计模式并不仅是一种对类进行组织的方式, 它还能用于沟通意图和解决问题。
(4) 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
(5) 抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
(6) 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
(7) 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。
(8) 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
桥接模式包含如下角色:
Abstraction:抽象类,抽象部分,提供高层控制逻辑,依赖于完成底层实际工作的实现对象。
RefinedAbstraction:扩充抽象类,提供控制逻辑的变体。 与其父类一样, 它们通过通用实现接口与不同的实现进行交互。
Implementor:实现类接口,实现部分,为所有具体实现声明通用接口。抽象部分仅能通过在这里声明的方法与实现对象交互。抽象部分可以列出和实现部分一样的方法,但是抽象部分通常声明一些复杂行为,这些行为依赖于多种由实现部分声明的原语操作。
ConcreteImplementor:具体实现类,包括特定于平台或场景的代码。
桥接模式类图表示如下:
接下来将使用代码介绍下桥接模式的实现。
// 1、抽象部分,提供高层控制逻辑
public class Abstraction {
protected Implementor implementor;
public Abstraction(Implementor implementor) {
this.implementor = implementor;
}
public void operation() {
implementor.operationImp();
}
}
// 2、实现部分,为所有具体实现声明通用接口
public interface Implementor {
void operationImp();
}
// 3、具体实现类,包含特定于平台或场景的代码
public class ConcreteImplementorA implements Implementor {
@Override
public void operationImp() {
System.out.println("---------do some thing in a concrete implementor A instance---------");
}
}
// 4、具体实现类,包括特定于平台或场景的代码
public class RefinedAbstraction extends Abstraction {
public RefinedAbstraction(Implementor implementor) {
super(implementor);
}
@Override
public void operation() {
this.implementor.operationImp();
}
}
// 5、客户端调用
public class BridgeClient {
public void test() {
// (1) 指定抽象部分并绑定具体的实现部分
Abstraction abstraction1 = new RefinedAbstraction(new ConcreteImplementorA());
// (2) 调用抽象部分接口
abstraction1.operation();
}
}
桥接模式有以下优点:
(1) 符合开闭原则。分离抽象接口及其实现部分。开发者可以单独新增抽象部分和实现部分,它们之间不会相互影响。
(2) 符合单一职责原则。抽象部分专注于处理高层逻辑, 实现部分处理平台细节。
(3) 提高了代码的可扩展性。在抽象部分或实现部分任意扩展一个维度,都不需要修改原有系统。
(4) 提高了代码的透明性。 实现细节对客户透明,可以对用户隐藏实现细节。
(5) 提高了代码的复用性。桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差。而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法。
但是桥接模式也存在以下缺点:
(1) 代码复杂度上升。比如对高内聚的类使用该模式可能会使代码更加复杂。
(2) 增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
(3) 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
组合模式是一种结构型设计模式,主要用来将多个对象组织成树形结构以表示“部分-整体”的层次结构,因此该模式也称为“部分-整体”模式。简言之,组合模式就是用来将一组对象组合成树状结构,并且能像使用独立对象一样使用它们。
在以下情况下可以考虑使用组合模式:
(1) 如果需要实现树状对象结构,可以考虑使用组合模式。
组合模式提供了两种共享公共接口的基本元素类型:简单叶节点和复杂容器。容器中可以包含叶节点和其他容器。这使得开发者可以构建树状嵌套递归对象结构。
(2) 如果希望客户端代码以相同方式处理简单和复杂元素, 可以使用该模式。
组合模式中定义的所有元素共用同一个接口。在这一接口的帮助下,客户端不必在意其所使用的对象的具体类。
为实现组合模式,首先需要创建一个可以组合多个对象的单一对象(Component),这个对象用来访问和管理其子对象,并对外提供公共接口。然后,定义没有子节点的对象(Leaf,基本对象)和包含子对象的对象(Composite,组合对象)。最后,将这些对象组装到之前创建的对象上。这样,外部(Client)就可通过Component调用公共接口。组合模式包含如下角色:
Component,组合对象,为组合中的对象声明公共接口,并提供默认实现。
Leaf,叶节点对象,叶节点最终会完成大部分的实际工作,因为它们无法将工作指派给其他部分。
Compoiste,组合,也称容器,包含叶节点或其他容器的单位。容器不知道其子项目所属的具体类, 它只通过通用的组件接口与其子项目交互。
组合模式类图表示如下:
注意:
(1) 组合模式对基本对象和组合对象的使用具有一致性。外部代码调用Component公共接口时,无需区别对待基本对象和组合对象(透明性),大多数情况下可以一致地处理它们。
接下来将使用代码介绍下组合模式的实现。
// 1、Component,组合对象,为组合中的对象声明公共接口,并提供默认实现。
public abstract class Component {
private String name;
protected List<Component> children = new ArrayList<>();
public Component(String componentName) {
this.name = componentName;
}
public void operation() {
System.out.println(this.name);
}
public Component getChild(String componentName) {
for (Component current : children) {
if (current.name.equals(componentName)) {
return current;
}
Component childComponent = current.getChild(componentName);
if (childComponent != null) {
return childComponent;
}
}
return null;
}
public abstract void add(Component component);
public abstract void remove(Component component);
}
// 2、Compoiste,组合,也称容器,包含叶节点或其他容器的单位。容器不知道其子项目所属的具体类,
// 它只通过通用的组件接口与其子项目交互。
public class Composite extends Component {
public Composite(String componentName) {
super(componentName);
}
@Override
public void add(Component component) {
this.children.add(component);
}
@Override
public void remove(Component component) {
this.children.remove(component);
}
}
// 3、Leaf,叶节点对象,叶节点最终会完成大部分的实际工作,因为它们无法将工作指派给其他部分。
public class Leaf extends Component {
public Leaf(String componentName) {
super(componentName);
}
@Override
public void add(Component component) {
throw new RuntimeException("叶节点不能添加子节点");
}
@Override
public void remove(Component component) {
throw new RuntimeException("叶节点不包含子节点,无法移除子节点");
}
}
// 4、客户端调用
public class CompositeClient {
public void test() {
Component root = new Composite("root");
root.add(new Leaf("Leaf A"));
Composite branch = new Composite("Composite X");
Leaf leafXa = new Leaf("Leaf XA");
branch.add(leafXa);
branch.add(new Leaf("Leaf XB"));
branch.remove(leafXa);
root.add(branch);
Component leafXb = root.getChild("Leaf XB");
leafXb.operation();
}
}
这里只介绍了基于透明性的设计与实现,组合模式还支持一种基于安全性的设计与实现,更多安全性相关知识可以执行搜索并学习。
组合模式最大特点是将多个对象组织成树形结构。组合模式有以下优点:
(1) 可以利用多态和递归机制更方便地使用复杂树结构。
(2) 符合开闭原则。无需更改现有代码,开发者就可以在应用中添加新元素,使其成为对象树的一部分。
但是组合模式也存在以下缺点:
(1) 对于功能差异较大的类, 提供公共接口或许会有困难。在特定情况下,开发者需要过度一般化组件接口,使其变得令人难以理解。
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“组合复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
行为型模式是 GoF 设计模式中最为庞大的一类,它包含以下 11 种模式。
模板方法(Template Method)模式:定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中,使得子类在可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
策略(Strategy)模式:定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
职责链(Chain of Responsibility)模式:把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
状态(State)模式:允许一个对象在其内部状态发生改变时改变其行为能力。
观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
迭代器(Iterator)模式:提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
访问者(Visitor)模式:在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
备忘录(Memento)模式:在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
解释器(Interpreter)模式:提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。
以上 11 种行为型模式,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式,下面我们将详细介绍它们的特点、结构与应用。
策略模式是一种行为设计模式,就是定义一系列算法,然后将每一个算法封装起来,并使它们可相互替换。本模式通过定义一组可相互替换的算法,实现将算法独立于使用它的用户而变化。
在以下情况可以考虑使用策略模式:
(1) 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
(2) 一个系统需要动态地在几种算法中选择一种, 可考虑使用策略模式。
策略模式能够将对象关联至可以不同方式执行特定子任务的不同子对象,从而以间接方式在运行时更改对象行为。
(3) 当类中使用了复杂条件运算符(如多重的条件选择语句)以在同一算法的不同变体中切换时, 可使用该模式。
策略模式可将所有继承自同样接口的算法抽取到独立类中,因此可以不需要条件语句。原始对象并不实现所有算法的变体,而是将执行工作委派给其中的一个独立算法对象。
(4) 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法和相关的数据结构,提高算法的保密性与安全性。
策略模式能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。 不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。
为实现一系列可相互替换的算法,可定义一个公共接口,然后定义一组实现该接口的具体策略,这样就可在上下文中使用该接口调用具体策略上定义的算法。
Context,上下文,维护指向具体策略的引用,且仅通过策略接口与该对象进行交流。上下文,可以维护一个对策略对象的引用,这符合组合设计原则。
上下文,定义了一个接口以封装对策略对象的访问。如果策略对象直接暴露给外部使用,会导致其内部实现细节的暴露,从而增加接口使用难度。
Strategy,策略基类或策略接口,声明了一个上下文用于执行策略的方法。
ConcreteStrategy,具体策略类,实现了策略类声明的方法。
策略模式类图表示如下:
接下来将使用代码介绍下策略模式的实现。
// 1、抽象策略类,声明执行策略的方法
public interface IStrategy {
void operation(String paramStr);
}
// 2、具体策略类A,实现策略接口声明的方法
public class ConcreteAStrategy implements IStrategy {
@Override
public void operation(String paramStr) {
System.out.println("do some thing in the concrete A instance");
}
}
// 2、具体策略类B,实现策略接口声明的方法
public class ConcreteBStrategy implements IStrategy {
@Override
public void operation(String paramStr) {
System.out.println("do some thing in the concrete B instance");
}
}
// 3、策略上下文,维护指向具体策略的引用,且仅通过策略接口与该对象进行交流。这里提供两种使用策略类的方式。
public class StrategyContext {
private static final Map<String, IStrategy> STRATEGY_MAP;
private IStrategy strategy;
static {
STRATEGY_MAP = new HashMap<>();
STRATEGY_MAP.put("type A", new ConcreteAStrategy());
STRATEGY_MAP.put("type B", new ConcreteBStrategy());
}
public StrategyContext() {
}
public StrategyContext(IStrategy strategy) {
this.strategy = strategy;
}
public void doSomething(String paramStr) {
strategy.operation(paramStr);
}
public void doSomething(String strategyType, String paramStr) {
IStrategy currentStrategy = STRATEGY_MAP.get(strategyType);
if (Objects.isNull(currentStrategy)) {
throw new RuntimeException("strategy is null");
}
currentStrategy.operation(paramStr);
}
}
// 4、策略模式客户端
public class StrategyClient {
public void test() {
StrategyContext strategyContextA = new StrategyContext(new ConcreteAStrategy());
strategyContextA.doSomething("TEST");
StrategyContext strategyContextB = new StrategyContext();
strategyContextB.doSomething("type B", "TEST");
}
}
策略模式有以下优点:
(1) 符合开闭原则。可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
(2) 定义一系列可重用的算法。策略模式提供了管理相关的算法族的办法。
(3) 使用组合来代替继承。实现支持多种算法或行为的方法。
(4) 避免使用多重条件语句。当不同的行为堆砌在一个类时,很难避免使用条件语句来选择合适的行为。如果将行为封装在一个个独立的Strategy类中,则可消除这些条件语句。
如使用字典的初始化从文件中读取的方式,就可将策略配置移除到外部,从而进一步减少不必要的代码修改。
但是策略模式也存在以下缺点:
(1) 如果使用的算法极少发生改变,那么没有任何理由引入新的类和接口。使用策略模式只会让程序过于复杂。
(2) 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
(3) 许多现代编程语言支持函数类型功能,允许在一组匿名函数中实现不同版本的算法。这样,就可以使用这些函数来替换策略对象,无需借助额外的类和接口来保持代码简洁。如在Java语言中是Lambda表达式,在C++语言中是函数指针。
模板方法模式是一种行为设计模式,在超类中定义了一个算法的框架,而将一些步骤的实现延迟到子类中,使得子类可重定义该算法的特定步骤。
在以下情况下可以考虑使用模板方法模式:
(1) 如果只希望子类扩展某个特定算法步骤,而不是整个算法或其结构时,可考虑使用模板方法模式。模板方法将整个算法转换为一系列独立的步骤,以便子类能对其进行扩展,同时还可让超类中所定义的结构保持完整。
(2) 当多个类的算法除一些细微不同之外几乎完全一样时, 可使用该模式。但其后果就是,只要算法发生变化,就可能需要修改所有的类。在将算法转换为模板方法时,可将相似的实现步骤提取到超类中以去除重复代码。子类间各不同的代码可继续保留在子类中。
模板方法模式包含如下角色:
Abstract Class,抽象类,实现一个模板方法,定义了算法的骨架。需要说明的是,算法中的步骤可以被声明为抽象类型,也可以提供一些默认实现。
Concrete Class,具体类,按需实现算法步骤,但不能重写模板方法自身。
模板方法模式类图表示如下:
一个模板方法用一些抽象的操作定义一个算法,而子类将重定义这些操作以提供具体的行为。通过使用抽象操作定义一个算法的一些步骤,模板方法确定了它们的先后顺序,但允许子类改变这些具体步骤以满足它们各自的需求。
接下来将使用代码介绍下模板方法模式的实现。
// 1、抽象类,实现一个模板方法,定义了算法的骨架
public abstract class AbstractClass {
// 将方法声明为final,表示该方法不可override
public final void templateMethod() {
operation1();
operation2();
}
protected abstract void operation1();
protected abstract void operation2();
}
//2、具体类,按需实现算法步骤
public class ConcreteClassA extends AbstractClass {
@Override
protected void operation1() {
System.out.println("operation1 in a ConcreteClassA instance");
}
@Override
protected void operation2() {
System.out.println("operation2 in a ConcreteClassA instance");
}
}
public class ConcreteClassB extends AbstractClass {
@Override
protected void operation1() {
System.out.println("operation1 in a ConcreteClassB instance");
}
@Override
protected void operation2() {
System.out.println("operation2 in a ConcreteClassB instance");
}
}
// 3、客户端
public class TemplateMethodClient {
public void test() {
// (1) 创建具体类示例
AbstractClass classA = new ConcreteClassA();
// (2) 调用模板方法
classA.templateMethod();
AbstractClass classB = new ConcreteClassB();
classB.templateMethod();
}
}
模板方法模式有以下优点:
(1) 仅允许子类重写算法中的特定部分,使得算法其他部分修改对其所造成的影响减小。
(2) 可将重复代码提取到父类中。
但是该模式也存在以下缺点:
(1) 部分子类可能会受到算法框架的限制。如果算法框架需要调整,则该模式不适用。
(2) 通过子类抑制默认步骤实现可能会导致违反里氏替换原则。
(3) 每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大。模板方法中的步骤越多,其维护工作就可能会越困难。
责任链模式是一种行为设计模式,允许将请求沿着处理者链进行发送。收到请求后,每个处理者均可对请求进行处理,或将其传递给链上的下个处理者。职责链模式使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。
在以下情况可以考虑使用责任链模式:
(1) 当必须按顺序执行多个处理者时, 可以使用该模式。无论以何种顺序将处理者连接成一条链,所有请求都会严格按照顺序通过链上的处理者。
(2) 当需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时,可以使用责任链模式。该模式能将多个处理者连接成一条链。
接收到请求后, 它会 “询问” 每个处理者是否能够对其进行处理。这样所有处理者都有机会来处理请求。
(3) 如果所需处理者及其顺序必须在运行时进行改变, 可以使用责任链模式。如果在处理者类中有对引用成员变量的设定方法, 将能动态地插入和移除处理者, 或者改变其顺序。
责任链模式包含如下角色:
Handler是抽象处理者,声明了一个处理请求的接口,该接口通常仅包含单个方法用于请求处理,但有时还会包含一个设置后继者的方法。
ConcreteHandler是具体处理者,处理它所负责的请求,每个处理者接收到请求后,都必须决定是否进行处理,以及是否向下传递请求。可访问它的后继者,如果可处理该请求就处理,否则就将该请求转发给它的后继者。
处理者通常是独立且不可变的, 需要通过构造函数一次性地获得所有必要地数据。
责任链模式类图表示如下:
接下来将使用代码介绍下责任链模式的实现。
// 1、Handler是抽象处理者,声明了一个处理请求的接口,另外还包含一个设置后继者的方法
public abstract class Handler {
private Handler nextHandler;
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
protected void doNext() {
if (nextHandler != null) {
nextHandler.handleRequest();
}
}
public abstract void handleRequest();
}
// 2、具体处理者,处理它所负责的请求,接收到请求后,决定是否进行处理,以及是否向下传递请求
public class ConcreteHandlerA extends Handler {
@Override
public void handleRequest() {
System.out.println("I am a concrete handler A instance");
doNext();
}
}
public class ConcreteHandlerB extends Handler {
@Override
public void handleRequest() {
System.out.println("I am a concrete handler B instance");
doNext();
}
}
// 3、客户端
public class ResponsibilityClient {
public void test() {
// (1) 创建处理器实例
Handler handlerA = new ConcreteHandlerA();
Handler handlerB = new ConcreteHandlerB();
// (2) 定义先后顺序
handlerA.setNextHandler(handlerB);
// (3) 处理请求
handlerA.handleRequest();
}
}
责任链模式有以下优点:
(1) 提高了灵活性。请求处理的顺序可以按需控制,提高了系统的灵活性。
(2) 符合开闭原则。可以在不更改现有代码的情况下在程序中新增处理者。
(3) 符合单一职责原则。发起操作和执行操作的类进行解耦。
但是该模式也存在以下缺点:
(1) 性能可能会受到影响。职责链的链路不宜过长,可能会引入性能问题。
(2) 不能保证请求一定被接收。因为没有指定处理者,所以存在请求直到链的末尾都得不到处理的情况。
观察着模式是一种行为设计模式,可以用来定义对象间的一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
在以下情况下可以考虑使用观察者模式:
(1) 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
(2) 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可使用观察者模式,以降低对象之间的耦合度。
(3) 一个对象必须通知其他对象,而并不知道这些对象是谁。
(4) 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
观察者模式包含如下角色:
Subject: 目标,提供注册和删除观察者对象的接口。会向观察者对象发送值得关注的事件。
ConcreteSubject: 具体目标,实现注册和删除观察者对象的接口。当目标的状态发生改变时,目标会遍历观察者列表并调用每个观察者对象的通知方法。
Observer: 观察者,为那些在目标发生改变时,需获得通知的对象定义了一个更新接口。在绝大多数情况下,该接口仅包含一个update方法。该方法可以拥有多个参数,使目标能在状态更新时传递详细信息。
ConcreteObserver: 具体观察者,维护一个指向ConcreteSubject的引用。实现Observer的更新接口,已使自身状态与目标状态保持一致。
观察者模式类图表示如下:
接下来将使用代码介绍下观察者模式的实现。
// 1、观察者,定义了一个更新接口,用于目标发生改变时,传递详细信息
public class Observer {
public void update() {
System.out.println("I am an observer instance");
}
}
// 2、具体观察者,实现观察者的更新接口,使自身状态与目标状态保持一致
public class ConcreteObserver extends Observer {
public void update() {
super.update();
doSomething();
}
private void doSomething() {
System.out.println("I am a concrete observer instance");
}
}
// 3、目标,提供注册和删除观察者对象的接口,会向观察者对象发送值得关注的事件
public abstract class Subject {
private List<Observer> observerList = new ArrayList<>();
public void attach(Observer observer) {
observerList.add(observer);
}
public void detach(Observer observer) {
observerList.remove(observer);
}
public void notifyObserver() {
if (observerList == null || observerList.size() == 0) {
return;
}
observerList.forEach(Observer::update);
}
public abstract void doSomething();
}
// 4、具体目标,实现目标的接口,指定通知观察者的具体时机
public class ConcreteSubject extends Subject {
public void doSomething() {
notifyObserver();
}
}
// 5、客户端
public class ObserverClient {
public void test() {
Observer observer1 = new ConcreteObserver();
Observer observer2 = new ConcreteObserver();
Subject subject = new ConcreteSubject();
subject.attach(observer1);
subject.attach(observer2);
subject.doSomething();
subject.detach(observer2);
subject.doSomething();
}
}
观察者模式有以下优点:
(1) 松耦合。在观察目标和观察者之间建立一个抽象的耦合。
(2) 符合开闭原则。无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。
(3) 支持广播通信。
(4) 可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色。
但是该模式也存在以下缺点:
(1) 如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
(2) 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
(3) 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
访问者模式是一种行为设计模式,可封装一些作用于当前数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
在以下情况下可以考虑使用访问者模式:
(1) 如果需要对一个复杂对象结构中的所有元素执行某些操作,可考虑使用访问者模式。访问者模式通过在访问者对象中为多个目标类提供相同操作的变体,让开发者能在属于不同类的一组对象上执行同一操作。
(2) 可使用访问者模式来清理辅助行为的业务逻辑。访问者模式可将所有非主要的行为抽取到一组访问者类中,使得程序的主要类能更专注于主要的工作。
(3) 当某个行为仅在类层次结构中的一些类中有意义,而在其他类中没有意义时,可考虑使用访问者模式。可将该行为抽取到单独的访问者类中,只需实现接收相关类的对象作为参数的访问者方法并将其他方法留空即可。
访问者模式包含如下角色:
Visitor,访问者基类,声明了一系列以对象结构的具体元素为参数的访问者方法。这些方法的名称可能是相同的,但是其参数一定是不同的。
ConcreteVisitor,具体访问者,会为不同的具体元素类实现相同行为的几个不同版本。
ObjectStructure,对象结构类,该类能枚举它包含的元素,可以提供一个高层的接口以允许访问者访问它的元素。
Element,元素,声明了一个方法来“接收”(accept)访问者。该方法必须有一个参数被声明为访问者接口类型。
ConcreteElement,具体元素,实现Element声明的接口。该方法的目的是根据当前元素类将其调用重定向到相应访问者的方法。
Client,客户端,客户端通常不知晓所有的具体元素类,因为它们会通过抽象接口与集合中的对象进行交互。
访问者模式类图表示如下:
访问者模式可将数据结构与数据操作分离,可以解决稳定的数据结构和易变的操作耦合问题。
接下来将使用代码介绍下访问者模式的实现。
// 1、访问者基类,声明了对对象结构的具体元素为参数的访问者方法
public interface IVisitor {
void visitElement(ConcreteElementA element);
void visitElement(ConcreteElementB element);
}
//2、具体访问者,为不同的具体元素类实现相同行为的几个不同版本
public class ConcreteVisitorA implements IVisitor {
@Override
public void visitElement(ConcreteElementA element) {
System.out.println("handle a ConcreteElementA instance in ConcreteVisitorA");
}
@Override
public void visitElement(ConcreteElementB element) {
System.out.println("handle a ConcreteElementB instance in ConcreteVisitorA");
}
}
public class ConcreteVisitorB implements IVisitor {
@Override
public void visitElement(ConcreteElementA element) {
System.out.println("handle a ConcreteElementA instance in ConcreteVisitorB");
}
@Override
public void visitElement(ConcreteElementB element) {
System.out.println("handle a ConcreteElementB instance in ConcreteVisitorB");
}
}
// 3、元素,声明了一个方法来“接收”(accept)访问者。该方法必须有一个参数被声明为访问者接口类型
public interface IElement {
void accept(IVisitor visitor);
}
// 4、具体元素,实现Element声明的接口
public class ConcreteElementA implements IElement {
public void accept(IVisitor visitor) {
visitor.visitElement(this);
}
}
public class ConcreteElementB implements IElement {
public void accept(IVisitor visitor) {
visitor.visitElement(this);
}
}
// 5、对象结构类,可枚举它包含的元素,可以提供一个高层的接口以允许访问者访问它的元素
public class ObjectStructure {
private IElement elementA;
private IElement elementB;
public ObjectStructure(IElement elementA, IElement elementB) {
this.elementA = elementA;
this.elementB = elementB;
}
public IElement getElementA() {
return this.elementA;
}
public IElement getElementB() {
return this.elementB;
}
}
// 6、客户端
public class VisitorClient {
public void test() {
// (1) 创建元素实例
IElement elementA = new ConcreteElementA();
IElement elementB = new ConcreteElementB();
// (2) 创建对象结构实例
ObjectStructure objectStructure = new ObjectStructure(elementA, elementB);
// (3) 创建具体访问者实例
IVisitor visitorA = new ConcreteVisitorA();
// (4) 调用访问者方法
visitorA.visitElement((ConcreteElementA) objectStructure.getElementA());
visitorA.visitElement((ConcreteElementB) objectStructure.getElementB());
IVisitor visitorB = new ConcreteVisitorB();
visitorB.visitElement((ConcreteElementA) objectStructure.getElementA());
visitorB.visitElement((ConcreteElementB) objectStructure.getElementB());
}
}
访问者模式有以下优点:
(1) 符合开闭原则。以引入在不同类对象上执行的新行为, 且无需对这些类做出修改。
(2) 符合单一职责原则。可将同一行为的不同版本移到同一个类中。
但是该模式也存在以下缺点:
(1) 代码可能会变得更加复杂。使用访问者模式可能会导致某些系统有过多的具体访问者类。
(2) 每次在元素层次结构中添加或移除一个类时,都要更新所有的访问者,所以该模式对于频繁调整对象结构的类并不友好。
(3) 在访问者同某个元素进行交互时,可能没有访问元素私有成员变量和方法的必要权限。这与迪米特法则相违背。
(4) 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素。
中介者模式是一种行为设计模式,可以减少对象之间混乱无序的依赖关系。该模式会限制对象之间的直接交互,迫使它们通过一个封装了对象间交互行为的中介者对象来进行合作,从而使对象间耦合松散,并可独立地改变它们之间的交互。中介者模式又称为调停者模式。
在以下情况下可以考虑使用中介者模式:
(1) 当一些对象和其他对象紧密耦合,产生的相互依赖关系结构混乱且难以理解,从而导致难以对其进行修改时,可考虑使用中介者模式。中介者模式可将对象间的所有关系抽取成为一个单独的类,以使对于特定组件的修改工作独立于其他组件。
(2) 当组件因过于依赖其他组件而无法在不同应用中复用时,可考虑使用中介者模式。应用中介者模式后, 每个组件不再知晓其他组件的情况。尽管这些组件无法直接交流,但它们仍可通过中介者对象进行间接交流。
如果希望在不同应用中复用一个组件,则需要为其提供一个新的中介者类。
(3) 如果为了能在不同情景下复用一些基本行为,导致需要被迫创建大量组件子类时,可考虑使用中介者模式。由于所有组件间关系都被包含在中介者中, 因此无需修改组件就能方便地新建中介者类以定义新的组件合作方式。
中介者模式包含如下角色:
Component,组件基类,声明组件的基本功能,有一个指向中介者的引用,该引用被声明为中介者接口类型。组件不知道中介者实际所属的类,因此可通过将其连接到不同的中介者以使其能在其他程序中复用。
Mediator,中介者接口,声明了与组件交流的方法,但通常仅包括一个通知方法。组件可将任意上下文作为该方法的参数,只有这样接收组件和发送者类之间才不会耦合。
ConcreteComponent,具体组件,实现组件声明的方法,并自定义业务逻辑接口。
ConcreteMediator,具体中介者,实现中介者接口声明的方法。
中介者模式类图表示如下:
接下来将使用代码介绍下中介者模式的实现。
// 1、中介者接口,声明了与组件交流的方法
public interface IMediator {
void notify(Sender sender);
}
//2、具体中介者,实现中介者接口声明的方法
public class ConcreteMediator implements IMediator {
@Override
public void notify(Sender sender) {
String message = sender.getMessage();
Component target = sender.getTarget();
target.operation(message);
}
}
// 3、组件基类,声明组件的基本功能,有一个指向中介者的引用,该引用被声明为中介者接口类型
public abstract class Component {
protected IMediator mediator;
public Component(IMediator mediator) {
this.mediator = mediator;
}
public void operation(String message) {
System.out.println("message is " + message);
}
public void send(String message, Component target) {
Sender sender = new Sender(message, this, target);
mediator.notify(sender);
}
}
// 4、具体组件,实现组件声明的方法,并自定义业务逻辑接口
public class ConcreteComponentA extends Component {
public ConcreteComponentA(IMediator mediator) {
super(mediator);
}
@Override
public void operation(String message) {
super.operation(message);
operationA();
}
public void operationA() {
System.out.println("operationA in a Concrete ComponentA instance");
}
}
public class ConcreteComponentB extends Component {
public ConcreteComponentB(IMediator mediator) {
super(mediator);
}
@Override
public void operation(String message) {
super.operation(message);
operationB();
}
public void operationB() {
System.out.println("operationB in a Concrete ComponentB instance");
}
}
public class ConcreteComponentC extends Component {
public ConcreteComponentC(IMediator mediator) {
super(mediator);
}
@Override
public void operation(String message) {
super.operation(message);
operationC();
}
public void operationC() {
System.out.println("operationC in a Concrete ComponentC instance");
}
}
public class ConcreteComponentD extends Component {
public ConcreteComponentD(IMediator mediator) {
super(mediator);
}
@Override
public void operation(String message) {
super.operation(message);
operationD();
}
public void operationD() {
System.out.println("operationD in a Concrete ComponentD instance");
}
}
// 5、客户端
public class MediatorClient {
public void test() {
IMediator mediator = new ConcreteMediator();
Component componentA = new ConcreteComponentA(mediator);
Component componentB = new ConcreteComponentB(mediator);
Component componentC = new ConcreteComponentC(mediator);
Component componentD = new ConcreteComponentD(mediator);
componentA.send("i am a", componentB);
componentB.send("i am b", componentC);
componentC.send("i am c", componentD);
componentD.send("i am d", componentA);
}
}
public class Sender {
private String message;
private Component source;
private Component target;
public Sender(String message, Component source, Component target) {
this.message = message;
this.source = source;
this.target = target;
}
public String getMessage() {
return this.message;
}
public Component getSource() {
return this.source;
}
public Component getTarget() {
return this.target;
}
}
中介者模式有以下优点:
(1) 符合单一职责原则。可以将多个组件间的交流抽取到同一位置,使其更易于理解和维护。
(2) 符合开闭原则。无需修改实际组件就能增加新的中介者。
(3) 可以减轻应用中多个组件间的耦合情况。
但是该模式也存在以下缺点:
(1) 在具体中介者类中包含了组件之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。一段时间后,中介者可能会演化成为上帝对象。
解释器模式是一种行为设计模式,可以解释语言的语法或表达式。给定一个语言,定义它的文法的一种表示,然后定义一个解释器,使用该文法来解释语言中的句子。解释器模式提供了评估语言的语法或表达式的方式。
在以下情况可以考虑使用解释器模式:
(1)如果需要解释执行的语言中的句子,可以表示为一个抽象语法树,可以考虑使用解释器模式。如SQL 解析、符号处理引擎、正则表达式等。
(2) 对于重复出现的问题,如果可以使用简单的语言来表达,可以考虑使用解释器模式。
(3) 一个简单语法需要解释的场景,可以考虑使用解释器模式。对于简单语法,由于其文法规则较简单,使用解释器模式要优于语法分析程序。
解释器模式包含如下角色:
Context,上下文,包含解释器之外的一些全局信息。
AbstractExpression,抽象表达式,声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点所共享。
TerminalExression,终结符表达式,实现与文法中的终结符相关联的解释操作。
NonterminalExpression,非终结符表达式,实现与文法中的非终结符相关联的解释操作,对文法中每一条规则R1、R2、…、Rn 都需要一个具体的非终结符表达式类。
Client,客户端,构建一个句子,它是TerminalExression和NonterminalExpression的实例的一个抽象语法树,然后初始化Context,并调用解释操作。
解释器模式类图表示如下:
接下来将使用代码介绍下解释器模式的实现。由于无法使用抽象的用例表示出解释器模式,所以这里会基于特定的场景给出代码示例。这里以常见的四则运算(由于除法需要特殊处理,这里暂不提供)的解析为例,介绍下解释器模式的实现。
// 1、抽象表达式,声明一个抽象的解释操作接口
public interface Expression {
int interpret();
}
//2、终结符表达式,实现与文法中的终结符相关联的解释操作,这里是数字
public class NumberExpression implements Expression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
public NumberExpression(String number) {
this.number = Integer.parseInt(number);
}
@Override
public int interpret() {
return this.number;
}
}
// 3、非终结符表达式,实现与文法中的非终结符相关联的解释操作,这里是运算符
public class AdditionExpression implements Expression {
private Expression firstExpression, secondExpression;
public AdditionExpression(Expression firstExpression, Expression secondExpression) {
this.firstExpression = firstExpression;
this.secondExpression = secondExpression;
}
@Override
public int interpret() {
return Math.addExact(this.firstExpression.interpret(), this.secondExpression.interpret());
}
@Override
public String toString() {
return "+";
}
}
public class SubtractionExpression implements Expression {
private Expression firstExpression, secondExpression;
public SubtractionExpression(Expression firstExpression, Expression secondExpression) {
this.firstExpression = firstExpression;
this.secondExpression = secondExpression;
}
@Override
public int interpret() {
return Math.subtractExact(this.firstExpression.interpret(), this.secondExpression.interpret());
}
@Override
public String toString() {
return "-";
}
}
public class MultiplicationExpression implements Expression {
private Expression firstExpression, secondExpression;
public MultiplicationExpression(Expression firstExpression, Expression secondExpression) {
this.firstExpression = firstExpression;
this.secondExpression = secondExpression;
}
@Override
public int interpret() {
return Math.multiplyExact(this.firstExpression.interpret(), this.secondExpression.interpret());
}
@Override
public String toString() {
return "*";
}
}
// 4、表达式分析器,将输入解析成表达式并执行相关的计算
public class ExpressionParser {
private static final String ADD = "+";
private static final String SUBTRACT = "-";
private static final String MULTIPLY = "*";
private static final String SPLITTER = " ";
private LinkedList<Expression> stack = new LinkedList();
public int parse(String str) {
String[] tokenList = str.split(SPLITTER);
for (String symbol : tokenList) {
if (!isOperator(symbol)) {
Expression numberExpression = new NumberExpression(symbol);
stack.push(numberExpression);
} else {
Expression firstExpression = stack.pop();
Expression secondExpression = stack.pop();
Expression operator = getExpressionObject(firstExpression, secondExpression, symbol);
if (operator == null) {
throw new RuntimeException("unknown symbol: " + symbol);
}
int result = operator.interpret();
NumberExpression resultExpression = new NumberExpression(result);
stack.push(resultExpression);
}
}
return stack.pop().interpret();
}
private boolean isOperator(String symbol) {
return symbol.equals(ADD) || symbol.equals(SUBTRACT) || symbol.equals(MULTIPLY);
}
private Expression getExpressionObject(Expression firstExpression, Expression secondExpression, String symbol) {
switch (symbol) {
case ADD:
return new AdditionExpression(firstExpression, secondExpression);
case SUBTRACT:
return new SubtractionExpression(firstExpression, secondExpression);
case MULTIPLY:
return new MultiplicationExpression(firstExpression, secondExpression);
default:
return null;
}
}
}
// 5、客户端
public class InterpreterClient {
public void test() {
// (1) 定义输入
String input = "2 1 5 + *";
System.out.println("input is: " + input);
// (2) 创建表达式分析器实例
ExpressionParser expressionParser = new ExpressionParser();
// (3) 执行分析操作
int result = expressionParser.parse(input);
System.out.println("result: " + result);
}
}
解释器模式有以下优点:
(1) 可扩展性好。因为该模式使用类来表示文法规则,可以使用继承来改变或扩展该文法。
(2) 易于实现简单的文法。定义抽象语法树各个节点的类的实现大体相似。
但是该模式也存在以下缺点:
(1) 可利用场景比较少。
(2) 对于复杂的文法比较难维护。包含许多规则的文法可能难以管理和维护。
(3) 会引起类膨胀。随着文法规则的复杂化,类的规模也会随之膨胀。
(4) 使用了大量的循环和递归,需要考虑效率问题。
迭代器模式是一种行为设计模式,可以在不暴露底层实现(列表、栈或树等)的情况下,遍历一个聚合对象中所有的元素。
在以下情况可以考虑使用迭代器模式:
(1) 当聚合对象的数据结构较复杂,且希望对客户端隐藏其复杂性时(出于易用性或安全性考虑),可考虑使用迭代器模式。迭代器封装了与复杂数据结构进行交互的细节,为客户端提供多个访问集合元素的简单方法。
这种方式不仅对客户端来说非常方便,而且能避免客户端在直接与集合交互时执行错误或有害的操作,从而起到保护集合的作用。
(2) 使用该模式可以减少程序中重复的遍历代码。重要迭代算法的代码往往体积非常庞大。当这些代码被放置在程序业务逻辑中时,它会让原始代码的职责模糊不清,降低其可维护性。因此,将遍历代码移到特定的迭代器中可使程序代码更加精炼和简洁。
(3) 如果希望代码能够遍历不同的甚至是无法预知的数据结构,可考虑使用迭代器模式。该模式为集合和迭代器提供了一些通用接口。如果在代码中使用了这些接口,那么将其他实现了这些接口的集合和迭代器传递给它时,它仍将可以正常运行。
迭代器模式包含如下角色:
Iterator,迭代器基类,声明遍历聚合对象所需的操作:获取下一个元素、获取当前位置、下一个元素是否存在等。
ConcreteIterator,迭代器实现类,实现遍历聚合对象的算法。
Aggregate,聚合基类,声明一个获取迭代器的接口。
ConcreteAggregate,具体聚合实现类,实现获取一个迭代器的接口并返回一个具体迭代器实例。
Client,客户端,通过聚合基类和迭代器基类的接口与两者进行交互。
迭代器模式类图表示如下:
接下来将使用代码介绍下迭代器模式的实现。
// 1、聚合接口,声明一个获取迭代器的接口,以及元素添加及删除接口
public interface IAggregate {
IIterator createIterator();
void append(Object element);
void removeLast();
}
//2、具体聚合实现类,获取一个迭代器的接口,并实现元素添加及删除接口,这里使用数组存储元素
public class ConcreteAggregate implements IAggregate {
private String[] elements;
private int cursor = -1;
public ConcreteAggregate(int size) {
elements = new String[size];
}
@Override
public void append(Object element) {
cursor++;
elements[cursor] = (String) element;
}
@Override
public void removeLast() {
elements[cursor] = "";
cursor--;
}
public int getCursor() {
return this.cursor;
}
public String[] getElements() {
return this.elements;
}
@Override
public IIterator createIterator() {
return new ConcreteIterator(this);
}
}
// 3、迭代器接口,声明遍历聚合对象所需的操作:获取下一个元素、获取当前位置、下一个元素是否存在等
public interface IIterator {
Object first();
Object next();
boolean hasNext();
Object currentItem();
}
// 4、迭代器实现类,实现遍历聚合对象的算法,及其他已声明接口
public class ConcreteIterator implements IIterator {
private ConcreteAggregate aggregate;
private int index;
public ConcreteIterator(ConcreteAggregate aggregate) {
this.aggregate = aggregate;
this.index = 0;
}
@Override
public Object first() {
String[] elements = aggregate.getElements();
return elements[0];
}
@Override
public Object next() {
int cursor = aggregate.getCursor();
if (cursor < 0 || index > cursor) {
return null;
}
String[] elements = aggregate.getElements();
return elements[index++];
}
@Override
public boolean hasNext() {
int cursor = aggregate.getCursor();
if (cursor < 0 || index > cursor) {
return false;
}
return true;
}
@Override
public Object currentItem() {
int cursor = aggregate.getCursor();
if (cursor < 0 || index > cursor) {
return null;
}
String[] elements = aggregate.getElements();
return elements[index];
}
}
// 5、客户端
public class IteratorClient {
public void test() {
// (1) 创建迭代对象实例
IAggregate aggregate = new ConcreteAggregate(10);
// (2) 增删元素
aggregate.append("hello");
aggregate.append("world");
aggregate.append("foo");
aggregate.removeLast();
// (3) 获取迭代器实例
IIterator iterator = aggregate.createIterator();
// (4) 执行遍历操作
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
迭代器模式有以下优点:
(1) 符合单一职责原则。通过将体积庞大的遍历算法代码抽取为独立的类,可对客户端代码和集合进行整理。
(2) 符合开闭原则。可实现新型的集合和迭代器并将其传递给现有代码,无需修改现有代码。
(3) 可以并行遍历同一集合,因为每个迭代器对象都包含其自身的遍历状态。
但是该模式也存在以下缺点:
(1) 如果只与简单的集合进行交互,应用该模式可能会矫枉过正。
(2) 对于某些特殊集合,使用迭代器可能比直接遍历的效率低。
(3) 增加了系统的复杂性。因为迭代器模式将存储数据和遍历数据的职责分离,增加了新的聚合类需要对应增加新的迭代器类,增加了系统的复杂性。
备忘录模式是一种行为设计模式,在不破坏封装性的前提下,允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
在以下情况下可以考虑使用备忘录模式:
(1) 当需要创建对象状态快照来恢复其之前的状态时,可以考虑使用备忘录模式。备忘录模式允许复制对象中的全部状态(包括私有成员变量),并将其独立于对象进行保存。
尽管大部分人因为 “撤销” 这个用例才记得该模式,但其实它在处理事务(比如需要在出现错误时回滚一个操作)的过程中也必不可少。
(2) 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时,可以考虑使用备忘录模式。备忘录让对象自行负责创建其状态的快照。任何其他对象都不能读取快照,这有效地保障了数据的安全性。
一个备忘录(memento)是一个对象,它存储另一个对象在某个瞬间的内部状态,而后者称为备忘录的原发器(originator)。当需要设置原发器的检查点时,取消操作机制会向原发器请求一个备忘录。
原发器用描述当前状态的信息初始化备忘录。只有原发器可以向备忘录中存取信息,备忘录对其他的对象"不可见"。
备忘录模式包含如下角色:
Originator,原发器,可以生成自身状态的快照,也可以在需要时通过快照恢复自身状态。
Memento,备忘录,是原发器状态快照的值对象(value object)。通常做法是将备忘录设为不可变的,并通过构造函数一次性传递数据。
Caretaker,负责人,仅知道“何时”和“为何”捕捉原发器的状态,以及何时恢复状态。负责人通过保存备忘录栈来记录原发器的历史状态。 当原发器需要回溯历史状态时,负责人将从栈中获取最顶部的备忘录, 并将其传递给原发器的恢复(restoration)方法。
备忘录模式类图表示如下:
接下来将使用代码介绍下备忘录模式的实现。
// 1、原发器,支持读写自身状态,支持生成自身状态的快照,支持通过快照恢复自身状态
public class Originator {
private String name;
private String describe;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setDescribe(String describe) {
this.describe = describe;
}
public String getDescribe() {
return this.describe;
}
public Memento save() {
return new Memento(this, name, describe);
}
public void restore(Memento memento) {
setName(memento.getName());
setDescribe(memento.getDescribe());
}
}
//2、备忘录,是原发器状态快照的值对象
public class Memento {
private String name;
private String describe;
private Originator originator;
public Memento(Originator originator, String name, String describe) {
this.originator = originator;
this.name = name;
this.describe = describe;
}
public String getName() {
return this.name;
}
public String getDescribe() {
return this.describe;
}
}
// 3、负责人,通过保存备忘录栈来记录原发器的历史状态。当原发器需要回溯历史状态时,负责人将从栈中获取最顶部的备忘录,并将其传递给原发器的恢复(restoration)方法
public class Caretaker {
private Originator originator;
private LinkedList<Memento> history;
public Caretaker(Originator originator) {
this.originator = originator;
history = new LinkedList<>();
}
public void snapshot() {
history.push(originator.save());
}
public void undo() {
Memento lastMemento = history.pop();
originator.restore(lastMemento);
}
}
// 4、客户端
public class MementoClient {
public void test() {
// (1) 创建原生器实例并设置状态
Originator originator = new Originator();
originator.setName("1");
originator.setDescribe("one");
// (2) 创建负责人实例
Caretaker caretaker = new Caretaker(originator);
// (3) 创建快照
caretaker.snapshot();
System.out.println("name is " + originator.getName() + " , " + "describe is " + originator.getDescribe());
originator.setName("2");
originator.setDescribe("two");
caretaker.snapshot();
System.out.println("name is " + originator.getName() + " , " + "describe is " + originator.getDescribe());
// (4) 恢复上一个状态
caretaker.undo();
System.out.println("name is " + originator.getName() + " , " + "describe is " + originator.getDescribe());
caretaker.undo();
System.out.println("name is " + originator.getName() + " , " + "describe is " + originator.getDescribe());
}
}
备忘录模式有以下优点:
(1) 可以在不破坏对象封装情况的前提下创建对象状态快照。
(2) 可以通过让负责人维护原发器状态历史记录来简化原发器代码。
(3) 给用户提供了一种可恢复状态的机制,能够比较方便地回滚到某个历史状态。
但是该模式也存在以下缺点:
(1) 如果客户端过于频繁地创建备忘录,程序将消耗大量内存。
(2) 负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。
命令模式是一种行为设计模式,可将一个请求封装为一个对象,用不同的请求将方法参数化,从而实现延迟请求执行或将其放入队列中或记录请求日志,以及支持可撤销操作。其别名为动作(Action)模式或事务(Transaction)模式。
在以下情况可以考虑使用命令模式:
(1) 如果需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互,可考虑使用该模式。
(2) 如果需要通过操作来参数化对象,可考虑使用该模式。命令模式可将特定的方法调用转化为独立对象。这一改变也带来了许多有趣的应用:开发者可以将命令作为方法的参数进行传递、将命令保存在其他对象中,或者在运行时切换已连接的命令等。
(3) 如果需要将操作放入队列中、操作的执行或者远程执行操作,可考虑使该模式。同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串),从而能方便地写入文件或数据库中。一段时间后,该字符串可被恢复成为最初的命令对象。因此,可以延迟或计划命令的执行。 但其功能远不止如此。使用同样的方式,还可以将命令放入队列、 记录命令或者通过网络发送命令。
(4) 如果需要实现命令的撤销(Undo)操作和恢复(Redo)操作,可考虑使该模式。为了能够回滚操作, 需要实现已执行操作的历史记录功能。 命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。
这种方法有两个缺点。 首先, 程序状态的保存功能并不容易实现, 因为部分状态可能是私有的。 可以使用备忘录模式来在一定程度上解决这个问题。
其次, 备份状态可能会占用大量内存。 因此, 有时需要借助另一种实现方式:命令无需恢复原始状态,而是执行反向操作。反向操作也有代价: 它可能会很难甚至是无法实现。
命令模式包含如下角色:
Command,命令基类,声明一个执行命令的接口。
ConcreteCommand,具体命令类,实现各种类型的请求。具体命令自身并不完成工作,而是会将调用委派给一个业务逻辑对象(Receiver)。但为了简化代码,这些类可以进行合并。
Invoker,调用者,负责对请求进行初始化,其中必须包含一个成员变量来存储对于命令对象的引用。触发命令执行,但不向接收者直接发送请求。 注意,调用者并不负责创建命令对象:它通常会通过构造函数从客户端处获得预先生成的命令。
Receiver,接收者,定义业务逻辑。几乎任何对象都可以作为接收者。绝大部分命令只处理如何将请求传递到接收者的细节,接收者自己会完成实际的工作。简化代码,这些类可以与具体命令类进行合并。
Client,客户端,创建并配置具体命令对象。客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。此后,生成的命令就可以与一个或多个发送者相关联了。
命令模式类图表示如下:
接下来将使用代码介绍下命令模式的实现。
// 1、命令基类,声明一个执行命令的接口
public interface ICommand {
void execute();
}
// 2、具体命令类,实现各种类型的请求
public class ConcreteCommandA implements ICommand {
private Receiver receiver;
public ConcreteCommandA(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
System.out.println("this is a concrete command A instance");
receiver.actionA();
}
}
public class ConcreteCommandB implements ICommand {
private Receiver receiver;
public ConcreteCommandB(Receiver receiver) {
this.receiver = receiver;
}
@Override
public void execute() {
System.out.println("this is a concrete command B instance");
receiver.actionB();
}
}
// 3、接收者,定义业务逻辑。绝大部分命令只处理如何将请求传递到接收者的细节,接收者自己会完成实际的工作。为简化代码,这些类可以与具体命令类进行合并
public class Receiver {
public void actionA() {
System.out.println("action A in a receiver instance");
}
public void actionB() {
System.out.println("action B in a receiver instance");
}
}
// 4、调用者,负责对请求进行初始化和触发命令执行。注意,调用者并不负责创建命令对象:它通常会通过构造函数从客户端处获得预先生成的命令
public class Invoker {
private ICommand command;
public Invoker(ICommand command) {
this.command = command;
}
public void executeCommand() {
this.command.execute();
}
}
// 5、客户端
public class CommandClient {
public void test() {
// (1) 创建接收者实例
Receiver receiver = new Receiver();
// (2) 创建命令实例
ICommand commandA = new ConcreteCommandA(receiver);
// (3) 创建调用者实例
Invoker invokerA = new Invoker(commandA);
// (4) 执行命令
invokerA.executeCommand();
ICommand commandB = new ConcreteCommandB(receiver);
Invoker invokerB = new Invoker(commandB);
invokerB.executeCommand();
}
}
命令模式有以下优点:
(1) 可以实现撤销和恢复功能。
(2) 可以实现操作的延迟执行。
(3) 可以将一组简单命令组合成一个复杂命令。
(4) 符合开闭原则。可以解耦触发和执行操作的类。
(5) 符合单一职责原则。发起操作和执行操作的类进行解耦。
但是该模式也存在以下缺点:
(1) 代码可能会变得更加复杂。使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个命令都需要设计一个具体命令类,因此某些系统可能需要大量具体命令类,这将影响命令模式的使用。
状态模式是一种行为设计模式,允许一个对象在其内部状态改变时改变它的行为,使其看起来修改了自身所属的类。其别名为状态对象(Objects for States)。
在很多情况下,一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,这样的对象叫做有状态的(stateful)对象,这样的对象状态是从事先定义好的一系列值中取出的。
当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。
在UML中可以使用状态图来描述对象状态的变化。
在以下情况可以考虑使用状态模式:
(1) 对象的行为依赖于它的状态(属性)并且可以根据它的状态改变而改变它的相关行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可以考虑使用状态模式。
(2) 代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,使客户类与类库之间的耦合增强。在这些条件语句中包含了对象的行为,而且这些条件对应于对象的各种状态。
(3) 当相似状态和基于条件的状态机转换中存在许多重复代码时,可考虑使用状态模式。状态模式能够生成状态类层次结构,通过将公用代码抽取到抽象基类中来减少重复。
状态模式包含如下角色:
Context,上下文类,保存了对于一个Concrete State对象(具体状态对象)的引用,并会将所有与该状态相关的工作委派给它。上下文通过状态接口与状态对象交互。
State,状态基类,接口会声明特定于状态的方法。
Concrete State,具体状态类,会自行实现特定于状态的方法。当多个状态中包含相似代码,可以提供一个封装有部分通用行为的中间抽象类。
状态对象可存储对于上下文对象的反向引用。状态对象可以通过该引用从上下文处获取所需信息,并且能触发状态转移。但这可能会带来对象的循环引用,在实际使用时,要通过对象传参的方式使用。
状态模式类图表示如下:
状态模式可能看上去与策略模式相似,但有一个关键性的不同——在状态模式中,特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换,而策略则几乎完全不知道其他策略的存在。
接下来将使用代码介绍下状态模式的实现。
// 1、State,状态接口,声明特定于状态的方法
public interface IState {
void handle(StateContext context);
void doSomething();
}
// 2、具体状态类,会自行实现特定于状态的方法,这里特定状态知道其他所有状态的存在,且能触发从一个状态到另一个状态的转换
public class ConcreteStateA implements IState {
private static ConcreteStateA state;
// 这里的单例实现暂不考虑并发场景
public static IState getInstance() {
if (state == null) {
state = new ConcreteStateA();
}
return state;
}
@Override
public void handle(StateContext context) {
doSomething();
context.setCurrentState(ConcreteStateB.getInstance());
}
@Override
public void doSomething() {
System.out.println("do some thing in the concrete A instance");
}
}
public class ConcreteStateB implements IState {
private static ConcreteStateB state;
// 这里的单例实现暂不考虑并发场景
public static IState getInstance() {
if (state == null) {
state = new ConcreteStateB();
}
return state;
}
@Override
public void handle(StateContext context) {
doSomething();
context.setCurrentState(ConcreteStateA.getInstance());
}
@Override
public void doSomething() {
System.out.println("do some thing in the concrete B instance");
}
}
// 3、状态上下文类,保存了对于一个Concrete State对象(具体状态对象)的引用,并会将所有与该状态相关的工作委派给它。
// 上下文通过状态接口与状态对象交互。
public class StateContext {
private IState currentState;
public StateContext(IState defaultState) {
this.currentState = defaultState;
}
public IState getCurrentState() {
return this.currentState;
}
public void setCurrentState(IState newState) {
this.currentState = newState;
}
public void request() {
currentState.handle(this);
}
}
// 4、客户端
public class StateClient {
public void test() {
StateContext stateContext = new StateContext(new ConcreteStateA());
stateContext.request();
stateContext.request();
stateContext.request();
}
}
状态模式有以下优点:
(1) 封装了转换规则。调用方无需关心状态转换的实现。
(2) 符合开闭原则。无需修改已有状态类和上下文就能引入新状态。
(3) 符合单一职责原则。将与特定状态相关的代码放在单独的类中。
(4) 消除了可能存在的条件语句。通过消除臃肿的状态机条件语句简化上下文代码。
(5) 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
但是该模式也存在以下缺点:
(1) 如果状态机只有很少的几个状态, 或者很少发生改变, 那么应用该模式可能会显得小题大作。
(2) 增加系统类和对象的个数。
(3) 实现较为复杂,如果使用不当将导致程序结构和代码的混乱。
(4) 对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
《设计模式:可复用面向对象软件的基础》 Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 著 李英军, 马晓星 等译
https://blog.csdn.net/zhaohongfei_358/article/details/115085887 设计模式面试题(设计模式速成版)
https://zhuanlan.zhihu.com/p/531834565 设计模式面试题
https://blog.csdn.net/czqqqqq/article/details/80451880 单例模式
https://www.nowcoder.com/discuss/353157688295104512 轻松理解工厂模式