警告
本文最后更新于 2022-09-02,文中内容可能已过时。
设计模式(Design Pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。
面向对象的特点是:
设计模式的六大原则:
- 开闭原则(Open-Closed Principle,OCP):一个软件实体如类、模块和函数应该对修改封闭,对扩展开放。
- 单一职责原则(Single Responsibility Principle,SRP):一个类只做一件事,一个类应该只有一个引起它修改的原因。
- 里氏替换原则(Liskov Substitution Principle,LSP):子类应该可以完全替换父类。也就是说在使用继承时,只扩展新功能,而不要破坏父类原有的功能。
- 依赖倒置原则(Dependence Inversion Principle,DIP):细节应该依赖于抽象,抽象不应依赖于细节。把抽象层放在程序设计的高层,并保持稳定,程序的细节变化由低层的实现层来完成。
- 迪米特法则(Law of Demeter,LoD):又名“最少知道原则”,一个类不应知道自己操作的类的细节,尽量降低类与类之间的耦合。
- 接口隔离原则(Interface Segregation Principle,ISP):客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。
Creational Patterns
Factory Pattern
构建对象最常用的方式是 new 一个对象。然而每 new 一个对象,相当于调用者多知道了一个类,增加了类与类之间的联系,不利于程序的松耦合。工厂模式便是用于封装对象构建过程的设计模式。
Simple Factory Pattern
当我们需要创建一个对象时,不直接 new 这个对象,而是通过提供对象信息给对象工厂,然后由工厂提供创建的对象。这样调用者无需关心对象的创建细节,实现了解耦。
| 1
2
3
4
5
6
7
8
9
 | class User {
    public void eat() {
        FruitFactory fruitFactory = new FruitFactory();
        Fruit apple = fruitFactory.create("苹果");
        apple.eat();
        Fruit pear = fruitFactory.create("梨");
        pear.eat();
    }
}
 | 
| 1
2
3
4
5
6
7
8
9
 | class FruitFactory {
    public Fruit create(String type) {
        switch (type) {
            case "苹果": return new Apple();
            case "梨": return new Pear();
            default: throw new IllegalArgumentException("暂时没有这种水果");
        }
    }
}
 | 
| 1
2
3
4
5
 | abstract class Fruit {}
class Apple extends Fruit {}
class Pear extends Fruit {}
 | 
优点:
- 封装了对象创建细节,调用者无需关心对象的创建细节,实现了解耦。
缺点:
- 如果需要生产的产品过多,此模式会导致工厂类过于庞大,承担过多的职责,变成超级类。当苹果生产过程需要修改时,要来修改此工厂。梨子生产过程需要修改时,也要来修改此工厂。也就是说这个类不止一个引起修改的原因,违背了单一职责原则。
- 当要生产新的产品时,必须在工厂类中添加新的分支。而开闭原则告诉我们:类应该对修改封闭。我们希望在添加新功能时,只需增加新的类,而不是修改既有的类,违背了开闭原则。
Factory Method Pattern
为了解决简单工厂的缺点,工厂方法模式应运而生,它规定每个产品都有一个专属工厂。
| 1
2
3
4
5
6
7
8
 | class User {
    public void eat() {
        Fruit apple = new AppleFactory().create();
        apple.eat();
        Fruit pear = new PearFactory().create();
        pear.eat();
    }
}
 | 
| 1
2
3
4
5
 | class AppleFactory {
    public Fruit create() {
        return new Apple();
    }
}
 | 
| 1
2
3
4
5
 | class PearFactory {
    public Fruit create() {
        return new Pear();
    }
}
 | 
优点:
缺点:
- 将调用者与创建对象解耦的同时,引入了调用者和对象工厂之间的耦合。
Abstract Factory Pattern
定义一个工厂接口,提供创建方法,然后由具体工厂去实现该方法。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
 | class User {
    public void eat() {
        FruitFactory fruitFactory = new AppleFactory();
        Fruit apple = fruitFactory.create();
        apple.eat();
        fruitFactory = new PearFactory();
        Fruit pear = fruitFactory.create();
        pear.eat();
    }
}
 | 
| 1
2
3
 | interface FruitFactory {
    Fruit create();
}
 | 
| 1
2
3
4
5
6
 | class AppleFactory implements FruitFactory {
    @Override
    public Fruit create() {
        return new Apple();
    }
}
 | 
| 1
2
3
4
5
6
 | class PearFactory implements FruitFactory {
    @Override
    public Fruit create() {
        return new Pear();
    }
}
 | 
优点:
- 实现调用者与创建对象的解耦,也实现了调用者与具体对象工厂的解耦。
- 只需少量代码即可无缝切换创建对象的具体工厂类。
缺点:
- 当需要新增功能时,所有实现了工厂接口的具体工厂都必须实现新增的抽象方法,代码修改量巨大。
Singleton Pattern
当一个类全局只需要一个实例时,即可使用单例模式。
不能自行创建,可通过指定方法获取该实例。
优点:
- 减少内存开销。
- 避免对资源的多重占用。
- 优化对共享资源的访问。
缺点:
- 单例模式一般没有接口,扩展困难。扩展必须修改代码,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
用户使用
| 1
 | Singleton instance = Singleton.getInstance();
 | 
实例在声明时便初始化,即使不使用。
| 1
2
3
4
5
6
7
8
9
 | class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}
 | 
