常见设计模式汇总

Aengus Sun 975 2020-04-10

软件设计模式是一项软技能,大部分初学者在学习编程之初并不会接触到这些,设计模式也很少出现在大学的课程中,但是却在大型企业,开源项目中有着广泛的应用。设计模式并不会对软件的正常运行造成任何的影响,却会大大影响代码结构的鲁棒性、扩展性以及可维护性,所以设计模式虽然不是必要的,但却应该是每一位开发人员必备的技能。

设计模式细分有二十多种,但是大部分用的比较少,这里列出的是常见的设计模式。

单例模式

单例模式是最常见也是最简单的设计模式,顾名思义,单例是指在软件运行过程中,对于一个类,内存中只有一个实例对象存在,当系统调用时始终返回此实例,实现单例显然需要将构造方法私有化,并有一个static对象。

单例模式又分为懒汉式与饿汉式,前者意味着只有在调用单例对象时才对其进行创建,而后者则代表在类初始化时就对单例对象进行创建。饿汉式是线程安全的,但如果创建对象需要较多资源时,饿汉式反而会拖慢系统运行速度。

**适用场景:**对某个类来说在内存中只需要一个实例对象

**例子:**Spring Boot默认创建的bean、Windows的任务管理器

饿汉式

public class Singleton {
    private static Singleton INSTANCE = new Singleton;
    private Singleton() {
        // 省略构造函数内容...
    }
    
    // 线程安全
    public static Singleton getSingleton() {
        return INSTANCE;
    }
}

懒汉式

public class Singleton {
    private static volatile Singleton INSTANCE;
    private Singleton() {...}
    
    // 线程不安全的
    public static Singleton getSingleton() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
    
