文章目录
设计模式学习总结
设计模式的学习主要应该从模式的解决的问题/作用、类图关系、模式的优缺点,以及模式之间区别等方向进行理解。
一、创建模式(5)
单例模式
推荐使用DCL方式或静态嵌套类持有单例引用方式,保证线程安全与延迟加载
DCL的Java实现
class Singleton{
private volatile Singleton instance;
private Singleton(){}
public Singleton getInstance(){
if(instance == null){
synchronized(this){
if(instance == null){
instance = new Singleton();
}
}
}
}
}
需注意由于volatile在jdk1.5之前有bug,此方式不可用
工厂模式
屏蔽创建对象的细节,提供简单的创建接口
抽象工厂
与工厂模式对比,主要区别在于抽象工厂能完成更复杂、一系列对象的创建。例如需要创建欧式风格的家居,其中涉及到欧式桌台、欧式地板、欧式壁画、欧式椅子,可以使用一个抽象工厂来完成创建:
class EuropeanFutureFactory{
public EuropeanTable newEuropeanTable();
public EurepeanFloor newEuropeanFloor();
........
}
建造者模式
创建对象可选参数过多时,避免(a, null, null, null, null)式不优雅且易出错不方便使用的构造方式,可使用建造者模式,常见的构造链builder.a(a).b(b).c©.build()即是构造者模式。
jdk与建造者模式
StringBuilder::append
原型模式
java cloneable接口即是原型模式一种实现,即将克隆创建对象的任务委派给要被克隆的对象本身。
二、结构模式(7)
结构模式是定义类之间交互、关系的设计模式。
代理模式
在某类提供服务前后进行一些额外的工作,常见实现:
interface Service{
void serve();
}
class ServiceA{
public void serve(){...}
}
class ProxyA implements Service{
private Service service = new ServiceA();
public void serve(){
... // do something
service.serve();
... // do something
}
}
常见用途:web拦截器、过滤器、日志打印、aop
装饰模式
装饰模式用于对已有对象进行“装饰”,扩展其行为,常用于兼容老代码。
装饰模式与代理模式
装饰模式与代理模式在实现上非常相似,不同的是,装饰器类中被装饰的对象通常是由客户端传入的。以代理模式中实现举例来说,ProxyA若是一个装饰器类,其构造函数中允许传入Service接口的引用,它可以是ServiceA对象,也可以是ServiceB对象,而非代理模式中已经写死确定要代理某个类的对象实例。
装饰模式与jdk
jdk IO中常见的构造链:
即是装饰模式的例子。
(redhat openjdk 11 FilterInputStream)
public class FilterInputStream extends InputStream {
/***
** The input stream to be filtered.*
**/
protected volatile** InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
}
public class FileInputStream extends InputStream
{...}
FileInputStream即是被装饰基类InputStream的一个派生类,属于被装饰者;FilterInputStream的子类DataInputStream、BufferedInputStream等属于装饰器。
适配器模式
来源于生活常见的电源适配器等适配器概念的引申。当老旧代码只提供接口provideService,他返回A。目前需求需要B,A与B存在共有的信息,可以定义适配器类,调用接口a,并将返回的A中信息用以构造B,并进行返回:
class OldInt{
public A provideService(){
return new A();
}
}
class Adapter{
private B transform(A a){
B b = new B();
... // do something to transform a into b;
return b;
}
public B provideService(){
return transform(new OldInt().provideService());
}
}
享元模式
享元模式解决的是内存占用问题,当对象或对象中属性存在普遍大量重复的情况,在只读模式下可以共用这些对象/属性节省内存。通常定义一个工厂类来判断应该使用已有实例还是新建实例。
jdk与享元模式
Integer等Number类对一定数据范围内对象的缓存、String对字符串的缓存
外观模式
外观模式即将一组类的接口进行组合,给用户提供使用更方便简单的接口。
外观模式与中介模式
其与行为模式中的中介模式比较像。都是有一个类知晓其他类的细节。他们主要区别在于,外观模式将用户与内部对象隔开,而内部对象是不知道外观类的,内部对象之间正常交流;而中介模式将内部对象相互之间隔开解耦,彼此不知晓对方存在,由中介来发号施令。
组合模式
组合模式能够表示树形结构,树中节点类型可以各不相同,但都实现同一接口,任一节点都持有其所有子节点的引用,对该节点调用接口方法时,会对所有子节点调用该方法。举例来说,某部门存在有技术人员、维护人员等工种,他们都实现了雇员接口:
interface Employee{
void work();
}
class Technician implements Employee{
public void work(){
... // work in their way
}
}
class Maintainer implements Employee{
public void work(){
... // work in their way
}
}
这时可以定义一个部门类统一管理他们的行为
class Department implements Employee{
private List<Employee> employee;
public void work(){
employee.forEach(Employee::work);
}
}
组合模式的难点在于实现统一的接口,为了抽象多个类共同特征可能牺牲可理解性。
在gui编程中,一个widget等ui组件通常还可以包含其他widget,这也是一种组合模式。
过滤器模式
过滤器模式允许将单一复杂标准分解为多个简单的标准,并能通过逻辑运算以解耦的方式连接起来。常见实现从Criteria接口开始拓展各过滤条件。JPA Criteria Queries就是一种应用。
三、行为模式(7)
策略模式
在代码常会面临大量的if-else或switch分支选择,不易于维护与阅读。策略模式是解决此类问题的一种办法,将每一种选择作为一个实现了策略接口的策略类,在需要时由使用者传入指定的策略对象。该模式的缺点是,由于策略过多,类也会随之增多;另外使用者需要知道有哪些策略。
策略模式与jdk
jdk中Comparator对象就是常见的策略,另外jdk8以上一些函数式接口如Consumer等也是策略模式的体现。
中介模式
中介模式将类之间关系解耦,只有中介了解类之间相互关系,并据此调控,其他类之间相互并不熟悉,只与中介交互。mvc模式是中介模式的一种应用,model与view职责分离,互不熟悉,只与controller进行交互。
该模式的一个缺点是,中介有变成上帝对象的风险,变得过于复杂难于维护,违反单一职责、最少知识原则。
中介模式与观察者模式
中介模式与外观模式、观察者模式相像,与外观模式的区别已在外观模式一节中叙述。中介模式与观察者模式的主要区别在于,中介模式中类与中介的关系可以是双向的,也可以是单向的;而观察者模式规定了单向关系,一端发布,另一端订阅。
观察者模式
观察者模式主要涉及subject——主题、observer——观察者两个概念。observer可以根据自己感兴趣的subject,到subject处进行注册,subject处事件发生时会将此事件通知给所有已注册的observer。
发布订阅模式
观察者模式的一种实现是发布订阅模式,主要概念有publisher——发布者,broker——经纪人,subscriber——订阅者(对应观察者模式中的observer)三者。发布者通过经纪人发布changes,订阅者在经纪人处注册并订阅自己感兴趣的信息。
消息队列是常见的发布订阅模式实现,这种应用中消息队列充当了broker的身份。
发布订阅模式与观察者模式的主要区别在于发布订阅模式中发布者与订阅者通过经纪人broker解耦,broker统一管理事件的发布与订阅;而观察者模式中观察者自行到发布者处订阅,若有多个发布者与多个订阅者,此时类之间关系就会很复杂。
观察者模式与反应器模式
与观察者模式相似的是反应器模式,它不属于23种设计模式之一,但常用于各种异步IO程序设计中。其也有消息源——分派者(dispatcher)——程序。两者主要区别是:
1.观察者模式中发布者通常只有一个,即事件源是单一的;而反应器模式中事件源并不局限一个,如NIO中事件源可以是各种socket事件;
2.观察者模式,如果使用消息队列实现,那么会有特别明显的时间上先后关系,而反应器模式没有;
3.对于同一事件,观察者模式中可以有多个订阅者收到;而反应器模式中一般只有单个程序进行处理。
模板方法模式
由父类定义整个业务骨架流程,而具体业务各步骤实现由子类实现,一般结构如下:
abstract class ServiceTemplate{
protected void initialize(); // no idea how to initialize
public final void serve(){
before();
}
}
class Service extends ServiceTemplate{
protected void initialize(){
... // do something
}
}
需要由子类实现的方法通常为protected仅有子类可见,骨架方法为final防止覆盖。常见的JDBCTemplate、RedisTemplate即是此模式的应用。另外jdk中如AbstractQueuedSynchronizer、AbstractCollection等Abstract…类也是此模式的应用,如AbstractCollection中有:
public abstract int size();public boolean isEmpty() { return size() == 0;}
isEmpty方法不需要知道,也不知道其子类如何计算size,因为数据结构不同,是否为空的计算方式也迥异。然而isEmpty唯一判断依据就是size=0。
迭代器模式
迭代器模式通过提供高度抽象易用的容器迭代接口,无需使用者了解容器实现细节即可使用。java中ListIterator、cpp中iterator都是此思想。
命令模式
命令模式是实现gui与业务逻辑解耦的一种模式,主要概念有invoker,如在gui中的button等元素、command,如gui中保存、剪切等操作(它并不知道这个操作该如何实现)、具体业务逻辑。该模式的好处在于无需在每一个invoker处都实现操作,如在菜单栏处点击剪切与使用快捷键点击剪切,两者可以使用同一个剪切Command。
保存有command的成员,在触发时调用command类的execute方法。如果需要,command类负责获取一些操作需要的参数,如光标位置等。随后execute方法具体委派给某个业务类来完成任务。
jdk中Thead与Runnable是命令模式的一个应用:
(redhat jdk 11 Thread类)
public class Thread implements Runnable { /* What will be run. */ private Runnable target; @Override public void run() { if (target != null) { target.run(); } } public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } private native void start0(); ...}
此例中,对应关系为:Thread——invoker,Runnable——Command,同时Thread通过start方法,让os回调Runnable的run方法,类似于invoker调用Command的execute方法。Thread类同时实现了Runnable接口与本模式无关,是出于向后兼容的考虑。
访问者模式
假设有需要将一整个部门序列化的需求,该部门可表示为一棵树,每个节点可以是员工也可以是更小的部门。通常该需求可以通过为部门类与员工类编写序列化方法并利用多态与递归完成。然而若该功能并不常用,并且这些类不允许被修改,访问者模式就是为了解决此类问题。
访问者模式将类与对类的操作分离,以上面举例来说,通常实现为以下形式:
interface Visitor { void visitEmployee(Employee e); void visitDepartment(Department d);}interface Visitee { void accept(Visitor visitor);}class Employee implements Visitee { @Override public void accept(Visitor visitor) { visitor.visitEmployee(this); }}class Department implements Visitee { @Override public void accept(Visitor visitor) { visitor.visitDepartment(this); }}
随后对整颗部门树,利用多态调用其accept方法,并传入一个了解如何访问Department与Employee的Visitor即可。访问者模式虽然还是在原有代码上有所增添,但比起一开始的方式已经很少了。
访问者模式与jdk
jdk中FileVisitor、File、Directory、Files::walkFileTree是访问者模式的一个应用,但出于向后兼容考虑,没有在File、Directory类中添加visitor.visitXXX方法,也没有Visitee接口,而是把这部分代码在walkFileTree中实现。
(redhat openjdk 11 Files::walkFileTree)
public static Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor) throws IOException{ try (FileTreeWalker walker = new FileTreeWalker(options, maxDepth)) { FileTreeWalker.Event ev = walker.walk(start); do { FileVisitResult result; switch (ev.type()) { case ENTRY : IOException ioe = ev.ioeException(); if (ioe == null) { assert ev.attributes() != null; result = visitor.visitFile(ev.file(), ev.attributes()); } else { result = visitor.visitFileFailed(ev.file(), ioe); } break; case START_DIRECTORY : result = visitor.preVisitDirectory(ev.file(), ev.attributes()); if (result == FileVisitResult.SKIP_SUBTREE || result == FileVisitResult.SKIP_SIBLINGS) walker.pop(); break; case END_DIRECTORY : result = visitor.postVisitDirectory(ev.file(), ev.ioeException()); if (result == FileVisitResult.SKIP_SIBLINGS) result = FileVisitResult.CONTINUE; break; default : throw new AssertionError("Should not get here"); } ... ...
FileVisitor对应访问者模式中访问者角色:
(redhat openjdk 11 FileVisitor)
public interface FileVisitor<T> { FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException; FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException; FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException;}