# 设计原则

# 单一职责原则 SRP

  • 定义:不要存在多于一个导致类变更的原因
  • 一个类/接口/方法只负责一项职责
  • 优点:降低类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险

系统应该由许多短小的类而不是少量巨大的类组成。每个小类封装一个职责,只有一个修改的理由,并与少数其它类一起协同达成期望的系统行为。

# 开闭原则 OCP

  • 定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭
  • 用抽象构建框架,用实现扩展细节(面向抽象编程)
  • 优点:提高软件系统的可复用性及可维护性

# 依赖倒置原则 DIP

  • 定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象
  • 抽象不应该依赖细节,细节应该依赖抽象(针对接口编程,不要针对实现编程)
  • 优点:可以减少类间的耦合性、提高系统稳定性,提高代码可读性和可维护性,可降低修改程序所造成的风险

# 接口隔离原则 ISP

  • 定义:用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
  • 一个类对一个类的依赖应该建立在最小的接口上
  • 建立单一接口,不要建立庞大臃肿的接口;尽量细化接口,接口中的方法尽量少;注意适度原则,一定要适度
  • 优点:符合高内聚低耦合的设计思想,从而使得类具有很好的可读性、可扩展性和可维护性

# 迪米特法则(最少知道原则)LoD

  • 定义:一个对象应该对其它对象保持最少的了解
  • 尽量降低类与类之间的耦合
  • 优点:降低类之间的耦合

# 里氏替换原则 LSP

  • 定义:如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都替换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型
  • 定义扩展:一个软件实体如果使用的是一个父类的话. 那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化,简单地说,子类型必须能够替换掉它们的父类型
  • 引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能
    • 1:子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
    • 2:子类中可以增加自己特有的方法
    • 3:当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松
    • 4:当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等
  • 优点:
    1. 约束继承泛滥,开闭原则的一种体现
    2. 加强程序的健壮性,同时变更时也可以做到非常好的兼容性;提高程序的维护性、扩展性。降低需求变更时引入的风险

# 组合/聚合复用原则

  • 定义:尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的
  • 聚合 has-a、组合 contains-a
  • 优点:可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少

# 设计模式

按照模式的应用目标分类,设计模式可以分为创建型模式、结构型模式和行为型模式:

  • 创建型模式,是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)
  • 结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验,常见的结构型模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等
  • 行为型模式,是从类或对象之间交互、职责划分等角度总结的模式,比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)

# 单例模式(Singleton)

  • 目的:保证在整个应用中某一个类有且只有一个实例,而且自行实例化并向整个系统提供这个实例

  • 例如,工具类的设计,工具类在开发中其实只需要存在一个对象即可:

    1. 如果工具方法没有使用 static 修饰,说明工具方法得使用工具类的对象来调用,此时应把工具类设计为单例
    2. 如果工具方法全部使用 static 修饰,说明工具方法只需要使用工具类名调用即可,此时必须把工具类的构造器私有化

# 饿汉式(Eager Singleton)

  • 在类装载的时候就创建对象实例
  • 使用场景:java.lang.Runtime
public class Singleton {
    // 本类中创建 private static final 修饰的本类对象实例
    private static final Singleton instance = new Singleton();