实例在第一次获取时才初始化,减少内存占用和减少类初始化时间。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 | 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;
    }
}
 | 
为什么使用 synchronized ?
如果有多个线程同一时间调用 getInstance 方法,instance 变量可能会被实例化多次。为了保证线程安全,我们需要给判空过程加上锁。
为什么使用两次 if 判断?
如果外面不做空检查,当多个线程调用 getInstance 时,每次都需要执行 synchronized 同步方法,这样会严重影响程序的执行效率。所以更好的做法是在同步之前,再加上一层检查。
如果里面不做空检查,可能会有两个线程同时通过了外面的空检查,然后在一个线程 new 出实例后,第二个线程进入锁中又 new 出一个实例,导致创建多个实例。
为什么使用 volatile ?
JVM 底层为了优化程序运行效率,可能会对我们的代码进行指令重排序,在一些特殊情况下会导致出现空指针,为了防止这个问题,更进一步的优化是给 instance 变量加上 volatile 关键字。
instance = new Singleton()实际上执行了三条重要的指令:
- 分配对象的内存空间。
- 初始化对象。
- 将变量 instance 指向刚分配的内存空间。
在这个过程中,如果第二条指令和第三条指令发生了重排序,可能导致 instance 还未初始化时,其他线程提前通过双检锁外层的 null 检查,获取到“不为 null,但还没有执行初始化”的 instance 对象,发生空指针异常。
静态内部类实现。内部类会在使用时才被加载,并且 JVM 会保证内部类只会被加载一次。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 | class Singleton {
    private static class Inner {
        public static Singleton instance = new Singleton();
    }
    private Singleton() {}
    public Singleton getInstance() {
        return Inner.instance;
    }
}
 | 
Builder Pattern
建造者模式用于创建过程稳定,但配置多变的对象。
用户不能通过 new 创建对象,只能通过 Builder 构建。对于必须配置的属性,通过 Builder 的构造方法传入,可选的属性通过 Builder 的链式调用方法传入,如果不配置,将使用默认配置。
使用建造者模式的好处是不用担心忘了指定某个配置,保证了构建过程是稳定的。
| 1
2
3
4
5
6
 | class User {
    public void buyMilkTea() {
        MilkTea milkTea = new MilkTea().Builder("原味奶茶").build();
        MilkTea mongoMilkTea = new MilkTea.Builder("杨枝甘露").size(2).ice(true).build();
    }
}
 | 
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 | class MikeTea {
    private String type;
    private int size;
    private boolean ice;
    private MikeTea() {}
    private MikeTea(Builder builder) {
        type = builder.type;
        size = builder.size;
        ice = builder.ice;
    }
    public static class Builder {
        private String type;
        private int size = 1;
        private boolean ice = true;
        public Builder(String type) {
            this.type = type;
        }
        public Builder size(int size) {
            this.size = size;
            return this;
        }
        public Builder ice(boolean ice) {
            this.ice = ice;
            return this;
        }
        public MikeTea create() {
            return new MikeTea(this);
        }
    }
}
 | 
Prototype Pattern
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
简单来说就是深拷贝复制对象。
| 1
2
3
4
5
6
7
 | class User {
    public void buyMilkTea() throws CloneNotSupportedException {
        // 两杯一样的
        MilkTea milkTea1 = new MilkTea("蜂蜜柚子茶", true);
        MilkTea milkTea2 = milkTea1.clone();
    }
}
 | 
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 | class MilkTea implements Cloneable {
    private String type;
    private boolean ice;
    public MilkTea(String type, boolean ice) {
        this.type = type;
        this.ice = ice;
    }
    @Override
    public MilkTea clone() throws CloneNotSupportedException {
        return (MilkTea) super.clone();
    }
}
 | 
Structural Patterns
Adapter Pattern
将一个类的接口转换成用户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
适配器模式适用于有相关性但不兼容的结构,源接口通过一个中间件转换后才可以适用于目标接口,这个转换过程就是适配,这个中间件就称之为适配器。
只有在遇到接口无法修改时才应该考虑适配器模式。如果接口可以修改,那么将接口改为一致的方式会让程序结构更加良好。
| 1
2
3
4
5
 | class Lightning {
    public void transmit() {
        System.out.println("正在使用 Lightning 传输数据");
    }
}
 | 
| 1
2
3
4
5
 | class USBC {
    public void transmit() {
        System.out.println("正在使用 USB-C 传输数据");
    }
}
 | 
