谈谈设计模式——以手动实现单例模式和Spring中使用到的设计模式为例

2019-04-14 16:56发布

设计模式也算是面试高频考点,理解典型的设计模式,有利于我们提高沟通、设计的效率和质量,在某种程度上也代表了一些特定情况的最佳实践。
设计模式的分类
大致上按照应用目标进行分类,可分为创建型模式、结构型模式、行为型模式。
  • 创建型模式:对对象创建过程中的各类问题和解决方案的总结,包含工厂模式、单例模式、构建器模式、原型模式。
  • 结构型模式:针对软件设计结构的总结,关于类、对象继承、组合方式的实践经验。譬如:桥接模式、适配器模式、装饰者模式、代理模式、组合模式、外观模式、享元模式。
  • 行为型模式:类和对象之间交互、职责划分等角度。譬如:策略模式、解释器模式、命令模式、观察者模式、迭代器模式、访问者模式、模板方法模式。
常见设计模式举例
  • 装饰者模式
    • 譬如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) { // 同步.class,意味着对同步类方法调用 if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
  1. 上面的 volatile 能够提供可见性,以及保证 getInstance 返回的是初始化完全的对象。
  2. 在同步之前进行 null 检查,以尽量避免进入相对昂贵的同步块。
  3. 直接在 class 级别进行同步,保证线程安全的类方法调用。
  4. 上面的代码中,争议较多的是volatile修饰静态变量,当Singleton类本身有很多成员变量时,需要保证初始化过程完成后才能被get到。
  5. 而在现在的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则是用了模板模式。