    // 线程安全的
    public static Singleton getSafeSingleton() {
        if (INSTANCE == null) {
            synchronized(Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

线程安全的方法中两次判空的代码叫做双重校验锁,第一次判空仅仅是判断对象是否已实例化,而第二次判空是防止在拿到锁之前有其他线程调用了这个方法对INSTANCE进行创建。虽然直接将getSafeSingleton()方法用synchronized关键字修饰也可以实现,但是这两种方法效率都比较低,所以并不推荐。

静态内部类

public class Singleton {
    
    private static class SingletonHolder {
        private static Singleton INSTANCE = new Singleton();
    }
    
    private Singleton() {...}
    
    // 线程安全
    public static Singleton getSingleton() {
        return SingletonHolder.INSTANCE;
    }
	
}

静态内部类由JVM保证单例,而且在外部类被加载时并不会加载内部类,故不会占用资源,而将系统需要用到单例对象时,才会触发内部类的加载,故此种方式即是线程安全的,也可以实现懒汉式的延迟加载,减少资源占用,是比较推荐的一种方式。

枚举方式

通过枚举实现单例模式是《Effective Java》中推荐的做法,同样是由JVM保证的单例以及线程安全。但是由于此种方法较为少见,容易造成混淆,故在实际应用中并不推荐。

public enum Singleton{
    INSTANCE;
    private Singleton() {...};
}

在使用时,我们可以直接用Singleton.INSTANCE来获取单例对象。

工厂模式

所谓工厂模式,是指用户传入指定参数,由工厂来创建参数对应的“产品”(实例对象),而不是用户自己使用new关键字来创建“产品”。工厂模式的优点是无需了解细节,仅仅传入参数便可以得到需要的实例对象。工厂模式最基本的需要有工厂(Factory)、抽象产品(Product)以及具体产品(ConcreteProduct)。

**适用场景:**客户端不知道或不需要知道它所需要的对象的类或者具体构造细节

**例子:**Spring Bean工厂

简单工厂模式

简单工厂模式适合产品种类较少的情况,若出现大量产品则会有大量的if...else...语句,并且每次有新产品都要修改代码。

abstract class Product {
    // 共有方法
    public void commonMehod() {...};
    
    // 业务方法
    public abstract void productMethod();
}
// 具体产品A
class ConcreteProductA extends Product {
    @Override
    public void productMethod(){
        System.out.println("具体产品A");
    };
}
// 具体产品B
class ConcreteProductB extends Product {
    @Override
    public void productMethod(){
        System.out.println("具体产品B");
    };
}
// 工厂
public class Factory {
    // 静态工厂方法
    public static Product getProduct(String productName) {
        if (productName.equals("A")) {
            return new ConcreteProductA();
        } else if (productName.equals("B")) {
            return new ConcreteProductB();
        } else {
            return null;
        }
    }
}

工厂方法模式

工厂方法模式简称为工厂模式。相较于简单工厂模式,工厂方法模式将工厂(Factory)改为了抽象工厂,并声明工厂方法(factoryMethod),而创建产品的具体实现则由其子类具体工厂(ConcreteFactory)来完成。可以通过配置文件存储具体工厂类ConcreteFactory的类名,更换新的具体工厂时无需修改代码。

interface Factory {
    public Product factoryMethod();
}

class ConcreteFactory implements Factory {
    @Override
    public Product factoryMethod() {
        Product product = new ConcreteFactory();
        product.config();  // 对产品进行一些配置...
        return product;
    }
}

// 实际调用
Factory factory;
factory = new ConcreteFactory(); // 由配置文件实现
Product product = factory.factoryMethod();

抽象工厂模式

可以看到工厂方法模式一个工厂仅仅生成一类产品,可能会导致系统中存在大量的工厂类,所以可以通过抽象工厂模式将简单工厂模式与工厂方法模式进行整合来继承两种工厂的优点。

// 抽象产品A
interface ProductA {
    public void productMethod();
}
// 具体产品AA
class ConcreteProductAA implements ProductA {
    @Override
    public void productMethod() {
        System.out.println("具体产品AA");
    }
}
// 具体产品AB
class ConcreteProductAB implements ProductA {
    @Override
    public void productMethod() {
        System.out.println("具体产品AB");
    }
}
/*****************************************************/
// 抽象产品B
interface ProductB {
    public void productMethod();
}
// 具体产品BA
class ConcreteProductBA implements ProductB {
    @Override
    public void productMethod() {
        System.out.println("具体产品BA");
    }
}
// 具体产品BB
class ConcreteProductBB implements ProductB {
    @Override
    public void productMethod() {
        System.out.println("具体产品BB");
    }
}
/*****************************************************/
// 抽象工厂
interface AbstractFactory {
    public ProductA getProductA();
    public ProductB getProductB();
}
// 具体工厂A
class ConcreteFactoryA implements AbstractFactory {
    @Override
    public ProductA getProductA() {
        System.out.println("工厂A生产A种类产品");
        return new ConcreteProductAA();
    }
    
    @Override
    public ProductB getProductB() {
        System.out.println("工厂A生产B种类产品");
        return new ConcreteProductBA();
    }
}
// 具体工厂B
class ConcreteFactoryB implements AbstractFactory {
    @Override
    public ProductA getProductA() {
        System.out.println("工厂B生产A种类产品");
        return new ConcreteProductAB();
    }
    
    @Override
    public ProductB getProductB() {
        System.out.println("工厂B生产B种类产品");
        return new ConcreteProductBB();
    }
}

在这个例子中,两个不同的工厂可以生产两个种类的四种不同产品,如果用实际生活中的例子比喻的话,可以说小米与华为分别有两个工厂生产手环与手机。小米的两个工厂生产小米手环与小米手机,而华为的两个工厂生产华为手环与华为手机,虽然名称不一样,但是小米手环与华为手环都是同类产品,小米手机与华为手机也是同类产品。

代理模式

实现间接访问

代理模式是指因为某些原因,客户端不想或者无法直接访问一个对象,这时可以通过一个称之为“代理”的第三者来实现间接访问。通常实现方式是给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。

代理模式是非常常见的结构型设计模式,在很多项目中都有广泛的应用。

适用场景:客户端不适合直接访问某个对象,一般来说代理对象提供的接口与原接口名称是一致的

**例子:**Spring AOP、JDK的Proxy中的newProxyInstance()方法、Windows的快捷方式

// 抽象主题角色,声明真实主题与代理主题的共同接口
interface Subject {
    public void request();
}
// 真实主题,完整的业务代码
class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("真正使用的方法");
    }
}
// 代理主题,调用代理对象的方法
class Proxy implements Subject {
    Subject delegrate;	// 代理对象
    
    public Proxy(Subject delegrate) {
        this.delegrate = delegrate;
    }
    
    @Override
    public void request() {
        configSomeCondition(delegrate);	// 对代理对象做一些处理...
        delegrate.request();	// 实现代理
    }
    
}

策略模式

算法封装与切换

策略模式最常见的应用是将算法的定义与使用分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法。在使用算法的环境中针对抽象策略类进行编程,出现新的算法时,只需要增加一个新的策略类即可。策略模式可以大大减少if...else的代码。

**适用场景:**一个系统需要动态的在几种算法中选择一种;只能用多重条件选择语句来实现对象多个行为的选择;不希望客户端知道算法的具体实现

**例子:**Java SE的容器布局管理、Spring中的Resource接口及其实现类

// 抽象策略类
abstract class AbstractStrategy {
    public abstract void algorithm(); // 算法定义
}
// 具体策略A
class StrategyA extends AbstractStrategy {
    @Override
    public void algorithm() {
        // 算法A实现
        ...
    }
}
// 具体策略B
class StrategyB extends AbstractStrategy {
    @Override
    public void algorithm() {
        // 算法B实现
        ...
    }
}
// 使用算法的环境类
class Context {
    private AbstractStrategy strategy;
    
    public Context(AbstractStrategy strategy) {
        this.strategy = strategy;
    }
    
    public void algorithm() {
        strategy.algorithm();
    }
}
// 实际应用通过反射使用特定的策略类
AbstractStrategy strategy = (AbstractStrategy)Class.forName("StrategyB").newInstance();
Context context = new Context(strategy);
context.algorithm();

装饰器模式

扩展系统功能

通常来说我们给一个类添加更多职责是通过继承实现的,装饰器模式的作用也在于此,但是相比于继承有更多的灵活性。装饰器模式可以在不改变对象职责的前提下给对象增加额外的行为,通过配置文件的方式也可以在运行时选择不同的具体装饰类。

较为方便扩展的装饰模式常常由四部分组成:抽象组件(Component)中声明在具体组件中实现的业务方法,是具体组件与抽象装饰器的父类,是具体装饰器的间接父类;具体组件(ConcreteComponent)中实现业务方法;抽象装饰器(Decorator)用于给具体构建增加职责,但是具体职责在其子类中实现;具体装饰器(ConcreteDecorator)实现具体的新增职责方法。由于在抽象装饰器Decorator中注入的是抽象组件Component,所以可以将一个已经装饰过的具体装饰器ConcreteDecorator再注入Decorator中进行二次装饰。

**适用场景:**不影响其他对象的情况下对某个对象添加职责;不能采取继承的方法对系统进行扩展(final修饰类)或者采取继承不利于系统扩展和维护时

**例子:**JavaIO中的输入输出流、Spring中的ClientHttpRequestDecorator等、Guava中的CharArrayDecorator

// 抽象组件
abstract class Component {
    public abstract void method();
}
// 具体组件
class ConcreteComponent extends Component {
    @Override
    public void method() {...};
}
// 装饰器
class Decorator extends Component {
    Component component;
    
    public Decorator(Component component) {
        this.component = component;
    }
    
    @Override
    public void method() {
        component.method();	// 调用原有业务方法
    }
}
// 具体装饰器
class ConcreteDecorator extends Decorator {
    public ConcreteDecorator(Component component) {
        super(component);
    }
    public void method() {
        super.method();
        newMethod();	// 装饰的方法(新增职责)
    }
    public void newMethod() {...};
}

适配器模式

不兼容结构的协调

适配器模式的常规用途是为了能继续使用旧的接口。当客户端使用新接口时,可能在旧的服务中有客户端需要的功能,然而此时旧接口的名称与新接口并不一致,这时候便可以使用适配器模式对新旧接口之间进行适配,从而让旧的接口或者说服务能够继续起作用。适配器模式与代理模式有些相似,但是两者的目的并不同,前者是为了继续使用旧的接口,后者是为了实现间接访问。一般来说适配器模式的接口是不一致的,而代理模式的接口名称一般是相同的。

适配器通常的结构有目标抽象类(Target),适配器类(Adapter)与适配者类(Adaptee)。目标抽象类是原本打算调用的接口,也就是新的接口,由于需要使用旧服务也就是适配者类的方法,使用适配器类将目标抽象类与适配者类进行适配。

**适用场景:**系统需要使用现成的类,而这些类的接口不符合系统的需要;创建一个可以重复使用的类,用于与一些彼此之前无太大关联的类一起工作

**例子:**log4j与sl4j等日志框架、SpringJPA

// 新接口,客户端需要调用此接口
interface Target {
    public void newMethod();
}
// 适配者类,里面含有客户端需要的功能代码
class Adaptee {
    public void oldMethod() {
        // 原来的方法
        ...
    }
}
// 适配器,当客户端调用新接口时将其转发到旧服务上面
class Adapter implements Target {
    private Adaptee adaptee;
    
    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }
    
    public void newMethod() {
        adaptee.oldMethod();	// 继续使用旧服务
    }
}

观察者模式

对象之间的联动

观察者模式用于建立对象与对象之间的依赖关系,当一个对象发生改变时将自动通知其他对象。观察者模式比较直观的应用是多人对战游戏,当一个游戏角色变化时,往往其他对象也要随之改变;另一个比较明显的应用是GUI组件,在发生点击事件后,GUI组件将对此产生一个事件。

观察者模式常常由四部分组成:目标(Subject)是指被观察的对象,其中有一个观察者的集合,一个观察者可以接受任意数量的观察者观察,其中还定义一个notify()方法;具体目标(ConcreteSubject)中包含有经常发生改变的数据,当其变化时调用实现的notify()方法通知观察者;观察者(Observer)一般定义为接口,声明了更新数据的方法update();具体观察者(ConcreteObserver)需要实现update()方法,有时候在实现时可以调用具体目标类的方法将自己添加到目标类的观察者集合中或将自己从中删除。

**适用场景:**一个对象的改变会导致其他对象的改变;需要在系统中创建一个触发链

例子:java.util.Observer接口及Observable类、Spring ApplicationContext事件机制

// 目标
abstract class Subject {
    // 观察者集合
    protected ArrayList<Observer> observers = new ArrayList<>();
    // 添加一个观察者
    public void attach(Observer observer) {
        observers.add(observer);
    }
    // 删除一个观察者
    public void detach(Observer observer) {
        observers.remove(observer);
    }
    
