设计模式
23 种经典设计模式共分为 3 种类型,分别是创建型、结构型和行为型。
创建型设计模式包括:单例模式、工厂模式、建造者模式、原型模式。它主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。
结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。结构型模式包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。
行为型设计模式主要解决的就是“类或对象之间的交互”问题。行为型模式比较多,有 11 种,它们分别是:观察者模式、模板模式、策略模式、职责链模式、迭代器模式、状态模式、访问者模式、备忘录模式、命令模式、解释器模式、中介模式。
设计模式不可照猫画虎,生搬硬套,更应注重的是设计原则。
单例模式
全局唯一,没必要一直创建,无状态的类,无后续扩展,不依赖外部系统,可以使用单例,有状态的类也可以,但是得考虑并发安全。
单例模式使用时违背基于接口而非实现的原则。
单例类会隐藏类之间依赖关系。
对扩展性不好。
可测试性不好。
不支持有参构造函数。
实现
饿汉式
直接作为类属性 new 出来。
曾经主流认为饿汉式,不如懒汉式,但现在分布式环境,更注重fail-fast原则,越早发现问题越好。
public class Test {
private Student s = new Student();
}
懒汉式
支持延迟加载
双锁检测 + volatile
public class Test {
private volatile Student s;
public Student getInstance() {
if(s == null) {
synchronized(Student.class) {
if(s == null) {
s = new Student();
}
}
}
return s;
}
}
指令重排序可能导致在 Student 类的对象被关键字 new 创建并赋值给 s 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。
静态内部类
基于JVM类加载机制,使用时才加载。写法比双锁更简单。
public class Test {
private static class Singleton {
private static final Student instance = new Student();
}
public static Student getInstance() {
return Singleton.instance;
}
}
枚举
枚举的实例化对象,枚举中每个实例化对象都等价与static final修饰。
编译后其实就是静态内部类。这种写法更简单。
public enum SingleEnum {
INSTANCE;
public void xxx方法()
public void xxx方法()
}
工厂模式
简单工厂
一种是获取对象直接用 if 判断,一种是用map维护条件和对象的关系,第二种可读性好一点,更常用,如下所示
public class Test {
private static final Map<String, IRule> ruleRelations = new HashMap<>();
static {
ruleRelations.put("ARule", new ARule());
ruleRelations.put("BRule", new BRule());
}
public static IRule getRule(String ruleName) {
// ...
return ruleRelations.get(ruleName);
}
}
工厂方法
工厂方法模式比起简单工厂模式更加符合开闭原则。
工厂方法就是给创建类,也做一个工厂,可以理解为工厂的工厂。在工厂里可以有很复杂的创建逻辑。
public interface IRuleFactory {
IRule create();
}
public class ARuleFactory implements IRuleConfigParserFactory {
public IRule create() {
return new ARule();
}
}
public class BRuleFactory implements IRuleConfigParserFactory {
public IRule create() {
return new BRule();
}
}
public class RuleConfigParserFactoryMap {
private static final Map<String, IRuleFactory> cacheMap = new HashMap<>();
static {
cacheMap.put("A", new ARuleFactory());
cacheMap.put("B", new BRuleFactory());
}
public static IRuleFactory getFactory(String type) {
return cacheMap.get(type);
}
}
当对象的创建逻辑比较复杂,不只是简单的 new 一下就可以,而是要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式。
如果对象不可复用,那工厂类每次都要返回不同的对象。也推荐使用工厂方法模式。
抽象工厂
抽象工厂其实就是让一个工厂方法的工厂负责创建多个不同类型的对象,而不是只创建一种对象。这样就可以有效地减少工厂类的个数。
主要是可能是多种分类情况下,一个大分类下好几种,另一个好几种。
public interface IRuleConfigFactory {
IRule create();
IConfig create();
}
public class ARuleFactory implements IRuleConfigParserFactory {
public IRule create() {
return new ARule();
}
public IConfig create() {
return new AConfig();
}
}
建造者模式
使用建造者模式的场景
为了避免构造方法参数过长,满足如下条件之一则可以使用建造者模式
- 类创建所必须属性
- 属性之间有一定的依赖关系或约束条件
- 希望创建不可变对象
代码很简单就是通过返回builder本身可以链式调用,IDEA有快捷键可以直接创建。
方法:
- 在需要创造建造者的类上点击右键,选择Generate
- 点击Builder
- 按需选择,一般都直接作为内部类,勾选 Inner builder
原型模式
使用场景:对象的创建成本比较大,而同一个类的不同对象之间大部分字段都相同。
创建成本比较大 : 复杂计算、网络、数据库、文件系统IO读取
目的:节省创建时间。
浅拷贝只复制索引,不复制数据本身。会导致拷贝出的对象,会跟原对象共用相同的对象。
深拷贝,则连引用的对象都复制一份。
最经典使用是 BeanUtils,但不要用Spring的,性能比较差,建议用cglib的实现。
cglib 使用生成代理类,首次可能略慢,但可以缓存复用,Spring每次都直接反射,整体看速度较慢。
代理模式
在不改变被代理类代码的情况下,通过代理类来增加附加功能
场景:
- 非功能性需求,如监控、统计、鉴权、限流、事物、日志等。
- RPC
- 缓存 @Cache 等
静态代理就是硬编码,组合或继承。
动态代理可使用JDK自带的方式,对接口添加动态代理,如下
public Object createProxy(Object proxiedObject) {
Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
}
private class DynamicProxyHandler implements InvocationHandler {
private Object proxiedObject;
public DynamicProxyHandler(Object proxiedObject) {
this.proxiedObject = proxiedObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// before
Object result = method.invoke(proxiedObject, args);
// after
return result;
}
}
桥接模式
将抽象和实现解耦,让它们可以独立变化。
一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。
桥接模式的实现其实就是 面向接口编程 这一原则。
类 A 与 B 通过抽象类或接口组合,这样就可以独立不断地通知实现子类进行独自变化
public interface MsgSender {
void send(String message);
}
public class AMsgSender implements MsgSender {
}
public class BMsgSender implements MsgSender {
}
public abstract class AbstractTest {
protected MsgSender msgSender;
public Notification(MsgSender msgSender) {
this.msgSender = msgSender;
}
public abstract void notify(String message);
}
public class ATest extends AbstractTest {
}
public class BTest extends AbstractTest {
}
装饰器模式
装饰器类是对功能的增强。
装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。
装饰器模式可以对原始类嵌套使用多个装饰器。
在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。(所有方法都继承)
常见就是 Java 的 流相关。
还有包装类,Request流只能读一次,使用装饰器包装成可以支持多次的。
适配器模式
将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。
主要是用来补救设计上的缺陷。
适配器模式有两种实现方式:类适配器和对象适配器。
类适配器使用继承关系来实现。:继承需适配的类,接口,将接口的方法直接调用super.xx,或自行实现。
对象适配器使用组合关系来实现。:持有对象,包装其方法。
两种怎么简单怎么来。
使用场景:
- 封装有缺陷的接口设计
- 统一多个类的接口设计
- 替换依赖的外部系统(隔离层,但是要早考虑)
- 兼容老版本接口
- 适配不同格式的数据
slf4j 的多个适配器就是适配器模式的不错案例。
门面模式
主要在接口设计方面使用。
接口设计,本身应该是小而美,但是有时候一个功能需要多个接口配合,如果接口过多,依赖太多,此时可以抽象出一个门面,专门对接这些接口实现某些功能。
官方点说就是,门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。
优点:
- 解决易用性问题,封装底层复杂实现,提供易用接口。
- 解决性能问题,前端后端接口交互,将多个封装为一个,减少网络请求。
- 解决分布式事务问题,将多个事物放到一个事物中。
组合模式
将一组对象组织(Compose)成树形结构,以表示一种“部分 - 整体”的层次结构。组合让代码的使用者。可以统一单个对象和组合对象的处理逻辑。
这模式可以看做是递归遍历代码的实现。
一般可以看成树状的数据,就可以使用。
经典案例:文件系统树
public abstract class FileNode {
protected String path;
public abstract int countNum();
public abstract long countSize();
}
public class File extends FileNode {
// 实现文件的
}
public class Directory extends FileNode {
// 实现目录的
}
享元模式
复用对象,节省内存。
前提是享元对象是不可变对象。
简单实现:通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象
只要是缓存其实就可,不一定非得map。
享元模式更注重 同时使用,复用,线程池等池化技术,虽然也复用,但使用时基本都是线程独占池中某个资源。
观察者模式
在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
主要目的就是解耦。
常见使用 Google Guava 的 EventBus 实现观察者模式。
EventBus eventBus = new EventBus(); // 同步阻塞模式
EventBus eventBus = new AsyncEventBus(Executors.newFixedThreadPool(8));// 异步阻塞模式
// EventBus 类提供了 register() 函数用来注册观察者。
public void register(Object object);
// 从 EventBus 中删除某个观察者
public void unregister(Object object);
// 给观察者发送消息
public void post(Object event);
// 观察者实现
public DObserver {
// 被调用方法需实现 @Subscribe,自动根据入参判断走哪个消息处理方法。
@Subscribe
public void f1(PMsg event) { //... }
@Subscribe
public void f2(QMsg event) { //... }
}
模板模式
模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
模板模式,主要是复用和扩展
复用相同代码。
扩展子类业务实现。
当发现多个类有同样的逻辑及执行流程时,可以抽象出模板,应用模板方法模式。
例如,文件上传,分为三步,1.校验,2.修改数据库状态,3.上传对象存储
public abstract class AbstractClass {
public final void templateMethod() {
//...
method1();
//...
method2();
//...
}
protected abstract void method1();
protected abstract void method2();
}
public class Class1 extends AbstractClass {
@Override
protected void method1() { }
@Override
protected void method2() { }
}
public class Class2 extends AbstractClass {
@Override
protected void method1() { }
@Override
protected void method2() { }
}
策略模式
定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端。
发现 if else 特别多的时候,策略模式该登场了。
public interface Strategy {
void algorithmInterface();
}
public class StrategyA implements Strategy {
@Override
public void algorithmInterface() {
//具体的算法...
}
}
public class StrategyB implements Strategy {
@Override
public void algorithmInterface() {
//具体的算法...
}
}
之后建一个策略工厂类,通过策略工厂,获取策略,然后执行。
责任链模式
将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。
职责链模式变体,请求会被所有的处理器都处理一遍,不存在中途终止的情况。这种变体有两种实现方式:用链表存储处理器和用数组存储处理器
例如 Mybatis 中的插件机制初始化所有插件的时候,就是责任链。以及Zuul中的 filter。
责任链模式,在不修改框架源码的情况下,基于职责链模式提供的扩展点,来扩展新的功能,实现开闭原则。
实现不用特别拘束,用链表和数组存储 链的子类或接口实现,然后从头开始调用即可。
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;
}
}
}
}
状态模式
状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中
状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。
事件也称为转移条件(Transition Condition)。
事件触发状态的转移及动作的执行。
关键是 双向引用 , 状态机和具体状态至今互相依赖,因为需要互相调用,状态变化,要修改状态机的数据,而状态机又要去调用当前状态应执行的逻辑。
public interface IState { //所有状态类的接口
State getName();
// 以下是定义的事件
void obtainOne();
void obtainTwo();
}
public class AState implements IState {
private MarioStateMachine stateMachine;
public AState(StateMachine stateMachine) {
this.stateMachine = stateMachine;
}
@Override
public State getName() {
return State.A;
}
@Override
public void obtainOne() {
stateMachine.setCurrentState();
stateMachine.setScore();
}
@Override
public void obtainTwo() {
stateMachine.setCurrentState();
stateMachine.setScore();
}
}
public class BState implements IState {
// ..
}
public class StateMachine {
private int score;
private IState currentState; // 不再使用枚举来表示状态
public StateMachine() {
this.score = 0;
this.currentState = new DefaultState(this);
}
public void obtainOne() {
this.currentState.obtainOne();
}
public void obtainTwo() {
this.currentState.obtainTwo();
}
public void setCurrentState(IState currentState) {
this.currentState = currentState;
}
}
解释器模式
解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
常用语自定义解析规则,解耦规则。
解释器模式的代码实现比较灵活,没有固定的模板。
就是将语法解析的工作拆分到各个小类中,以此来避免大而全的解析类。
一般的做法是,将语法规则拆分一些小的独立的单元,然后对每个单元进行解析,最终合并为对整个语法规则的解析。
不是特别常用。
中介模式
中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
通过引入中介这个中间层,将一组对象之间的交互关系(或者说依赖关系)从多对多(网状关系)转换为一对多(星状关系)。原来一个对象要跟 n 个对象交互,现在只需要跟一个中介对象交互
处是中介类有可能会变成大而复杂的类。
只有当参与者之间的交互关系错综复杂,维护成本很高的时候,我们才考虑使用中介模式
比方说 A 调用 B和 C, B 又调用 C 和 D 等等,给这种关系,搞到一个中介者中。
迭代器模式
一般不会自行实现,Java 常见就是 集合框架的 Iterator 。
访问者模式
允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。
访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。但是它们继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。
public abstract class ResourceFile {
abstract public void accept(Visitor vistor);
}
public class PdfFile extends ResourceFile {
}
public interface Visitor {
void visit(PdfFile pdfFile);
void visit(PPTFile pdfFile);
void visit(WordFile pdfFile);
}
public class Extractor implements Visitor {
}
public class Compressor implements Visitor {
}
// 使用
Extractor extractor = new Extractor();
for (ResourceFile resourceFile : resourceFiles) {
resourceFile.accept(extractor);
}
Compressor extractor = new Compressor();
for (ResourceFile resourceFile : resourceFiles) {
resourceFile.accept(extractor);
}
备忘录模式
用来防丢失、撤销、恢复等
在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。
主要就是用栈,存储上次副本。
命令模式
命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。
常用于游戏开发,系统之间交互,eureka server之间同步,多层封装,批量同步,其实也是命令模式。
落实到编码实现,命令模式用到最核心的实现手段,就是将函数封装成对象。
就是要做什么的意图,封装为对象。
public interface Command {
void execute();
}
public class ACommand implements Command {
@Override
public void execute() {
// 执行相应的逻辑
}
}
// 下边的代码可以理解为 服务端收到请求之后 一直轮询处理。
for (Request request : requests) {
Event event = request.getEvent();
Command command = null;
if (event.equals(Event.A)) {
command = new ACommand(/*数据*/);
} else if (event.equals(Event.B)) {
command = new BCommand(/*数据*/);
} // ...一堆else if...
queue.add(command);
Command command = queue.poll();
command.execute();
命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等。