跳转至

设计模式

设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。

创建型:

常用的有:单例模式、工厂模式(工厂方法和抽象工厂)、建造者模式。

不常用的有:原型模式。

结构型:

常用的有:代理模式、桥接模式、装饰者模式、适配器模式。

不常用的有:门面模式、组合模式、享元模式。

行为型:

常用的有:观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式。

不常用的有:访问者模式、备忘录模式、命令模式、解释器模式、中介模式。

创建型

创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。

单例模式

一个类只允许创建一个对象(实例)。

为什么需要单例?

  1. 处理资源访问冲突。例如一个日志类Logger,多线程环境下分别创建两个Logger对象同时写日志到log.txt 中,就可能存在日志覆盖问题。解决方案:类级别锁、分布式锁、并发队列、单例模式(所有线程共享Logger对象,共享一个FileWriter对象,FileWriter对象是对象级别线程安全的)。
  2. 表示全局唯一类。例如配置信息类、唯一递增ID号码生成器

要实现一个单例,我们需要关注的点有下面几个:

  • 构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;
  • 考虑对象创建时的线程安全问题;
  • 考虑是否支持延迟加载;
  • 考虑 getInstance() 性能是否高(是否加锁)。

饿汉式

在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载。

public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {
        // 私有构造方法,防止外部实例化
    }

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

懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载。

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // 私有构造方法,防止外部实例化
    }
    // 使用了同步关键字来确保线程安全, 可能会影响性能
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

双检锁

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        // 私有构造方法,防止外部实例化
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

因为指令重排序,可能会导致 Singleton 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻 辑),就被另一个线程使用了。

要解决这个问题,我们需要给 instance 成员变量加上 volatile 关键字,禁止指令重排序。

实际上,只有很低版本的 Java 才会有这个问题。我们现在用的高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。

静态内部类

一种比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。

public class Singleton {
    // 私有构造方法,防止外部实例化
    private Singleton() {
    }

    // 静态内部类,负责实例的初始化
    private static class SingletonHolder {
        // 静态初始化器,线程安全
        private static final Singleton INSTANCE = new Singleton();
    }

    // 公共方法,提供全局访问点
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

SingletonHolder 是一个静态内部类,当外部类 Singleton 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

枚举

通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

1
2
3
public enum Singleton {
    INSTANCE;
}

通过 Singleton.INSTANCE 来获取这个单例实例,然后调用实例的方法。

工厂模式

一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。

当创建逻辑比较复杂,是一个“大工程”的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。何为创建逻辑比较复杂呢?

第一种情况:类似规则配置解析的例子,代码中存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。针对这种情况,我们就考虑使用工厂模式,将这一大坨 if-else 创建对象的代码抽离出来,放到工厂类中。 推荐简单工厂模式

第二种情况:尽管我们不需要根据不同的类型创建不同的对象,但是,单个对象本身的创建过程比较复杂,比如要组合其他类对象,做各种初始化操作。在这种情况下,我们也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。推荐工厂方法模式

简单工厂

public class RuleConfigParserFactory {
    public static IRuleConfigParser createParser(String configFormat) {
        IRuleConfigParser parser = null;
        if ("json".equalsIgnoreCase(configFormat)) {
            parser = new JsonRuleConfigParser();
        } else if ("xml".equalsIgnoreCase(configFormat)) {
            parser = new XmlRuleConfigParser();
        } else if ("yaml".equalsIgnoreCase(configFormat)) {
            parser = new YamlRuleConfigParser();
        } else if ("properties".equalsIgnoreCase(configFormat)) {
            parser = new PropertiesRuleConfigParser();
        }
        return parser;
    }
}

实际上,如果 parser 可以复用,为了节省内存和对象创建的时间,我们可以将 parser 事先创建好缓存起来。当调用 createParser() 函数的时候,我们从缓存中取出 parser 对象直接使用。

public class RuleConfigParserFactory {
    private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();
    static {
        cachedParsers.put("json", new JsonRuleConfigParser());
        cachedParsers.put("xml", new XmlRuleConfigParser());
        cachedParsers.put("yaml", new YamlRuleConfigParser());
        cachedParsers.put("properties", new PropertiesRuleConfigParser());
    }
    public static IRuleConfigParser createParser(String configFormat) {
        if (configFormat == null || configFormat.isEmpty()) {
            return null;//返回null还是IllegalArgumentException全凭你自己说了算
        }
        IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
        return parser;
    }
}

简单工厂负责创建所有解析器,如果要添加新的解析器,通常需要修改工厂类的代码。

工厂方法

工厂方法模式引入了抽象工厂和具体工厂的概念,每个具体工厂只负责创建一个具体产品,添加新的产品只需要添加新的工厂类而无需修改原来的代码,这样就使得产品的生产更加灵活,支持扩展,符合开闭原则。

工厂方法模式分为以下几个角色:

  • 抽象工厂:一个接口,包含一个抽象的工厂方法(用于创建产品对象)。
  • 具体工厂:实现抽象工厂接口,创建具体的产品。
  • 抽象产品:定义产品的接口。
  • 具体产品:实现抽象产品接口,是工厂创建的对象。
// 抽象产品
interface Shape {
    void draw();
}

// 具体产品 - 圆形
class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Circle");
    }
}

// 具体产品 - 正方形
class Square implements Shape {
    @Override
    public void draw() {
        System.out.println("Square");
    }
}

// 抽象工厂
interface ShapeFactory {
    Shape createShape();
}

// 具体工厂 - 创建圆形
class CircleFactory implements ShapeFactory {
    @Override
    public Shape createShape() {
        return new Circle();
    }
}

// 具体工厂 - 创建正方形
class SquareFactory implements ShapeFactory {
    @Override
    public Shape createShape() {
        return new Square();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        ShapeFactory circleFactory = new CircleFactory();
        Shape circle = circleFactory.createShape();
        circle.draw();  // 输出:Circle

        ShapeFactory squareFactory = new SquareFactory();
        Shape square = squareFactory.createShape();
        square.draw();  // 输出:Square
    }
}

