设计模式
设计模式
设计原则
单一职责原则
- 一个类应该只有一个发生变化的原因(业务需求)
开闭原则
- 软件中的对象,类,模块和函数对扩展应该是开放的,但对于修改是封闭的.这意味着应该用抽象定义结构,用具体实现扩展细节,以此确保软件系统开发和维护过程的可靠性
- 面向抽象编程
里氏替换原则
- 如果S是T的子类型,那么所有的T类型对象都可以在不破坏程序的情况下被S类型的对象替换
- 当子类继承父类时,除添加新的方法且完成新增功能外,尽量不要重写父类的方法.
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更加宽松
- 当子类的方法实现父类的方法时(重写,重载或实现抽象方法),方法的后置条件(即方法的输出或返回值)要比父类的方法更加严格或与父类的方法相同.
- 作用
- 里氏替换原则时实现开闭原则的重要方式之一
- 解决了继承中重写父类造成的可复用性变差的问题
- 是动作正确性的保证,即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性
- 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性,可扩展性,降低需求变更时引入的风险
迪米特法则
- Law of Demeter 又称最少知道原则( Least Knowledge principle)
- 一个对象类对于其他对象类来说,知道的越少越好.也就是说,两个类之间不要有过多的耦合关系,保持最少关联性
接口隔离原则
- 一个类对另一个类的依赖应该建立在最小接口上,要求程序将臃肿庞大的接口拆分成更小和更具体的接口,让接口中只包含客户感兴趣的方法
- 衡量规则
- 接口尽量小,但是要有限度,一个接口只服务于一个子模块或业务逻辑
- 为依赖接口的类定制服务,只提供调用者需要的方法,屏蔽不需要的方法
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同。要深入了解业务逻辑
- 提高内聚,减少对外交互。让接口用最少的方法完成最多的事情
依赖倒置原则
- 在设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象
- 依赖倒置原则是实现开闭原则的重要途径之一,它降低了类之间的耦合,提高了系统的稳定性和可维护性,同时这样的代码一般更易读,且便于传承
设计模式
- 模式:在某种情境下,针对某问题的某种解决方案
- 情境:应用某个模式的情况,应该是会普遍出现的情况。
- 问题:你想在某情境下达到的目标,也可以是某情境下的约束。
- 解决方案:一个通用的设计,用来解决约束,达到目标。
分类
- 根据模式类目中所采用的分类方式
- 创建型:涉及将对象实例化,这类模式都提供一个方法,将客户从所需要实例化的对象中解耦
- 工厂方法,抽象工厂
- 原型模式
- 单例模式
- 建造者模式
- 结构型:类或对象组合到更大的结构中
- 适配器模式
- 组合模式
- 代理模式
- 外观模式
- 组合模式
- 享元模式
- 中介模式
- 行为模式:涉及到类和对象如何交互及分配职责
- 模板方法模式
- 遍历器
- 观察者
- 状态模式
- 命令模式
- 策略模式
- 创建型:涉及将对象实例化,这类模式都提供一个方法,将客户从所需要实例化的对象中解耦
单例模式(Singleton)
- 确保一个类只有一个实例,并提供全局访问
实现方式
- 饿汉式:采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就可以避免在程序运行的时候,再去初始化导致的性能问题。
1
2
3
4
5
6
7
8
9
10
11public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();// 饿汉式的精髓
private IdGenerator() {}
public static IdGenerator getInstance() {
return instance;
}
public long getId() {
return id.incrementAndGet();
}
} - 懒汉式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance; // 懒汉式,调用getInstance时才实例化
private IdGenerator() {}
public static synchronized IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
} - DLC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
// 因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。高版本的 Java 已经在 JDK 内部实现中解决了这个问题(解决的方法很简单,只要把对象 new 操作和初始化操作设计为原子操作,就自然能禁止重排序)。
private static IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
if (instance == null) { //1. 第一次检测
synchronized(IdGenerator.class) { // 此处为类级别的锁
if (instance == null) { //2. 第二次检测
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
} - 静态内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {}
private static class SingletonHolder{
private static final IdGenerator instance = new IdGenerator();
}
public static IdGenerator getInstance() {
return SingletonHolder.instance;
}
public long getId() {
return id.incrementAndGet();
}
}
//SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。
//只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。 - 枚举
1
2
3
4
5
6
7
8public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
单例存在的问题
- 单例对 OOP 特性的支持不友好
- 单例会隐藏类之间的依赖关系 单例无法通过构造函数、参数传递的方式来进行创建,无法通过函数的定义来查看类的依赖关系,具体实现逻辑都是隐藏在内部的,需要仔细查看代码实现才能知道这个类到底依赖哪些类。
- 单例对代码的扩展性不友好
- 单例对代码的可测试性不友好 单例类这种硬编码式的使用方式,会导致在编写单元测试的时候很难实现 对依赖的外部资源进行mock 替换
- 单例不支持有参数的构造函数 单例不支持有参数的构造函数
原型模式
- 如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。
- 实现方式: 深拷贝和浅拷贝
- 构造函数不会执行,必须实现Cloneable 接口
工厂模式(factory)
- 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。
- 优点:避免创建者与具体的产品逻辑耦合。满足单一职责,每一个业务逻辑的实现都在自己所属的类中完成;满足开闭原则,无须更改调用方式就可以在程序中引入新的产品类型。
- 依赖倒置原则
- 应避免依赖具体类,尽量依赖抽象。
简单工厂模式
- 又称为静态工厂方法(Static Factory Method)模式,在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
工厂方法
- 使用多态的方式创建对象,减少了if-else语句的使用
抽象工厂模式
- 提供一个接口用于创建相关或依赖对象的家族,而不需要明确指定具体类
- 相同点和不同点
- 工厂方法通过继承类来创建对象,抽象通过接口实现
- 工厂方式使用继承,把对象的创建委托给子类,由子类实现工厂方法来实现对象的创建。
- 抽象工厂使用对象组合,对象的创建被实现在工厂的接口所暴露出来的方法中。
建造者模式(Builder Pattern)
- 又叫生成器模式
- 封装一个产品的构造过程,并允许按步骤构造。Builder 设计模式的目的是将复杂对象的构造与其表示分离。通过这样做,相同的构建过程可以创建不同的表示
享元模式(Flyweight Pattern)
- 享元(Flyweight)的核心思想很简单:如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度。
- 在实际应用中,享元模式主要应用于缓存,即客户端如果重复请求某些对象,不必每次查询数据库或者读取文件,而是直接返回内存中缓存的数据。
- 例子: Java中的Integer -128- 127 提前创建对象缓存
- 享元模式与单例的区别
- 在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享
- 应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。
- 享元模式和缓存的区别
- 在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用。
- 享元模式和对象池的区别
- 池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。
组合模式(Composite)
- 允许你将对象组合成树形结构来表先”整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
- 使用组合模式我们能把相同的操作应用在组合和个别对象上,就是可以忽略对象组合和个别对象之间的差别。
- 使用组合模式的前提在于,你的业务场景必须能够表示成树形结构。所以,组合模式的应用场景也比较局限,它并不是一种很常用的设计模式。
代理模式(proxy)
- 为另一个对象提供替身或占位符以控制对这个对象的访问。被代理的对象可以是远程对象,创建开销大的对象或需要安全控制的对象。
- 代理也会造成项目中类的数量的增加。
远程代理(stub-proxy)
stub是服务器,proxy是客户端的接口。proxy持有ReadSubject的引用,二者拥有共同的方法。Proxy 的接口供客户端程序调用,然后它内部会把信息包装好,以某种方式(比如 RMI)传递给 Stub,而后者通过对应的接口作用于服务端系统,从而完成了“远程调用”。
虚拟代理(Virtual Proxy)
- 虚拟代理作为创建开销大的对象的代理。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。
保护代理(动态代理)
- 根据访问权限绝对是否可以访问对象的代理(拦截器)。
其他代理模式
- 防火墙代理(FireWall proxy)
- 控制网络资源的访问,保护主题免于侵害
- 职能引用代理(smart Reference Proxy)
- 当主题被引用时,进行额外的动作,例如计算一个对象被引用的次数
- 缓存代理(Caching Proxy)
- 为开销大的运算结果提供暂时存储,也允许多个用户共享结果,以减少计算或网络延迟。
- 同步代理(Synchronization Proxy)
- 为分布式环境提供同步控制访问
- 复杂隐藏代理(Complexity Hiding Porxy)
- 用来隐藏一个类的复杂集合的复杂度,并进行访问控制。又是也成为外观代理(Facade Proxy),和外观模式的区别就是代理还会控制访问。
- 写入时复制代理(Cope-On-Write Po)
- 用来控制对象的复制,方法时延迟对象的复制,知道客户真的需要为止。这是虚拟代理的变体。实例:JAVA的CopyOnWriteArrayList
桥接模式(Bridge Pattern)
- 不只改变你的实现,也改变你的抽象。
- 将实现和抽解耦,应用实例: JDBC
装饰者模式(Decorator Pattern)
- 定义:动态地将责任附加到对象上,想要扩展功能,装饰者提供有别于继承的另一种选择。
- 缺点:需要管理更多的对象
适配器模式(Adapter Pattern)
- 将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以兼容。
- 适配器模式包含类适配器(多重继承)和对象适配器
- 适配器模式是改配类的接口来适配需求
- 典型应用: DDD结构中的gateway层
外观模式(Facade Design Pattern)
- 提供一个统一的接口,用来访问子系统中的一群接口,外观定义了一个高层接口,让子系统更容易使用。
- 最小知识原则
中介模式(Mediator Design Pattern)
- 中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互
- 中介模式的设计思想跟中间层很像,通过引入中介这个中间层,将一组对象之间的交互关系(或者依赖关系)从多对多(网状关系)转换为一对多(星状关系)。
- 经典例子: 航空管制
- 为了让飞机在飞行的时候互不干扰,每架飞机都需要知道其他飞机每时每刻的位置,这就需要时刻跟其他飞机通信。
- 飞机通信形成的通信网络就会无比复杂。这个时候,我们通过引入“塔台”这样一个中介,让每架飞机只跟塔台来通信,发送自己的位置给塔台,由塔台来负责每架飞机的航线调度。这样就大大简化了通信网络。
命令模式(Command)
- 将请求封装成对象,这可以让你使用不同的请求,队列或者日志来参数化其他对象,命令模式也可以支持撤销操作。
- 当需要将发出请求的对象与执行请求的对象解耦的时候,使用命令模式
- 命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。
责任链模式(Chain of Responsibility Pattern)
- 定义:将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止
- 让一个以上对象有机会能够处理某个请求的时候,就是用责任链模式
- 职责链模式最常用来开发框架的过滤器和拦截器
迭代器模式(Iterator)
- 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
观察者模式(Observer)
- 定义
- 在对象之间定义一对多依赖,这样一来,当一个对象改变状态,依赖他的对象都会收到通知,并自动更新。
- 优点
- 交互对象之间的松耦合,有弹性的OO系统,使对象之间的相互依赖降到了最低。
- java内置的观察者模式
- 缺点:被观察者要继承父类而不是实现接口
策略模式(Strategy Pattern)
- 定义:定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
- 理解
- 将一些容易变化的行为或者属性抽象为接口,独立出来实现,方便维护。
状态模式(State)
- 允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
- 与策略模式的差异
- 策略模式中,客户是有意识的进行对象转换,而状态模式,客户对此不会关心及察觉
- 策略模式是除继承外一种弹性的替代方式。状态模式是为了将行为包装到状态中,从而替代在context的很多条件判断。
有限状态机
- 状态机有 3 个组成部分:
状态(State)
、事件(Event)
、动作(Action)
。 - 状态机的实现方式
- 分支逻辑法:
- 参照状态转移图,将每一个状态转移,原模原样地直译成代码。
- 利用 if-else 或者 switch-case 分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,这种实现方式最简单、最直接,是首选。
- 查表法:
- 状态机还可以用二维表来表示,在二维表中,第一维表示当前状态,第二维表示事件
- 对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。
- 状态模式
- 对于状态并不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,我们首选这种实现方式。
- 分支逻辑法:
模板方法模式(Template Method Design Pattern)
- 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况,重新定义算法中的某些步骤。
- 钩子
- 子类必须提供算法中的某个方法或者步骤的实现时,使用抽象方法,如果是可选的,使用钩子。
- 钩子还可以允许子类在内部列表重新组织后执行某些动作
复合模式
OO设计
基础
- 抽象
- 封装
- 多态
- 继承
原则
- 封装变化
- 多用接口,少用继承
- 针对接口编程,不针对实现编程
- 为交互对象之间的松耦合设计而努力
- 对扩展开放,对修改关闭
- 依赖抽象,不依赖具体类
- 一个类应该只有一个引起变化的原因
中英文对照表
中文 | 英文 |
---|---|
工厂方法模式 | Factory Method Pattern |
抽象工厂模式 | Abstract Factory Pattern |
建造者模式 | Builder Pattern |
原型模式 | Prototype Pattern |
单例模式 | Singleton Pattern |
适配器模式 | Adapter Pattern |
桥梁模式/桥接模式 | Bridge Pattern |
组合模式 | Composite Pattern |
装饰模式 | Decorator Pattern |
门面模式/外观模式 | Facade Pattern |
享元模式 | Flyweight Pattern |
代理模式 | Proxy pattern |
责任链模式 | Chain of Responsibility Pattern |
命令模式 | Command Pattern |
解释器模式 | Interpreter Pattern |
迭代器模式 | Iterator Pattern |
中介者模式 | Mediator Pattern |
备忘录模式 | Memento Pattern |
观察者模式 | Observer Pattern |
状态模式 | State Pattern |
策略模式 | Strategy Pattern |
模板方法模式 | Template Method Pattern |
访问者模式 | Visitor Pattern |
设计模式
https://x-leonidas.github.io/2022/02/01/06设计模式/设计模式/