    // 私有化构造器
    private Singleton() {
        if(instance != null) {
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }

    // 提供一个 public static 修饰的方法返回此对象实例
    public static Singleton getInstance() {
        return instance;
    }

    // 序列化和反序列化安全
    private Object readResolve(){
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 懒汉式 / 懒加载(Lazy Singleton)

  • 每次获取实例都会进行判断,看是否需要创建实例

  • 将目标属性声明为 volatile 型 ,保证多线程场景下共享变量的内存可见性:原因参照 《The "Double-Checked Locking is Broken" Declaration》 (opens new window)

    原因在于:初始化 Singleton将对象地址写到 instance 字段的顺序是不确定的(存在指令排序)。在某个线程 new Singleton() 时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给 instance 字段了,然而该对象可能还没有初始化;此时若另外一个线程来调用 getInstance,取到的就是状态不正确的对象。

    instance = new Singleton() 包括三步:1. 分配对象的内存空间;2. 初始化对象;3. 设置 instance 指向内存空间。

    跟据 Java 语言规范,所有线程在执行 Java 程序时必须遵守 intra-thread semantics。intra-thread semantics 保证重排序不会改变单线程内的程序执行结果。换句话说,intra-thread semantics 允许那些在单线程内,不会改变单线程程序执行结果的重排序。

    所以第 2、3 步可能会被重排序

  • “双重检查加锁”机制(double-checked locking):不是每次进入 getInstance() 方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而**减少了多次在同步情况下进行判断**所浪费的时间。

public class Singleton {
    // 本类中定义 private static volatile 修饰的、引用为空的类变量
    private static volatile Singleton instance = null;

    // 私有化构造器
    private Singleton() {}

    // 提供一个 public static 修饰的方法返回对象实例
    public static Singleton getInstance() {
        // 先检查实例是否存在,如果不存在才进入下面的同步块
        if (instance == null) {
            synchronized (Singleton.class) {
                // 双重检测锁:再次检查实例是否存在,如果不存在才真正的创建实例
                 if (instance == null) {
                     
                     // 1. 分配对象的内存空间
                     // 2. 初始化对象
                     // 3. 设置 instance 指向内存空间
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
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
public class Singleton {
    // 私有化构造器
    private Singleton() {}

    // 静态内部类不会在单例类加载时就加载,而是在调用 getInstance() 方法时才进行加载
    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Java 语言规范规定,对于每一个类或者接口 C,都有一个唯一的初始化锁 LC 与之对应。从 C 到 LC 的映射,由 JVM 的具体实现去自由实现。JVM 在类初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类已经被初始化过了。

// 延迟初始化时线程同步解决模式
public class CodingExample {
    private volatile Map<String, String> helloWordsMap;

    private void setHelloWords(String language, String greeting) {
        // 声明为 volatile 的 helloWordsMap 是一个共享的资源,它的读写,需要在不同的线程间保持同步
        // 由于 volatile 变量的引用(地址)一旦初始化,就不再变更
        // 因此可以使用局部变量 temporaryMap 进行操作,降低共享变量读写频率
        Map<String, String> temporaryMap = helloWordsMap;
        if (temporaryMap == null) {    // 1st check (no locking)
            synchronized (this) {
                temporaryMap = helloWordsMap;
                if (temporaryMap == null) {    // 2nd check (locking)
                    temporaryMap = new ConcurrentHashMap<>();
                    helloWordsMap = temporaryMap;
                }
            }
        }
        
        temporaryMap.put(language, greeting);
    }

    // snipped
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 使用枚举类做单例模式

  • 定义一个包含单个元素的枚举类型
public enum Singleton {
    INSTANCE;
}

// 调用
Singleton.INSTANCE.sort(null);
1
2
3
4
5
6

# 模板方法模式(Template Method)

  • 在采用某个算法的框架,需要对它的某些部分进行改进

  • 抽象父类的一个方法中定义该方法的总体算法的骨架(模板方法),而把其中某些具体的步骤(不能实现的部分)抽象成抽象方法,延迟到子类中去实现

  • 抽象父类至少提供的方法:

    1. 模板方法:一种通用的处理方式,即模板(总体算法的骨架)
    2. 抽象方法:一种具体的业务功能实现,由子类完成

使用场景:JDBCTemplate、JUC中的 AQS

public abstract class OperateTimeTemplate{
    // 模板方法:总体算法的骨架,子类不能修改
    public final long getTotalTime() {
        long begin = System.currentTimeMillis();
        this.doWork(); // 具体的步骤(延迟到子类中去实现)
        long end = System.currentTimeMillis();
        return end - begin;
    }
    // 抽象方法
    protected abstract void doWork();
}

public class StringOperate extends OperateTimeTemplate {
    public void doWork() {
        String str = "";
        for (int i = 0; i < 10000; i++) {
            str += i;
        }
    }
}

// 调用
new StringOperate().getTotalTime()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 装饰器模式(Decorator)

  • 在不必改变源代码基础上,动态地扩展一个对象的功能
  • 通过创建一个包装对象,包裹真实的对象(通过构造方法传入真实的对象)
  • 使用场景:IO 流中的包装流、Servlet 中的敏感字过滤
  • 特点:
    1. 装饰对象和真实对象有相同的接口
    2. 装饰对象包含一个真实对象的引用(多用组合,少用继承)
    3. 装饰对象接受所有来自客户端的请求,并把这些请求转发给真实的对象
    4. 装饰对象可以在转发这些请求以前或以后增加一些附加功能

# 适配器模式(Adapter)

  • 将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作

# 代理模式(Proxy)

  • 客户端不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象

# 策略模式(Strategy)

  • 策略模式代表了解决一类算法的通用解决方案,需要在运行时选择使用哪种方案
  • 定义一系列的算法,并将每一个算法封装起来,而且使它们可以相互替换,让算法独立于使用它的客户而独立变化
  • 策略模式包含三部分内容
    1. 一个代表某个算法的接口(它是策略模式的接口)
    2. 一个或多个该接口的具体实现,它们代表了算法的多种实现
    3. 一个或多个使用策略对象的客户
/**
 * 策略模式+工厂模式
 */

public interface IPay {
    void pay();
}

@Service
public class AliaPay implements IPay {

    @PostConstruct
    public void init() {
        PayStrategyFactory.register("aliaPay", this);
    }

    @Override
    public void pay() {
        System.out.println("===发起支付宝支付===");
    }
}

@Service
public class WeixinPay implements IPay {

    @PostConstruct
    public void init() {
        PayStrategyFactory.register("weixinPay", this);
    }

    @Override
    public void pay() {
        System.out.println("===发起微信支付===");
    }
}

public class PayStrategyFactory {

    private static Map<String, IPay> PAY_REGISTERS = new HashMap<>();

    public static void register(String code, IPay iPay) {
        if (null != code && !"".equals(code)) {
            PAY_REGISTERS.put(code, iPay);
        }
    }

    public static IPay get(String code) {
        return PAY_REGISTERS.get(code);
    }
}

@Service
public class PayService3 {

    public void toPay(String code) {
        PayStrategyFactory.get(code).pay();
    }
}
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
52
53
54
55
56
57
58

# 观察者模式

  • 某些事件发生时(比如状态转变),一个对象(主题)需要自动地通知其他多个对象(观察者)

# 责任链模式

  • 一种创建处理对象序列(比如操作序列)的通用方案:一个处理对象可能需要在完成一些工作之后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个处理对象,以此类推

# 工厂模式

  • 无需向客户暴露实例化的逻辑就能完成对象的创建
  • Spring 中:BeanFactory 和 ApplicationContext

# 迭代器模式

  • 提供一个一致的方法来顺序访问集合中的对象,这个方法与底层的集合的具体实现无关
  • JDK 中:java.util.Iterator、java.util.Enumeration

# 享元模式

  • 使用缓存来加速大量小对象的访问时间
  • JDK 中:java.lang.Integer#valueOf(int)、java.lang.Boolean#valueOf(boolean)、java.lang.Byte#valueOf(byte)、java.lang.Character#valueOf(char)
Updated at: 2023-03-03 18:03:06