抽象工厂

在工厂方法模式中,每个具体工厂只负责创建单一的产品。抽象工厂模式可以确保一系列相关的产品被一起创建,这些产品能够相互配合使用。

比如沙发、茶几、椅子,都具有古典风格的和现代风格的,抽象工厂模式可以将生产现代风格的家具放在一个工厂类中,将生产古典风格的家具放在另一个工厂类中,这样每个工厂类就可以生产一系列的家具。

抽象工厂模式包含多个抽象产品接口,多个具体产品类一个抽象工厂接口和多个具体工厂,每个具体工厂负责创建一组相关的产品

  • 抽象产品接口AbstractProduct: 定义产品的接口,可以定义多个抽象产品接口,比如说沙发、椅子、茶几都是抽象产品。
  • 具体产品类ConcreteProduct: 实现抽象产品接口,产品的具体实现,古典风格和沙发和现代风格的沙发都是具体产品。
  • 抽象工厂接口AbstractFactory: 声明一组用于创建产品的方法,每个方法对应一个产品。
  • 具体工厂类ConcreteFactory: 实现抽象工厂接口,负责创建一组具体产品的对象,在本例中,生产古典风格的工厂和生产现代风格的工厂都是具体实例。
// 1. 定义抽象产品
// 抽象产品A 沙发
interface ProductA {
    void display();
}

// 抽象产品B 椅子
interface ProductB {
    void show();
}

// 2. 实现具体产品类 
// 具体产品A1-现代
class ConcreteProductA1 implements ProductA {
    @Override
    public void display() {
        System.out.println("Concrete Product A1");
    }
}

// 具体产品A2-古典
class ConcreteProductA2 implements ProductA {
    @Override
    public void display() {
        System.out.println("Concrete Product A2");
    }
}

// 具体产品B1-现代
class ConcreteProductB1 implements ProductB {
    @Override
    public void show() {
        System.out.println("Concrete Product B1");
    }
}

// 具体产品B2-古典
class ConcreteProductB2 implements ProductB {
    @Override
    public void show() {
        System.out.println("Concrete Product B2");
    }
}

// 3. 定义抽象工厂接口
interface AbstractFactory {
    ProductA createProductA();
    ProductB createProductB();
}
// 4. 实现具体工厂类
// 具体工厂1,生产产品A1和B1
class ConcreteFactory1 implements AbstractFactory {
    @Override
    public ProductA createProductA() {
        return new ConcreteProductA1();
    }

    @Override
    public ProductB createProductB() {
        return new ConcreteProductB1();
    }
}

// 具体工厂2,生产产品A2和B2
class ConcreteFactory2 implements AbstractFactory {
    @Override
    public ProductA createProductA() {
        return new ConcreteProductA2();
    }

    @Override
    public ProductB createProductB() {
        return new ConcreteProductB2();
    }
}

// 客户端代码
public class AbstractFactoryExample {
    public static void main(String[] args) {
        // 使用工厂1创建产品A1和产品B1
        AbstractFactory factory1 = new ConcreteFactory1();
        ProductA productA1 = factory1.createProductA();
        ProductB productB1 = factory1.createProductB();
        productA1.display();
        productB1.show();

        // 使用工厂2创建产品A2和产品B2
        AbstractFactory factory2 = new ConcreteFactory2();
        ProductA productA2 = factory2.createProductA();
        ProductB productB2 = factory2.createProductB();
        productA2.display();
        productB2.show();
    }
}

简单工厂、工厂方法、抽象工厂的区别:

  • 简单工厂模式:一个工厂方法创建所有具体产品
  • 工厂方法模式:一个工厂方法创建一个具体产品
  • 抽象工厂模式:一个工厂方法可以创建一类具体产品

建造者模式/生成器模式

主要思想是将复杂对象的创建过程分为几个步骤,并为每个步骤定义一个抽象的接口。具体的过程由实现了这些接口的具体建造者类来完成。同时有一个指导者类负责协调建造者按照一定的顺序或逻辑来执行构建步骤,最终完成工作。

基本结构:

  • 产品Product:被构建的复杂对象, 包含多个组成部分。
  • 抽象建造者Builder: 定义构建产品各个部分的抽象接口和一个返回复杂产品的方法getResult
  • 具体建造者Concrete Builder:实现抽象建造者接口,构建产品的各个组成部分,并提供一个方法返回最终的产品。
  • 指导者Director:调用具体建造者的方法,按照一定的顺序或逻辑来构建产品。
// 产品类
class Car {
    private String engine;
    private String wheels;
    private String doors;

    public void setEngine(String engine) {
        this.engine = engine;
    }

    public void setWheels(String wheels) {
        this.wheels = wheels;
    }

    public void setDoors(String doors) {
        this.doors = doors;
    }

    @Override
    public String toString() {
        return "Car with engine: " + engine + ", wheels: " + wheels + ", doors: " + doors;
    }
}

// 抽象建造者
abstract class CarBuilder {
    protected Car car;

    public void createNewCar() {
        car = new Car();
    }

    public Car getCar() {
        return car;
    }

    public abstract void buildEngine();
    public abstract void buildWheels();
    public abstract void buildDoors();
}

// 具体建造者 - 豪华车
class LuxuryCarBuilder extends CarBuilder {
    @Override
    public void buildEngine() {
        car.setEngine("V8 Engine");
    }

    @Override
    public void buildWheels() {
        car.setWheels("18-inch Alloy Wheels");
    }

    @Override
    public void buildDoors() {
        car.setDoors("4 Doors");
    }
}

// 具体建造者 - 经济型车
class EconomyCarBuilder extends CarBuilder {
    @Override
    public void buildEngine() {
        car.setEngine("V4 Engine");
    }

    @Override
    public void buildWheels() {
        car.setWheels("15-inch Steel Wheels");
    }

    @Override
    public void buildDoors() {
        car.setDoors("4 Doors");
    }
}

// 指挥者类
class Director {
    private CarBuilder builder;

    public void setCarBuilder(CarBuilder builder) {
        this.builder = builder;
    }

