设计模式也算是面试高频考点,理解典型的设计模式,有利于我们提高沟通、设计的效率和质量,在某种程度上也代表了一些特定情况的最佳实践。
设计模式的分类
大致上按照应用目标进行分类,可分为创建型模式、结构型模式、行为型模式。
- 创建型模式:对对象创建过程中的各类问题和解决方案的总结,包含工厂模式、单例模式、构建器模式、原型模式。
- 结构型模式:针对软件设计结构的总结,关于类、对象继承、组合方式的实践经验。譬如:桥接模式、适配器模式、装饰者模式、代理模式、组合模式、外观模式、享元模式。
- 行为型模式:类和对象之间交互、职责划分等角度。譬如:策略模式、解释器模式、命令模式、观察者模式、迭代器模式、访问者模式、模板方法模式。
常见设计模式举例
- 装饰者模式
- 譬如InputStream是一个抽象类,其子类有FileInputStream、ByteArrayInputStream等。分别从不同的角度对其进行功能那个扩展,这是典型的装饰者模式的应用案例。识别装饰者模式,可以通过识别类设计特征进行判断,也就是其类构造函数以相同的抽象类或者接口为输入参数。实质上装饰者模式是包装同类型实例,我们对目标对象的调用,往往会通过包装类覆盖过的方法,迂回调用被包装的实例,这就可以很自然地实现增加额外逻辑的目的,也就是所谓的“装饰”。例如:BufferedInputStream 经过包装,为输入流过程增加缓存,类似这种装饰器还可以多次嵌套,不断地增加不同层次的功能。
public BufferedInputStream(InputStream in)
- 请看下面的类图,关于InputStream的装饰模式实践。
- 工厂模式
- 创建型模式尤其是工厂模式,在代码中随处可见。这里我举个典型的构建器模式,即Builder。譬如在JDK最新版本的HTTP/2 Client API中的HttpRequest的创建过程,通常会被实现成fluent风格的API,也被称为方法链。
HttpRequest request = HttpRequest.newBuilder(new URI(uri))
.header(headerAlice, valueAlice)
.headers(headerBob, value1Bob,
headerCarl, valueCarl,
headerBob, value2Bob)
.GET()
.build();
- 使用构建器模式,可以比较优雅解决构建复杂对象的麻烦,复杂度高是指类似需要输入的参数组合较多,如果用构造函数,我们往往需要为每一种可能的输入参数组合实现相应的构造函数,一系列复杂的构造函数会让代码阅读性和可维护性变得很差。
- 上面的分析也反映了创建型模式的初衷,即把对象的创建过程单独抽离出来,在结构上把对象使用逻辑和创建逻辑互相独立,隐藏对象实例的细节实现,进而实现更加规范化的逻辑。
- 典型的设计模式的实现,考察代码的基本功。
- 首先我们实现一个日常非常熟悉的单例设计模式,看似很简单。
public class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
总感觉有啥不妥,因为Java会自动为没有明确声明构造函数的类定义一个public 的无参构造函数,所以上面的代码不能保证没有额外的对象被创建出来,因为别人完全可以直接new一个Singleton出来。
那么应该如何处理呢?可以给此类定义一个private的构造函数,这样就不存在额外对象被创建出来的风险。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
还有什么改进的方式吗?在ConcurrentHashMap 中,提到过标准类库中很多地方使用懒加载(lazy-load),改善初始内存开销,单例同样适用,下面是修正后的改进版本。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面的实现在单线程环境不存在问题,但如果出现在并发环境中,则需要考虑线程安全,最熟悉的莫过于双重锁机制。
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton() {
}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
- 上面的 volatile 能够提供可见性,以及保证 getInstance 返回的是初始化完全的对象。
- 在同步之前进行 null 检查,以尽量避免进入相对昂贵的同步块。
- 直接在 class 级别进行同步,保证线程安全的类方法调用。
- 上面的代码中,争议较多的是volatile修饰静态变量,当Singleton类本身有很多成员变量时,需要保证初始化过程完成后才能被get到。
- 而在现在的Java体系的JMM中,通过volatile 的write和read能保证所谓的happen-before原则,也就是保证能够避免提到的指令重排序现象。也就是说,对象的store指令可以保证一定在read之前完成。
当然也有人建议使用内部类持有静态对象的方式实现,其理论依据是对象初始化过程中隐含的初始化锁。这和前面提到的双重锁机制都能保证线程安全,不过可读性不够强。具体实现如下:
public class Singleton {
private Singleton(){}
public static Singleton getSingleton(){
return Holder.singleton;
}
private static class Holder {
private static Singleton singleton = new Singleton();
}
}
综上所述,即使是简单的单例模式,在增加了高标准的需求之后,同样需要非常多的实现思考。但是在我们Java核心类库张中,譬如RunTime类。其源码中并没有使用复杂的双重锁机制检查,静态实例被声明为final,这是通常实践所忽略的,一定程度上保证了实例不被篡改。
private static final Runtime currentRuntime = new Runtime();
private static Version version;
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
那么主流开源框架中具体是怎么使用设计模式的呢?
- BeanFactory和ApplicationContext使用了工厂模式。
- Bean的创建过程中,不同的scope定义的对象,提供了单例模式和原型模式。
- AOP的实现过程中使用了代理模式,装饰者模式,适配器模式。
- 各种事务的监听器采用的是观察者模式。
- 而类似JdbcTemplate则是用了模板模式。