| 1
2
3
4
5
6
 | class PC {
    // 电脑只有 USB-C 接口
    public void connect(USBC plug) {
        plug.transmit();
    }
}
 | 
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 | class LightningToUSBCAdapter extends USBC {
    private Lightning plug;
    public LightningToUSBCAdapter(Lightning plug) {
        this.plug = plug;
    }
    @Override
    public void transmit() {
        plug.transmit();
    }
}
 | 
| 1
2
3
4
5
6
7
 | class User {
    public void transmitData() {
        PC pc = new PC();
        pc.connect(new USBC());
        pc.connect(new LightningToUSBCAdapter(new Lightning()));
    }
}
 | 
Bridge Pattern
将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体模式或接口模式。
| 1
2
3
 | interface Shape {
    void draw();
}
 | 
| 1
2
3
4
5
6
 | class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("画矩形");
    }
}
 | 
| 1
2
3
4
5
6
 | class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("画三角形");
    }
}
 | 
| 1
2
3
4
5
6
 | class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("画圆");
    }
}
 | 
Composite Pattern
组合模式:又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
组合模式用于整体与部分的结构,当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式。
- 透明方式:在 Component 中声明所有管理子对象的方法,包括 add 、remove 等,这样继承自 Component 的子类都具备了 add、remove 方法。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口。
- 安全方式:在 Component 中不声明 add 和 remove 等管理子对象的方法,这样叶节点就无需实现它,只需在枝节点中实现管理子对象的方法即可。
安全方式和透明方式各有好处,在使用组合模式时,需要根据实际情况决定。但大多数使用组合模式的场景都是采用的透明方式,虽然它有点不安全,但是客户端无需做任何判断来区分是叶子结点还是枝节点。
Decorator Pattern
动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器,与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”。
- 增强一个类原有的功能。
- 为一个类添加新的功能。
- 不会改变原有的类功能。
没有修改原有的功能,只是扩展了新的功能,这种模式在装饰模式中称之为半透明装饰模式。
这个装饰类对客户端来说是可见的、不透明的。而被装饰者可以是实现了指定接口的任意对象,所以被装饰者对客户端是不可见的、透明的。由于一半透明,一半不透明,所以称之为半透明装饰模式。
半透明装饰模式中,我们无法多次装饰。
Java I/O 的设计框架便是使用的装饰者模式。
Facade Pattern
外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式。
外观模式就是这么简单,它使得两种不同的类不用直接交互,而是通过一个中间件——也就是外观类——间接交互。外观类中只需要暴露简洁的接口,隐藏内部的细节,所以说白了就是封装的思想。
Flyweight Pattern
运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式。
享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。FlyWeightFactory 负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象,如果没有,则创建一个新对象,FlyWeight是超类。
Proxy Pattern
代理模式:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 | interface Shape {
    void draw(String color);
    void erase();
}
class Circle implements Shape {
    @Override
    public void draw(String color) {
        System.out.println("画" + color + "圆");
    }
    @Override
    public void erase() {
        System.out.println("擦除图形");
    }
}
class CircleProxy implements Shape {
    private final Circle circle;
    public CircleProxy(Circle circle) {
        this.circle = circle;
    }
    @Override
    public void draw(String color) {
        System.out.println("准备作画");
        circle.draw(color);
        System.out.println("结束作画");
    }
    @Override
    public void erase() {
        System.out.println("准备擦除");
        circle.erase();
        System.out.println("擦除完毕");
    }
}
class User {
    public void drawing() {
        Circle circle = new Circle();
        circle.draw("红色");
        circle.erase();
        CircleProxy proxy = new CircleProxy(circle);
        proxy.draw("红色");
        proxy.erase();
    }
}
 | 
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 | interface Shape {
    void draw(String color);
    void erase();
}
class Circle implements Shape {
    @Override
    public void draw(String color) {
        System.out.println("画" + color + "圆");
    }
    @Override
    public void erase() {
        System.out.println("擦除图形");
    }
}
class CircleProxy implements InvocationHandler {
    private Circle circle;
    public Shape getInstance(Circle circle) {
        this.circle = circle;
        return (Shape) Proxy.newProxyInstance(circle.getClass().getClassLoader(), circle.getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        if (method.getName().equals("draw")) {
            System.out.println("开始作画");
            method.invoke(circle, args);
            System.out.println("结束作画");
        } else if (method.getName().equals("erase")) {
            System.out.println("开始擦除");
            method.invoke(circle, args);
            System.out.println("擦除完毕");
        }
        return result;
    }
}
class User {
    public void drawing() {
        Circle circle = new Circle();
        circle.draw("红色");
        circle.erase();
        Shape proxy = new CircleProxy().getInstance(circle);
        proxy.draw("红色");
        proxy.erase();
    }
}
 | 
Behavioral Patterns
- 图说设计模式 — Graphic Design Patterns
- 设计模式 - 菜鸟教程