    public Car getCar() {
        return builder.getCar();
    }

    public void constructCar() {
        builder.createNewCar();
        builder.buildEngine();
        builder.buildWheels();
        builder.buildDoors();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        Director director = new Director();

        // 构建豪华车
        CarBuilder luxuryBuilder = new LuxuryCarBuilder();
        director.setCarBuilder(luxuryBuilder);
        director.constructCar();
        Car luxuryCar = director.getCar();
        System.out.println(luxuryCar);  // 输出: Car with engine: V8 Engine, wheels: 18-inch Alloy Wheels, doors: 4 Doors

        // 构建经济型车
        CarBuilder economyBuilder = new EconomyCarBuilder();
        director.setCarBuilder(economyBuilder);
        director.constructCar();
        Car economyCar = director.getCar();
        System.out.println(economyCar);  // 输出: Car with engine: V4 Engine, wheels: 15-inch Steel Wheels, doors: 4 Doors
    }
}

原型模式

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同), 在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式,简称原型模式。

原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝只会复制对象中基本数据类型数据和引 用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到 的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空 间。

基本结构:

  • 原型接口: 声明一个克隆自身的方法clone
  • 具体原型类: 实现clone方法,复制当前对象并返回一个新对象。
// 原型接口
interface Prototype extends Cloneable {
    Prototype clone();
}

// 具体原型类 - 文档
class Document implements Prototype {
    private String title;
    private String content;

    public Document(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public String getTitle() {
        return title;
    }

    public String getContent() {
        return content;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public Prototype clone() {
        try {
            return (Prototype) super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }

    @Override
    public String toString() {
        return "Document{" +
               "title='" + title + '\'' +
               ", content='" + content + '\'' +
               '}';
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 创建原型对象
        Document originalDoc = new Document("Original Title", "Original Content");
        System.out.println("Original Document: " + originalDoc);

        // 通过克隆创建副本
        Document clonedDoc = (Document) originalDoc.clone();
        System.out.println("Cloned Document: " + clonedDoc);

        // 修改克隆对象
        clonedDoc.setTitle("Cloned Title");
        clonedDoc.setContent("Cloned Content");
        System.out.println("Modified Cloned Document: " + clonedDoc);

        // 原始对象保持不变
        System.out.println("Original Document After Cloning: " + originalDoc);
    }
}

结构型

代理模式

代理模式(Proxy Design Pattern)在不改变原始类 (或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

代理模式常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事 务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类统一处理,让程序员只 需要关注业务方面的开发。除此之外,代理模式还可以用在 RPC、缓存等应用场景中。

基本结构:

  • Subject(抽象主题): 抽象类,通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • RealSubject(真实主题):定义了Proxy所代表的真实对象,是客户端最终要访问的对象。
  • Proxy(代理):包含一个引用,该引用可以是RealSubject的实例,控制对RealSubject的访问,并可能负责创建和删除RealSubject的实例。
// 1. 定义抽象主题
interface Subject {
    void request();
}

// 2. 定义真实主题
class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("RealSubject handles the request.");
    }
}

// 3. 定义代理
class Proxy implements Subject {
    // 包含一个引用
    private RealSubject realSubject;

    @Override
    public void request() {
        // 在访问真实主题之前可以添加额外的逻辑
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        // 调用真实主题的方法
        realSubject.request();

        // 在访问真实主题之后可以添加额外的逻辑
    }
}

// 4. 客户端使用代理
public class Main {
    public static void main(String[] args) {
        // 使用代理
        Subject proxy = new Proxy();
        proxy.request();
    }
}

一方面,我们需要在代理类中,将原始类中的所有的方法,都重新代理一遍,并且为每个方法都附加相似的代码逻辑。另一方面,如果要添加的附加功能的类有不止一个,我们需要针对每个类都创建一个代理类。这会导致项目中类的个数成倍增加,增加了代码维护成本。并且,每个代理类中的代码都有点像模板式的“重复”代码,也增加了不必要的开发成本。

我们可以使用动态代理来解决这个问题。所谓动态代理(Dynamic Proxy),就是我们不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。

JDK动态代理

JDK 动态代理依赖于 Java 的反射机制,要求被代理的对象必须实现至少一个接口。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 抽象主题接口
interface Subject {
    void request();
}

// 真实主题类
class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("RealSubject: Handling request.");
    }
}

// 代理类的调用处理器
class DynamicProxyHandler implements InvocationHandler {
    private final Object realSubject;

    public DynamicProxyHandler(Object realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Proxy: Before method " + method.getName());
        Object result = method.invoke(realSubject, args);
        System.out.println("Proxy: After method " + method.getName());
        return result;
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 创建真实主题
        RealSubject realSubject = new RealSubject();

        // 创建代理
        Subject proxy = (Subject) Proxy.newProxyInstance(
                realSubject.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(),
                new DynamicProxyHandler(realSubject)
        );

        // 调用代理方法
        proxy.request(); // 输出: Proxy: Before method request
                         //       RealSubject: Handling request.
                         //       Proxy: After method request
    }
}

CGLIB 动态代理:

CGLIB(Code Generation Library) 是一种基于继承的动态代理方式,能够为没有实现接口的类生成代理。CGLIB 通过字节码生成技术生成子类来覆盖非 final 方法,进而实现动态代理。

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

// 真实主题类,不实现接口
class RealSubject {
    public void request() {
        System.out.println("RealSubject: Handling request.");
    }
}

// 代理类的调用处理器
class CglibProxyHandler implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("CGLIB Proxy: Before method " + method.getName());
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("CGLIB Proxy: After method " + method.getName());
        return result;
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 创建代理对象
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(RealSubject.class);
        enhancer.setCallback(new CglibProxyHandler());

        RealSubject proxy = (RealSubject) enhancer.create();
        proxy.request(); // 输出: CGLIB Proxy: Before method request
                         //       RealSubject: Handling request.
                         //       CGLIB Proxy: After method request
    }
}

桥接模式

将抽象和实现解耦,让它们可以独立变化。