    // 声明通知方法
    public abstract void notify();
}
// 具体目标类
class ConcreteSubject extends Subject {
    @Override
    public void notify() {
        for (Observer observer: observers) {
            observer.update();
        }
    }
}
// 观察者
interface Observer {
    public void update();
}
// 具体观察者
class ConcreteObserver implements Observer {
    @Override
    public void update() {
        // 当目标状态变化时自身的响应代码
        ...
    }
}

建造者模式

复杂对象传递组装与创建

建造者模式是使用频率较低的一种设计模式,它的目的是返回一种较为复杂的对象,此对象常常由多个部件组成。由于不同的建造者定义了不同建造过程,且彼此之间相互独立,因此增加新的建造者非常方便。建造者模式允许用户只通过指定复杂对象的类型和内容就可以构建它们,而不需要知道内部的具体构建细节。

建造者主要由四部分组成,分别是抽象建造者(Builder),负责声明建造产品Product对象各个部件的接口,此外还有一类方法是getResult(),用于返回负责对象;具体建造者(ConcreteBuilder)复杂实现建造产品的接口;产品(Product)是被建造的对象,其中含有多个组件;指挥者(Director)负责安排负责对象的建造顺序,一般客户端只与指挥者进行交互。

**适用场景:**产品对象有复杂的内部结构;产品对象的属性相互依赖,需要制定生成顺序;隔离复杂对象的创建与使用

**例子:**游戏角色的“捏脸”、视频播放显示模式(全屏、精简等)

// 复杂产品对象
class Product {
    private String partA;
    private PartB partB;
    private int partC;
    // 省略Setter与Getter方法
    ...
}
// 抽象建造者
abstract class Builder {
    protected Product product = new Product();
    
