In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design. It is not a finished design that can be transformed directly into source or machine code. It is a description or template for how to solve a problem that can be used in many different situations. Design patterns are formalized best practices that the programmer can use to solve common problems when designing an application or system.
总结一下,设计模式就是针对某类问题提出的通用的、可复用的最佳实践。设计模式一共分为三大类,分别是创建型模式、结构型模式、行为型模式。
参考
创建型模式
创建型模式的作用就是用来创建对象,在不同的上下文中以合适的方式创建对象。
创建型模式又分为对象创建型模式和类创建型模式。对象创建型模式把对象创建的一部分推迟到另一个对象中,而类创建型模式将它对象的创建推迟到子类中。
简单工厂模式
定义一个工厂类,根据客户端传入的不同参数,返回不同的对象,返回的对象同属一个父类。
应用场景
工厂类负责创建的对象较少,客户端不需要知道指定类的名字,只需要知道表示该类的一个参数,并提供一个方法,将该参数传入该方法就可以很方便地获取该类的实例。
角色
产品工厂:负责创建所有产品的工厂,负责实现创建所有产品的逻辑
抽象产品:所有具体产品的父类
具体产品:产品工厂具体要创建的产品实例
特性
- 优点
- 简单工厂模式可以分担客户端创建对象的责任,客户端只需要使用对象即可,符合类的单一职责原则,有利于解耦合。
- 缺点
- 当增加新的产品时,需要修改原有工厂代码,不符合开闭原则。
- 工厂类负责了所有对象的创建,当要创建的对象过多的话,业务逻辑过于复杂,职责过重。
具体应用
JDK中的用于格式化时间的DateFormat
1 | public final static DateFormat getDateInstance(); |
工厂方法模式
不同类型的对象使用不同的工厂创建,通过定义一个创建对象的接口,但让实现这个接口的类来决定负责创建哪个类的实例
应用场景
如果在未来可能会增加新的产品类型,此时用简单工厂模式的话,需要修改原有工厂模式中的代码(添加额外的判断逻辑),违反了开闭原则,扩展性不好。使用工厂方法模式的话,只需要添加一个针对此产品类型的工厂即可,符合开闭原则。
角色
- 抽象工厂:定义了创建对象的方法,所有创建对象的具体工厂需要实现这个接口,重写创建对象的方法。
- 具体工厂:用来创建具体类型产品的实例
- 抽象产品:所有具体类型产品的父类
- 具体产品:具体要创建的产品类型,不同类型的产品使用不同的工厂创建
特性
- 优点
- 避免了简单工厂模式中的由单一工厂创建所有对象的职责。符合对扩展开放,对修改关闭的原则。
- 缺点
- 当增加新产品类型时需要定义新的产品类和与之对应的具体工厂类,导致类的数量成对增加,增加了系统的复杂度。
具体应用
JDK中的Iterator
和Collection
。Iterator
相当于一个抽象产品,具体的产品有Itr
、KeyIterator
。Collection
相当于一个抽象工厂,具体的工厂有ArrayList
负责创建具体的Itr
,HashSet
负责创建具体的KeyIterator
。
1 | List iteratorFactory = new ArrayList(); // 具体工厂 |
1 | Set iteratorFactory = new HashSet<>(); // 具体工厂 |
抽象工厂模式
提供一个创建一系列相关或独立对象的接口,而无须指定这些对象的具体类。在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法创建不同类型的产品,这些产品构成了一个产品族。
应用场景
当需要工厂负责创建多个对象时,而不是单一对象。需要用到抽象工厂模式。抽象工厂模式面向的是产品族,通过将一系列的产品组成一个产品族,交给一个具体工厂去创建。
角色
- 抽象工厂: 声明了一组创建一族产品的方法,每一个方法负责创建一种具体类型的产品。
- 具体工厂: 每一个具体工厂负责创建一组产品,这组产品属于一个产品族
- 抽象产品: 某种具体类型产品的父类
- 具体产品: 某种具体的产品
特性
- 优点
- 减小了工作量,避免了工厂方法模式中,每次添加一个新的产品,都需要添加一个具体的工厂类与之对应。
- 会保证客户端使用的都是一个产品族中的产品
- 当增加新的产品族时,不用修改现有的业务逻辑,符合开闭原则。
- 缺点
- 当增加新的产品等级(产品类型),需要修改抽象工厂(添加创建该产品的方法)以及所有的具体工厂,违背了开闭原则。
具体应用
JDK中的java.sql
中的Connection
是抽象工厂,具体的工厂有MysqlCollectionImpl
,里面有创建不同产品中的API,负责创建Statement
、PreparedStatement
、CallableStatement
一系列抽象产品,具体产品有StatementImpl
、PreparedStatementImpl
、 CallableStatementImpl
。
1 | Statement createStatement() throws SQLException; |
单例模式
确保应用中某一个类只有一个该类的实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
应用场景
系统只需要一个实例对象,比如只需要一个工具类对象负责读取配置信息。
角色
- 单例类:负责创建该类的实例
特性
- 优点
- 保证整个应用中只有一个该类的实例,避免了频繁的创建对象和销毁对象,节约系统资源。
- 缺点
- 将创建对象的职责和执行业务逻辑的职责都交给单例对象,违背了单一职责原则。
- 单例模式没有做抽象,所以扩展性比较差。
具体使用
- JDK中的
RunTime
符合单例模式
1 | RunTime runTime = Runtime.getRuntime(); |
- 饿汉式
1 | public class Singleton { |
- 懒汉式
1 | /** |
1 | /** |
1 | /** |
建造者模式
通过抽象一个建造者来完成对复杂对象的属性注入,使得代码的可读性更好。
应用场景
当构建的对象中含有较多的属性时,如果使用构造函数或者setter方法注入的话,整体的代码会比较臃肿。使用建造者模式可以让代码更加优雅一些。
角色
抽象建造者:为创建一个复杂对象的各个组件(属性)指定API,以及一个返回该复杂对象的API
具体建造者:实现了抽象建造者中定义的API
产品:要被构造的复杂对象
特性
- 优点
- 优化了复杂对象的创建等,,,相关的博客说了一堆优点,但是并没有完全理解,不想写了:)
- 缺点
- 如果产品内部变化复杂,此时就需要定义多个建造者来完成对象的创建,使得系统更复杂。
具体应用
JDK中的DocumentBuilder
1 | public abstract void setEntityResolver(EntityResolver er); |
原型模式
克隆一个已有的对象。
应用场景
当需要某个对象作为模板时,只需要在基于这个对象克隆出的对象的基础上,做少量修改就可以得到想要的对象。比如现实中的周报,可以提供一个模板,每个人在这个模板的基础上做少量修改就可以。
具体应用
JDK中的clone()
可以实现原型模式,不过主要注意的是默认是浅拷贝,如果要克隆的对象含有非原始类型的属性需要重写clone()
,实现深拷贝。 另一个就是clone()
默认是由protected
修饰,只有子类和当前包的类可以访问,如果要让其它类访问的话,需要将protected
修改为public
。最后就是被克隆的对象要实现Cloneable
接口:)
结构型模式
通过将类组织成不同的结构,达到解耦合或者是做类增强功能。
代理模式
创建目标对象的一个代理对象,来代替目标对象接收客户端请求,代理对象内部维护了目标对象的引用,最终调用目标对象完成客户端的请求,在请求前后可以加上额外的业务逻辑来做对方法进行 “包装” 做增强功能。(TODO:个人理解)
应用场景
对于一些非业务逻辑比如记录日志,开启事务,如果和业务代码写在一块,使得整体的耦合性较高,此时可以通过创建代理对象,在执行目标对象方法之前之后加上记录日志的功能来做类增强。
角色
代理类:负责接口客户端请求,要“表现”的和目标类一样,所以要继承自目标类,不过内部持有目标类的引用
目标类:真正执行客户端的请求
特性
- 优点
- 将业务代码和非业务代码分离,实现松耦合
- 缺点
- 增加了对代理对象的访问,请求会先到达代理对象,然后再到达目标对象,效率会有影响。
具体应用
Spring AOP中用到的是动态代理,通过
@Aspect
定义一个切面,然后通过@PointCut
确定一个切点,在切点前后执行额外的业务逻辑@Advice
,执行的时机可以是@Before
、@After
、@Around
Windows里面的快捷方式就是代理对象
装饰者模式
通过创建装饰器来装饰对象,对已有对象的功能进行扩展,增强。每一个装饰器内部都维护了抽象对象的引用,可以调用抽象对象的方法,并且可以添加额外的增强方法。
应用场景
当不适合用继承的方式对系统进行扩展时,比如系统中存在大量独立的扩展类,如果扩展类之间组合会生成大量的类,此时可以使用装饰者模式
角色
- 抽象组件:是具体组件和抽象装饰器的父类,抽象装饰器内部维护了抽象组件的引用,使得可以装饰所有的具体组件
- 具体组件:要被装饰的具体组件,继承自抽象组件
- 抽象装饰器:内部维护了抽象组件的引用,所以可以调用抽象组件的方法
- 具体装饰器:继承自抽象装饰器,并且内部有新的方法,来对具体组件做增强功能
特性
- 优点
- 当扩展一个对象的功能时,装饰者模式采用类组合来代替继承,不会导致类的数量大幅度增加。
- 缺点
- 当对类装饰以后,无法面向接口(抽象)编程,否则无法使用装饰的功能,因为装饰的功能都在具体的装饰器里面。
具体应用
JDK中的IO框架,抽象组件是InputStream
,具体组件有FileInputStream
,抽象装饰器是FilterInputStram
,具体的装饰器有BufferedInputStream
、DataInputStream
适配器模式
通过引入适配器将不兼容的类适配,让这些类的可以一块工作。
适配器模式分为对象适配器和类适配器两种,对象适配器指适配者和适配器之间是组合的关系,类适配器指适配者和适配器之间是继承的关系。
应用场景
对于已经定义好的接口,但是并没有具体的实现, 不过现有的类可以提供这个接口中定义的功能,但是这个类并没有现成的源代码,比如说只有一个编译好的jar包,此时可以引入一个适配器,适配器实现这个接口,内部持有这个类的引用,通过使用这个类中的功能,间接实现这个接口。
角色
- 被适配者:已经定义好的接口,但是并没有具体的实现
- 适配器:将适配者和被适配者适配到一块
- 适配者:已经有接口中定义的功能,但是和这个接口并不兼容。
特性
- 优点
- 实现类的复用,通过适配器复用已有的可以提供服务但是和被适配者不兼容的适配者类
- 缺点
- 说的没理解:) TODO:继续看
具体应用
JDK中IO框架里的InputStreamReader
将InputStream
适配成Reader
外观(门面)模式
通过创建一个门面角色完成客户端和其它多个子系统复杂的交互流程。
应用场景
当系统中类与类之间的交互关系比较复杂,比较多时,比如客户端需要和多个子系统进行交互,可以引入一个外观角色来和这些子系统进行交互。
角色
- 门面角色:内部持有子系统的引用,将客户端的请求委派给子系统处理。
- 子系统:执行门面角色传过来的客户端的请求
特性
- 优点
- 降低解除了子系统和客户端的耦合性
- 缺点
- 当需要修改和子系统交互的执行流程时,需要修改门面角色的代码,违背了开闭原则。
具体应用
slf4j相当于一个门面,而Log4J、Log-Back就是子系统。
行为型模式
行为型模式重点关注的是类之间的相互作用,将职责划分清楚。
策略者模式
策略者模式是指完成一件事情,在不同的上下文里可以采用不同的策略。具体采用哪种策略根据上下文决定
应用场景
系统可以选择多种算法来完成某件事情,具体选用那种算法可以根据问题的上下文决定
角色
- 上下文:问题的上下文
- 抽象策略:为了给客户端呈现统一的接口
- 具体策略:对于抽象策略的不同实现
特性
- 优点
- 当需要不同的策略时,只需要新创建一个新的策略即可,符合开闭原则。
- 缺点
- 可能一些细微的改动,就得创建新的策略,需要创建的类会急剧增加
具体应用
JDK中ThreadPoolExecutor
中的四种拒绝策略
1 | public interface RejectedExecutionHandler; // 抽象策略 |
观察者模式
观察者通过订阅它感兴趣的事件,当事件发生时会通知之前订阅它的观察者。
应用场景
一个抽象模型有两个方面,其中一个方面依赖于另一个方面,也就是说其它一些对象依赖某一个对象,当这个对象发生改变时,其它这些对象也会跟着变化,也就是一种联动的状态。
角色
- 抽象观察者:定义了具体要做什么动作的方法
- 具体观察者:做了具体的实现
- 抽象观察目标
- 具体目标:当具体目标发生变化时,会通知订阅它的观察者
特性
- 优点
- 观察者模式适合广播通信,观察目标会对所有已经订阅的观察者发送通知
- 缺点
- 如果一个观察目标有很多观察者的话,通知所有的观察者会浪费很多的时间
具体应用
JDK中对观察者模式做了实现,Observer
是观察者 Observerable
是观察目标
责任链模式
将处理请求的多个处理对象链接成一个链,当请求过来以后,会从链头逐个向链中的节点发送请求。
应用场景
一个处理流程需要多个处理人来处理,每个处理人可能有权限处理,可能没权限处理,并且这个处理流程是动态变化的
角色
- 抽象处理者:定义了处理请求的API
- 具体处理者:对API做了实现,如果当前处理者没有权限处理,就转交给下一节点处理
特性
- 优点
- 可以应付一些动态的处理流程,如果新加处理对象后,只需要客户端重新建链即可。
- 缺点
- 处理流程过长会影响效率
具体使用
JDK中的类加载器,当AppClassLoader接到类加载请求以后,会交给它的下一节点ExtensionClassLoader,然后交给BootStrapClassLoader。