一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。

基本结构:

  • 抽象Abstraction:一般是抽象类,定义抽象部分的接口,维护一个对【实现】的引用。
  • 精确抽象RefinedAbstaction:对抽象接口进行扩展,通常对抽象化的不同维度进行变化或定制。
  • 实现Implementor: 定义实现部分的接口,提供具体的实现。这个接口通常是抽象化接口的实现。
  • 具体实现ConcreteImplementor:实现实现化接口的具体类。这些类负责实现实现化接口定义的具体操作。

假设我们要设计一个图形绘制系统,该系统可以绘制不同类型的图形(如圆形、正方形),并且可以使用不同的颜色(如红色、绿色)来绘制。我们可以使用桥接模式来将图形和颜色的抽象进行分离,使它们能够独立变化。

// 实现部分接口
interface Color {
    void applyColor();
}

// 具体实现部分 - 红色
class RedColor implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying red color");
    }
}

// 具体实现部分 - 绿色
class GreenColor implements Color {
    @Override
    public void applyColor() {
        System.out.println("Applying green color");
    }
}

// 抽象部分
abstract class Shape {
    protected Color color;

    protected Shape(Color color) {
        this.color = color;
    }

    abstract void draw();
}

// 精确抽象部分 - 圆形
class Circle extends Shape {
    public Circle(Color color) {
        super(color);
    }

    @Override
    void draw() {
        System.out.print("Drawing Circle with ");
        color.applyColor();
    }
}

// 精确抽象部分 - 正方形
class Square extends Shape {
    public Square(Color color) {
        super(color);
    }

    @Override
    void draw() {
        System.out.print("Drawing Square with ");
        color.applyColor();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        Shape redCircle = new Circle(new RedColor());
        redCircle.draw();  // 输出: Drawing Circle with Applying red color

        Shape greenSquare = new Square(new GreenColor());
        greenSquare.draw();  // 输出: Drawing Square with Applying green color
    }
}

装饰器模式

通常情况下,扩展类的功能可以通过继承实现,但是扩展越多,子类越多,装饰模式可以在不定义子类的情况下动态的给对象添加一些额外的功能。具体的做法是将原始对象放入装饰类,从而为原始对象动态添加新的行为,而无需修改其代码。

基本结构:

  • 组件Component:通常是抽象类或者接口,是具体组件和装饰者的父类,定义了具体组件需要实现的方法。
  • 具体组件ConcreteComponent: 实现了Component接口的具体类,是被装饰的对象
  • 装饰类Decorator: 一个抽象类,给具体组件添加功能,但是具体的功能由其子类具体装饰者完成,持有一个指向Component对象的引用。
  • 具体装饰类ConcreteDecorator: 扩展Decorator类,负责向Component对象添加新的行为,加牛奶的咖啡是一个具体装饰类,加糖的咖啡也是一个具体装饰类。

假设我们有一个基本的饮料类 Beverage,并且我们希望能够动态地向饮料添加不同的配料(如牛奶、糖等),而不改变原有的 Beverage 类结构。我们可以使用装饰器模式来实现这个功能。

// 抽象组件 - 饮料
interface Beverage {
    String getDescription();
    double cost();
}

// 具体组件 - 咖啡
class Coffee implements Beverage {
    @Override
    public String getDescription() {
        return "Coffee";
    }

    @Override
    public double cost() {
        return 5.0;
    }
}

// 抽象装饰器 - 饮料装饰器
abstract class BeverageDecorator implements Beverage {
    protected Beverage beverage;

    public BeverageDecorator(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription();
    }

    @Override
    public double cost() {
        return beverage.cost();
    }
}

// 具体装饰器 - 牛奶
class MilkDecorator extends BeverageDecorator {
    public MilkDecorator(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Milk";
    }

    @Override
    public double cost() {
        return beverage.cost() + 1.5;
    }
}

// 具体装饰器 - 糖
class SugarDecorator extends BeverageDecorator {
    public SugarDecorator(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Sugar";
    }

    @Override
    public double cost() {
        return beverage.cost() + 0.5;
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        Beverage beverage = new Coffee();  // 一杯咖啡
        System.out.println(beverage.getDescription() + " $" + beverage.cost());

        beverage = new MilkDecorator(beverage);  // 添加牛奶
        System.out.println(beverage.getDescription() + " $" + beverage.cost());

        beverage = new SugarDecorator(beverage);  // 添加糖
        System.out.println(beverage.getDescription() + " $" + beverage.cost());
    }
}

在Java的I/O库中,装饰者模式被广泛用于增强I/O流的功能。例如,BufferedInputStreamBufferedOutputStream这两个类提供了缓冲区的支持,通过在底层的输入流和输出流上添加缓冲区,提高了读写的效率,它们都是InputStreamOutputStream的装饰器。

适配器模式

它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

基本结构:

  • 目标接口Target: 客户端希望使用的接口
  • 适配器类Adapter: 实现客户端使用的目标接口,持有一个需要适配的类实例。
  • 被适配者Adaptee: 需要被适配的类
// 目标接口
interface Target {
    void request();
}

// 被适配者类
class Adaptee {
    void specificRequest() {
        System.out.println("Specific request");
    }
}

// 适配器类
class Adapter implements Target {
    // 持有一个被适配者实例
    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void request() {
        // 调用被适配者类的方法
        adaptee.specificRequest();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        Target target = new Adapter(new Adaptee());
        target.request();
    }
}

应用场景:

  • 封装有缺陷的接口设计:假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计。
  • 统一多个类的接口设计:某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统 一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。
  • 替换依赖的外部系统:把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。
  • 兼容老版本接口
  • 适配不同格式数据

代理、桥接、装饰器、适配器 4 种设计模式的区别

代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

门面模式/外观模式

门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。

假设有一个系统 A,提供了 a、b、c、d 四个接口。系统 B 完成某个业务功能,需要调用 A 系统的 a、b、d 接口。利用门面模式,我们提供一个包裹 a、b、d 接口调用的门面接口 x,给系统 B 直接使用。