    public abstract void buildPartA();
    public abstract void buildPartB();
    public abstract void buildPartC();
    
    public Product getResult() {
        return product;
    }
}
// 具体建造者
class ConcreteBuilderA extends Builder {
    
    @Override
    public void buildPartA() {
        // 建造字段A的具体建造方法
        ...
    }
    
    @Override
    public void buildPartB() {
        ...
    }
    
    @Override
    public void buildPartC() {
        ...
    }
}
// 指挥者
class Director {
    private Builder builder;
    public Director(Builder builder) {
        this.builder = builder;
    }
    
    // 产品构建方法
    public Product construct() {
        builder.buildPartA();
        builder.buildPartC();
        builder.buildPartB();
        return builder.getResult();
    }
}
// 具体中的使用方式
Builder builder = (ConcreteBuilderA)Class.forName("ConcreteBuilderA").newInstance();
Director director = new Director(builder);
Product product = director.construct();

Builder模式

在《Effective Java》中,作者建议使用Builder模式来实现很多可选参数对象的创建。Builder模式非常适合链式编程,常见示例如下:

class Proudct {
    private final int fieldA;
    private final int fieldB;
    private final int fieldC;
    private final int fieldD;
    private final int fieldE;
    private final int fieldF;
    
    public static class Builder {
        // 必要参数
        private final int fieldA;
        private final int fieldB;
        
        // 可选参数
        private int fieldC = 0;
        private int fieldD = 0;
        private int fieldE = 0;
        private int fieldF = 0;
        
        public Builder(int fieldA, int fieldB) {
            this.fieldA = fieldA;
            this.fieldB = fieldB;
        }
        
        public Builder fieldC(int fieldC) {
            this.fieldC = fieldC;
            return this;
        }
        
        public Builder fieldD(int fieldD) {
            this.fieldD = fieldD;
            return this;
        }
        
        public Builder fieldE(int fieldE) {
            this.fieldE = fieldE;
            return this;
        }
        
        public Builder fieldF(int fieldF) {
            this.fieldF = fieldF;
            return this;
        }
        
        public Product build() {
            return new Product(this);
        }
    }
    
    public Product(Builder builder) {
        this.fieldA = builder.fieldA;
        this.fieldB = builder.fieldB;
        this.fieldC = builder.fieldC;
        this.fieldD = builder.fieldD;
        this.fieldE = builder.fieldE;
        this.fieldF = builder.fieldF;
    }
}
// 实际应用使用链式编程
Product product = new Product.Builder(10, 5).fieldB(20).fieldE(15).build();

参考

《设计模式Java版》

《Effective Java》

[模式的秘密-适配器模式和代理模式的区别](