// 子系统A
class SubsystemA {
    public void operationA() {
        System.out.println("SubsystemA operation");
    }
}

// 子系统B
class SubsystemB {
    public void operationB() {
        System.out.println("SubsystemB operation");
    }
}

// 子系统C
class SubsystemC {
    public void operationC() {
        System.out.println("SubsystemC operation");
    }
}

// 外观类
class Facade {
    private SubsystemA subsystemA;
    private SubsystemB subsystemB;
    private SubsystemC subsystemC;

    public Facade() {
        this.subsystemA = new SubsystemA();
        this.subsystemB = new SubsystemB();
        this.subsystemC = new SubsystemC();
    }

    // 外观方法,封装了对子系统的操作
    public void facadeOperation() {
        subsystemA.operationA();
        subsystemB.operationB();
        subsystemC.operationC();
    }
}

// 客户端
public class Main {
    public static void main(String[] args) {
        // 创建外观对象
        Facade facade = new Facade();

        // 客户端通过外观类调用子系统的操作
        facade.facadeOperation();
    }
}

应用场景:

  • 解决易用性问题:用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。
  • 解决性能问题:我们通过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高 App 客户端的响应速度。
  • 解决分布式事务问题:设计一个包裹两个操作的新接口,让新接口在一个事务中执行。

组合模式

将对象组合成树状结构来表示“部分-整体”的层次关系。组合模式使得客户端可以统一处理单个对象和对象的组合,而无需区分它们的具体类型。

基本结构:

  • Component组件: 定义了组合对象和叶子对象的共同接口,包括对子对象的管理方法,如 addremove 等,以及通用的操作方法。
  • Leaf叶子:实现了 Component 接口,表示树的叶子节点,叶子节点没有子节点。
  • Composite组合: 实现了 Component 接口,表示树的非叶子节点,可以包含子节点(包括其他组合节点和叶子节点)。

设计一个类来表示文件系统中的目录,能方便地实现下面这些功能:动态地添加、删除某个目录下的子目录或文件; 统计指定目录下的文件个数; 统计指定目录下的文件总大小。

// 抽象文件系统组件
public abstract class FileSystemComponent {
    protected String path;
    public FileSystemComponent(String path) {
        this.path = path;
    }
    public abstract int countNumOfFiles();
    public abstract long countSizeOfFiles();
    public String getPath() {
        return path;
    }
}

// 文件类,叶子节点
public class File extends FileSystemComponent {
    public File(String path) {
        super(path)
    }
    @Override
    public int countNumOfFiles() {
        return 1;
    }
    @Override
    public long countSizeOfFiles() {
        java.io.File file = new java.io.File(path);
        if (!file.exists()) return 0;
        return file.length();
    }
}

// 目录类,组合节点
public class Directory extends FileSystemComponent {
    private List<FileSystemComponent> subNodes = new ArrayList<>();
    public Directory(String path) {
        super(path);
    }
    @Override
    public int countNumOfFiles() {
        int numOfFiles = 0;
        for (FileSystemComponent fileOrDir : subNodes) {
            numOfFiles += fileOrDir.countNumOfFiles();
        }
        return numOfFiles;
    }
    @Override
    public long countSizeOfFiles() {
        long sizeofFiles = 0;
        for (FileSystemComponent fileOrDir : subNodes) {
            sizeofFiles += fileOrDir.countSizeOfFiles();
        }
        return sizeofFiles;
    }
    public void addSubNode(FileSystemNode fileOrDir) {
        subNodes.add(fileOrDir);
    }
    public void removeSubNode(FileSystemNode fileOrDir) {
        int size = subNodes.size();
        int i = 0;
        for (; i < size; ++i) {
            if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
                break;
            }
        }
        if (i < size) {
            subNodes.remove(i);
        }
    }
}

享元模式

所谓“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。

享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或者 List 来缓存已经创建好的享元对象,以达到复用的目的。

享元模式 VS 单例、缓存、对象池:

享元模式跟单例的区别:在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每 个对象被多处代码引用共享。

享元模式跟缓存的区别:缓存主要是为了提高访问效率,而非复用。

享元模式跟对象池的区别:池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。

应用:

在 Java Integer 的实现中,-128 到 127 之间的整型对象会被事先创建好,缓存在 IntegerCache 类中。当我们使用自动装箱或者 valueOf() 来创建这个数值区间的整型对象时,会复用 IntegerCache 类事先创建好的对象。

在 Java String 类的实现中,JVM 开辟一块存储区专门存储字符串常量,这块存储区叫作字符串常量池,类似于 Integer 中的 IntegerCache。不过,跟 IntegerCache 不同的是,它并非事先创建好需要共享的对象,而是在程序的运行期间,根据需要来创建和缓存字符串常量。

行为型

观察者模式

在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。

基本结构:

  • 被观察者(Subject):也称为目标或者主题,维护一组观察者对象,提供注册和删除观察者的接口,以及通知观察者的方法。

  • 观察者(Observer):定义一个更新接口,用于接收被观察者状态的变化。

  • 具体被观察者(ConcreteSubject):实现被观察者接口,维护其状态的变化,并在状态变化时通知观察者。

  • 具体观察者(ConcreteObserver):实现观察者接口,定义具体的更新方法,以便在被观察者状态变化时更新自身状态或执行相应操作。

// 主题接口 (主题)
interface Subject {
    // 注册观察者
    void registerObserver(Observer observer);
    // 移除观察者
    void removeObserver(Observer observer);
    // 通知观察者
    void notifyObservers();
}
// 观察者接口 (观察者)
interface Observer {
    // 更新方法
    void update(String message);
}

// 具体主题实现
class ConcreteSubject implements Subject {
    // 观察者列表
    private List<Observer> observers = new ArrayList<>();
    // 状态
    private String state;

    // 注册观察者
    @Override
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }
    // 移除观察者
    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }
    // 通知观察者
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            // 观察者根据传递的信息进行处理
            observer.update(state);
        }
    }
    // 更新状态
    public void setState(String state) {
        this.state = state;
        notifyObservers();
    }
}

// 具体观察者实现
class ConcreteObserver implements Observer {
    // 更新方法
    @Override
    public void update(String message) {
    }
}

public class ObserverPatternDemo {
    public static void main(String[] args) {
        // 创建被观察者
        ConcreteSubject subject = new ConcreteSubject();

        // 创建观察者
        Observer observer1 = new ConcreteObserver("观察者1");
        Observer observer2 = new ConcreteObserver("观察者2");
        Observer observer3 = new ConcreteObserver("观察者3");

        // 注册观察者
        subject.registerObserver(observer1);
        subject.registerObserver(observer2);
        subject.registerObserver(observer3);

        // 发送通知
        subject.setMessage("第一个通知");

        // 移除一个观察者
        subject.removeObserver(observer2);

        // 发送另一个通知
        subject.setMessage("第二个通知");
    }
}

观察者模式有几种不同的实现方式,包括:同步阻塞、异步非阻塞、进程内、进程间的实现方式。 同步阻塞是最经典的实现方式,主要是为了代码解耦;异步非阻塞除了能实现代码解耦之 外,还能提高代码的执行效率;进程间的观察者模式解耦更加彻底,一般是基于消息队列来实现,用来实现不同进程间的被观察者和观察者之间的交互。

模板模式

模板模式主要是用来解决复用和扩展两个问题。

模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

基本结构:

  • 模板类AbstractClass:由一个模板方法和若干个基本方法构成,模板方法定义了逻辑的骨架,按照顺序调用包含的基本方法,基本方法通常是一些抽象方法,这些方法由子类去实现。基本方法还包含一些具体方法,它们是算法的一部分但已经有默认实现,在具体子类中可以继承或者重写。
  • 具体类ConcreteClass:继承自模板类,实现了在模板类中定义的抽象方法,以完成算法中特定步骤的具体实现。
// 模板类
abstract class AbstractClass {
    // 模板方法,定义了算法的骨架, final避免被重写
    public final void templateMethod() {
        step1();
        step2();
        step3();
    }

    // 抽象方法,强迫子类重写
    protected abstract void step1();
    protected abstract void step2();
    protected abstract void step3();
}

// 具体类
class ConcreteClass extends AbstractClass {
    @Override
    protected void step1() {
        System.out.println("Step 1 ");
    }

    @Override
    protected void step2() {
        System.out.println("Step 2 ");
    }

    @Override
    protected void step3() {
        System.out.println("Step 3");
    }
}

public class Main {
    public static void main(String[] args) {
        AbstractClass concreteTemplate = new ConcreteClass();
        // 触发整个算法的执行
        concreteTemplate.templateMethod();
    }
}

策略模式

定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。

工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式跟两者类似,也能起到解耦的作用,不过,它解耦的是策略的定义、创建、使用这三部分。

基本结构:

  • 策略类Strategy: 定义所有支持的算法的公共接口。
  • 具体策略类ConcreteStrategy: 实现了策略接口,提供具体的算法实现。
  • 上下文类Context: 包含一个策略实例,并在需要时调用策略对象的方法。

策略的定义:

public interface Strategy {
    void algorithmInterface();
}

class ConcreteStrategyA implements Strategy {
    @Override
    public void algorithmInterface() {
        // 具体的策略1执行逻辑
    }
}

class ConcreteStrategyB implements Strategy {
    @Override
    public void algorithmInterface() {
        // 具体的策略2执行逻辑
    }
}

策略的创建:

public class StrategyFactory {
    private static final Map<String, Strategy> strategies = new HashMap<>();
    static {
        strategies.put("A", new ConcreteStrategyA());
        strategies.put("B", new ConcreteStrategyB());
    }
    public static Strategy getStrategy(String type) {
        if (type == null || type.isEmpty()) {
            throw new IllegalArgumentException("type should not be empty.");
        }
        return strategies.get(type);
    }
}

一般来讲,如果策略类是无状态的,不包含成员变量,只是纯粹的算法实现,这样的策略对象是可以被共享使用的,不需要在每次调用 getStrategy() 的时候,都创建一个新的策略对象。针对这种情况,我们可以使用上面这种工厂类的实现方式,事先创建好每个策略对象, 缓存到工厂类中,用的时候直接返回。

相反,如果策略类是有状态的,根据业务场景的需要,我们希望每次从工厂方法中,获得的都是新创建的策略对象,而不是缓存好可共享的策略对象,那我们就需要按照如下方式来实现策略工厂类。

public class StrategyFactory {
    public static Strategy getStrategy(String type) {
        if (type == null || type.isEmpty()) {
            throw new IllegalArgumentException("type should not be empty.");
        }
        if (type.equals("A")) {
            return new ConcreteStrategyA();
        } else if (type.equals("B")) {
            return new ConcreteStrategyB();
        }
        return null;
    }
}

策略的使用:

我们事先并不知道会使用哪个策略,而是在程序运行期间, 根据配置、用户输入、计算结果等这些不确定因素,动态决定使用哪种策略。

责任链模式

将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它 为止。

在职责链模式中,多个处理器(也就是“接收对象”)依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。

public interface IHandler {
    boolean handle();
}
public class HandlerA implements IHandler {
    @Override
    public boolean handle() {
        boolean handled = false;
        //...
        return handled;
    }
}
public class HandlerB implements IHandler {
    @Override
    public boolean handle() {
        boolean handled = false;
        //...
        return handled;
    }
}
public class HandlerChain {
    private List<IHandler> handlers = new ArrayList<>();
    public void addHandler(IHandler handler) {
        this.handlers.add(handler);
    }
    public void handle() {
        for (IHandler handler : handlers) {
            boolean handled = handler.handle();
            if (handled) {
                break;
            }
        }
    }
}
// 使用举例
public class Application {
    public static void main(String[] args) {
        HandlerChain chain = new HandlerChain();
        chain.addHandler(new HandlerA());
        chain.addHandler(new HandlerB());
        chain.handle();
    }
}

在 GoF 给出的定义中,如果处理器链上的某个处理器能够处理这个请求,那就不会继续往下传递请求。实际上,职责链模式还有一种变体,那就是请求会被所有的处理器都处理一 遍,不存在中途终止的情况。

状态模式

适用于一个对象在在不同的状态下有不同的行为时,比如说电灯的开、关状态,状态不同时,对应的行为也不同,在没有状态模式的情况下,为了添加新的状态或修改现有的状态,往往需要修改已有的代码,这违背了开闭原则,而且如果对象的状态切换逻辑和各个状态的行为都在同一个类中实现,就可能导致该类的职责过重,不符合单一职责原则。

而状态模式将每个状态的行为封装在一个具体状态类中,使得每个状态类相对独立,并将对象在不同状态下的行为进行委托,从而使得对象的状态可以在运行时动态改变,每个状态的实现也不会影响其他状态。

基本结构:

  • State(状态): 定义一个接口,用于封装与Context的一个特定状态相关的行为。
  • ConcreteState(具体状态): 负责处理Context在状态改变时的行为, 每一个具体状态子类实现一个与Context的一个状态相关的行为。
  • Context(上下文): 维护一个具体状态子类的实例,这个实例定义当前的状态。
// State interface 定义状态行为
interface State {
    void pressSwitch(LightContext context);
}

// ConcreteState On 具体状态:电灯开
class OnState implements State {
    @Override
    public void pressSwitch(LightContext context) {
        System.out.println("Switching light off.");
        context.setState(new OffState());
    }
}

// ConcreteState Off 具体状态:电灯关
class OffState implements State {
    @Override
    public void pressSwitch(LightContext context) {
        System.out.println("Switching light on.");
        context.setState(new OnState());
    }
}

// Context 电灯上下文
class LightContext {
    private State currentState;

    public LightContext() {
        // 初始状态为关
        currentState = new OffState();
    }

    public void setState(State state) {
        currentState = state;
    }

    public void pressSwitch() {
        currentState.pressSwitch(this);
    }
}

// 测试代码
public class StatePatternExample {
    public static void main(String[] args) {
        LightContext light = new LightContext();

        // 按下开关,打开灯
        light.pressSwitch();  // Output: Switching light on.

        // 再次按下开关,关闭灯
        light.pressSwitch();  // Output: Switching light off.

        // 再次按下开关,打开灯
        light.pressSwitch();  // Output: Switching light on.
    }
}

迭代器模式

迭代器模式(Iterator Design Pattern),也叫作游标模式(Cursor Design Pattern)。 它用来遍历集合对象。这里说的“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。

遍历集合一般有三种方式:for 循环、foreach 循环、迭代器遍历。后两种本质上属于一种,都可以看作迭代器遍历。相对于 for 循环遍历,利用迭代器来遍历有下面三个优势:

  1. 迭代器模式封装集合内部的复杂数据结构,开发者不需要了解如何遍历,直接使用容器提供的迭代器即可;
  2. 迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一;
  3. 迭代器模式让添加新的遍历算法更加容易,更符合开闭原则。除此之外,因为迭代器都实现自相同的接口,在开发中,基于接口而非实现编程,替换迭代器也变得更加容易。

基本结构:

  • 迭代器接口:定义访问和遍历元素的接口, 通常会包括hasNext()方法用于检查是否还有下一个元素,以及next()方法用于获取下一个元素。有的还会实现获取第一个元素以及获取当前元素的方法currentItem()
  • 迭代器实现类:实现迭代器接口,实现遍历逻辑对聚合对象进行遍历。
  • 容器接口:定义了创建迭代器的接口,包括一个createIterator方法用于创建一个迭代器对象。
  • 容器实现类:实现在抽象聚合类中声明的createIterator()方法,返回一个与具体聚合对应的具体迭代器
public interface Iterator<E> {
    boolean hasNext();
    void next();
    E currentItem();
}
public class ArrayIterator<E> implements Iterator<E> {
    private int cursor;
    private ArrayList<E> arrayList;
    public ArrayIterator(ArrayList<E> arrayList) {
        this.cursor = 0;
        this.arrayList = arrayList;
    }
    @Override
    public boolean hasNext() {
        return cursor != arrayList.size(); 
    }
    @Override
    public void next() {
        cursor++;
    }
    @Override
    public E currentItem() {
        if (cursor >= arrayList.size()) {
            throw new NoSuchElementException();
        }
        return arrayList.get(cursor);
    }
}
public interface List<E> {
    Iterator iterator();
    //...省略其他接口函数...
}
public class ArrayList<E> implements List<E> {
    //...
    public Iterator iterator() {
        return new ArrayIterator(this);
    }
    //...省略其他代码
}
public class Demo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("xzg");
        names.add("wang");
        names.add("zheng");
        Iterator<String> iterator = names.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.currentItem());
            iterator.next();
        }
    }
}

在 Java 中,如果在使用迭代器的同时删除容器中的元素,会导致迭代器报错,这是为什么呢?如何来解决这个问题呢?

在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。有两种比较干脆利索的解决方案,来避免出现这种不可预期的运行结果。一种是遍历的时候不允许增删元素,另一种是增删元素之后让遍历报错。第一种解决方案比较难实现,因为很难确定迭代器使用结束的时间点。第二种解决方案更加合理。

Java 语言就是采用的第二种解决方案。增删元素之后让遍历操作直接抛出 ConcurrentModificationException 运行时异常。当创建一个迭代器时,迭代器会记录下集合的 modCount 值(表示集合修改的次数)。在遍历过程中,迭代器会不断检查集合的 modCount 是否发生了变化。如果在迭代器运行期间,集合的 modCount 发生了变化(例如直接通过集合对象删除元素),迭代器就会认为集合的结构发生了变化,于是抛出 ConcurrentModificationException

像 Java 语言,迭代器类中除了前面提到的几个最基本的方法之外,还定义了一个 remove() 方法,能够在遍历集合的同时,安全地删除集合中的元素。Java 迭代器中提供的 remove() 方法作用有限。它只能删除游标指向的前一个元素,而且一个 next() 函数之后,只能跟着最多一个 remove() 操作,多次调用 remove() 操作会报错。

访问者模式

应用它会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议你不要使用访问者模式。

允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。

不同场景下,我们需要对一组对象进行一系列不相关的业务操作 (抽取文本、压缩等),但为了避免不断添加功能导致类不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类中。

public abstract class ResourceFile {
    protected String filePath;
    public ResourceFile(String filePath) {
        this.filePath = filePath;
    }
    abstract public void accept(Visitor vistor);
}
public class PdfFile extends ResourceFile {
    public PdfFile(String filePath) {
        super(filePath);
    }
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    //...
}
//...PPTFile、WordFile跟PdfFile类似,这里就省略了...

public interface Visitor {
    void visit(PdfFile pdfFile);
    void visit(PPTFile pdfFile);
    void visit(WordFile pdfFile);
}
public class Extractor implements Visitor {
    @Override
    public void visit(PPTFile pptFile) {
        //...
        System.out.println("Extract PPT.");
    }
    @Override
    public void visit(PdfFile pdfFile) {
        //...
        System.out.println("Extract PDF.");
    }
    @Override
    public void visit(WordFile wordFile) {
        //...
        System.out.println("Extract WORD.");
    }
}
public class Compressor implements Visitor {
    @Override
    public void visit(PPTFile pptFile) {
        //...
        System.out.println("Compress PPT.");
    }
    @Override
    public void visit(PdfFile pdfFile) {
        //...
        System.out.println("Compress PDF.");
    }
    @Override
    public void visit(WordFile wordFile) {
        //...
        System.out.println("Compress WORD.");
    }
}
public class ToolApplication {
    public static void main(String[] args) {
        Extractor extractor = new Extractor();
        List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
        for (ResourceFile resourceFile : resourceFiles) {
            resourceFile.accept(extractor);
        }
        Compressor compressor = new Compressor();
        for(ResourceFile resourceFile : resourceFiles) {
            resourceFile.accept(compressor);
        }
    }
    private static List<ResourceFile> listAllResourceFiles(String resourceDirecto
        List<ResourceFile> resourceFiles = new ArrayList<>();
        //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
        resourceFiles.add(new PdfFile("a.pdf"));
        resourceFiles.add(new WordFile("b.word"));
        resourceFiles.add(new PPTFile("c.ppt"));
        return resourceFiles;
    }
}

实际上,我们还有其他的实现方法,比如,我们还可以利用工厂 模式来实现,定义一个 Extractor 接口。PdfExtractor、 PPTExtractor、WordExtractor 类实现 Extractor 接口,分别实现 Pdf、PPT、Word 格式文件的文本内容抽取。ExtractorFactory 工厂类根据 不同的文件类型,返回不同的 Extractor。

支持双分派的语言不需要访问者模式。Single Dispatch 之所以称为“Single”,是因为执行哪个对象的哪个方法,只跟“对象”的运行时类型有关。Double Dispatch 之所以称为“Double”,是因为执行哪个对象的哪个方法,跟“对象”和“方法参数”两者的运行时类型有关。当前主流的面向对象编程语言(比如,Java、C++、C#)都只支持 Single Dispatch,不支持 Double Dispatch。

Java 支持多态特性,代码可以在运行时获得对象的实际类型,然后根据实际类型决定调用哪个方法。尽管 Java 支持函数重载,但 Java 设计的函数重载的语法规则时,并不是在运行时根据传递进函数的参数的实际类型来决定调用哪个重载函数,而是在编译时根据传递进函数的参数的声明类型来决定调用哪个重载函数。也就是说,具体执行哪个对象的哪个方法,只跟对象的运行时类型有关,跟参数的运行时类型无关。所以,Java 语言只支持 Single Dispatch。

备忘录模式

备忘录模式也叫快照模式,具体来说,就是在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。这个模式的定义表达了两部分内容:一部分是,存储副本以便后期恢复;另一部分是,要在不违背封装原则 的前提下,进行对象的备份和恢复。

备忘录模式的应用场景也比较明确和有限,主要是用来防丢失、撤销、恢复等。它跟平时我们常说的“备份”很相似。两者的主要区别在于,备忘录模式更侧重于代码的设计和实现,备份更侧重架构设计或产品设计。

命令模式

命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等 (附加控制)功能。

落实到编码实现,命令模式用的最核心的实现手段,是将函数封装成对象。我们知道,C 语言支持函数指针,我们可以把函数当作变量传递来传递去。但是,在大部分编程语言中,函数没法儿作为参数传递给其他函数,也没法儿赋值给变量。借助命令模式,我们可以将函数 封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。

当我们把函数封装成对象之后,对象就可以存储下来,方便控制执行。所以,命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。

// 命令接口
public interface Command {
    void execute();
}

// 具体命令类(打开灯)
class TurnOnLightCommand implements Command {
    private Light light;

    public TurnOnLightCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.turnOn();
    }
}

// 具体命令类(关灯)
class TurnOffLightCommand implements Command {
    private Light light;

    public TurnOffLightCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.turnOff();
    }
}

// 接收者类
class Light {
    public void turnOn() {
        System.out.println("The light is on");
    }

    public void turnOff() {
        System.out.println("The light is off");
    }
}

// 调用者类
class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        // 创建接收者
        Light light = new Light();

        // 创建具体命令并指定接收者
        Command turnOn = new TurnOnLightCommand(light);
        Command turnOff = new TurnOffLightCommand(light);

        // 创建调用者并设置命令
        RemoteControl remote = new RemoteControl();

        // 执行命令
        remote.setCommand(turnOn);
        remote.pressButton();  // 输出: The light is on

        remote.setCommand(turnOff);
        remote.pressButton();  // 输出: The light is off
    }
}

解释器模式

解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。解释器模式只在一些特定的领域会被用到,比如编译器、规则引擎、正则表达式。

中介模式

中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。

实际上,中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互,从而最小化对象之间的交互关系,降低了代码的复杂度,提高了代码的可读性和可维护性。