21天搞定Spring---Spring常用设计模式(day2)

书接上回,我们继续较为深入的分析Spring中常用的一些设计模式.

上一章节,我们主要分析Spring的一些架构原则(其实也是软件设计的6大设计原则),并且较为详细的阐述了工厂模式和代理模式的常用实现方式。这一节主要包含以下内容:

  1. 单例模式
  2. 委派模型(不是23中设计模式的成员)
  3. 策略模式
  4. 模式方法描述

单例模式

单例模式,顾名思义,就是在JVM的整个运行期间,一个类只会存在一个对象。因此叫做单例模式。

如Spring实现的IOC,默认就是单例模式(可以通过修改scope=“prototype”来改变该行为),并且通过三级缓存的架构,完美解决了循环依赖的问题。

单例模式实现的一个基本原则是:定义默认构造方法为private。

  1. 饿汉式单例模式:即不管用不用这个单例对象,都会初始化生成这个单例对象。
public class HungrySingletion {
     
    private static final HungrySingletion instance = new HungrySingletion();
    private HungrySingletion(){
     
    }
    public static HungrySingletion getInstance(){
     
        return HungrySingletion.instance;
    }
}

优点:

  1. 实现及其简单。
  2. 不存在线程安全问题。

缺点:

  1. 无论用不用该单例对象,都会默认初始化,造成资源浪费。

是否推荐:不推荐。

  1. 懒汉式单例模式:即只有在用到该单例对象时,才会初始化,如果用不到,默认不初始化。
public class UnsafeLazySingleton {
     
    private static UnsafeLazySingleton instance = null;
    private UnsafeLazySingleton(){
     
    }
    public static UnsafeLazySingleton getInstance(){
     
        if (instance == null){
      // Step1
            instance = new UnsafeLazySingleton(); // Step2
        }
        return instance;// Step3
    }
}

优点:

  1. 设计简单
  2. 用到时才会初始化,节约资源。

缺点:

  1. 存在线程安全问题

是否推荐:

  1. 单线程程序:推荐
  2. 多线程程序:禁用

也许有不了解多线程的小伙伴不清楚为什么会线程不安全,我简单介绍一下。注意上面程序注释Step的一行

时间节点 thread-1 thread-2
t1 执行到Step1:判断instance == null为true 为获得CPU时间片
t2 失去CPU时间片 获得CPU时间片:执行Step1语句:判断instance == null为true
t3 失去CPU时间片 继续执行Step2和Step3,返回一个对象A
t4 获得CPU时间片:执行Step2,Step3 失去CPU时间片
t5 返回,返回对象B
  1. 同步代码块懒汉式:即通过synchronized关键字,实现同步代码块,来解决多线程问题。
public class BadSafeLazySingleton {
     
    private static BadSafeLazySingleton instance= null;
    private BadSafeLazySingleton(){
     
    }
    public static synchronized BadSafeLazySingleton getInstance(){
     
        if (instance == null){
     
            instance = new BadSafeLazySingleton();
        }
        return instance;
    }
}

优点

  1. 设计简单
  2. 用到时才会初始化,节约资源。
  3. 线程安全

缺点:

  1. 无论instance是否为null,都需要加锁。高并发情况下,效率会很低。

是否推荐:

  1. 并发较低时:推荐
  2. 高并发情况下:不推荐
  1. 双重检查锁懒汉式:通过对instance做两次null判断,以此消除同步代码块懒汉式无法适应高并发的问题。
public class LikeSafeLazySingleton {
     
// 注意这两行代码:一个多了volatile关键字
// 如果不加volatile关键字,则由于new 一个对象时,可能发生指令重排序,导致另一个线程获取到一个未初始化完成的对象。
//    private static LikeSafeLazySingleton instance = null;
    private static volatile LikeSafeLazySingleton instance = null;
    
    private LikeSafeLazySingleton(){
     
    }

    public static LikeSafeLazySingleton getInstance(){
     
    // 先进行第一次判断instance是否为null,不为null,这直接返回
        if (instance == null){
     
        // 同步阻塞,执行初始化过程
            synchronized (LikeSafeLazySingleton.class){
     
            // 再次判断instance是否为null,避免第一次判断为null后,另一个线程初始化了instance对象
                if (instance == null){
     
                // 初始化instance对象
                    instance = new LikeSafeLazySingleton();
                }
            }
        }
        return instance;
    }
}

优点:

  1. 用到时才会初始化,节约资源。
  2. 线程安全。(注意上面instance定义时的volatile关键字)

缺点:

  1. 设计相对复杂。
  2. 初学者(或者对并发原理不了解的小伙伴)可能会遗漏instance定义时的volatile关键字,造成线程安全问题。

是否推荐:较为推荐

  1. 内部类懒汉式:即通过定义一个静态内部类的方式实现懒汉式单例模式:
public class PlaceholdLazySingleton {
     

    private PlaceholdLazySingleton() {
     
    }
    /*
     * 根据JVM类加载特性,内部类不会再启动时加载,只有调用时,才会加载
     * */
    private static class Placehold {
     
        private static PlaceholdLazySingleton instance = new PlaceholdLazySingleton();
    }

    public static final PlaceholdLazySingleton getInstance() {
     
        return Placehold.instance;
    }
}

优点:

  1. 用到时才会初始化,节约资源。
  2. 线程安全。

缺点:

  1. 设计相对复杂,需要较为深入理解JVM的类加载原理。

是否推荐:较为推荐

注意:以上单例模式,都无法避免两个问题:

  1. 可以通过反射的方式暴力获取多个对象,从而破坏单例。
  2. 如果单例类实现了Serializable接口,可以通过序列化与反序列化的方式获取到多个对象,从而破坏单例

因此下面介绍一种可以避免反射暴力创建新对象,即序列化与反序列化获取新对象的一种单例模式。即枚举单例模式-----顾名思义,就是通过枚举实现单例模式。

不过通过枚举实现的单例模式,如果反编译字节码分析的话,可以看到,其实也是一种饿汉式单例模式。

public enum EnumSingleton {
     

    INSTANCE;

    public static EnumSingleton getInstance(){
     
        return EnumSingleton.INSTANCE;
    }
}

优点:

  1. 设计简单
  2. 线程安全。

是否推荐:推荐


委派模式

委派模式的基本作用就是负责任务的调用和分配,跟代理模式很像,但是代理模式注重过程,而委派模式注重结果。如SpringMVC中的DispatcherServlet就是典型的委派模式,另外NIO编程中的Reactor模型也是基于IO事件派发的委派模式来完成的。
现在模拟一下经典的IO事件派发模型

  1. 首先,我们需要定义一系列的事件,此处可以通过枚举来表示一个个的IO事件。方便起见,这里只抽象两个事件READ,WRIET
public enum IOEvent {
     
    READ,
    WRITE
}
  1. 其次,我们定义事件的抽象处理器,并针对每个IO事件定制自己的处理逻辑。此处可以通过策略模式或者模板方法模式来实现。此处我们介绍模板方法的实现方式。
// 抽象的模板类:其中定义两个抽象方法
public abstract class AbstractEventHandler {
     
    public abstract IOEvent support();
    public void handler(IOEvent event, Object params){
     
        System.out.println("common code: the event is " + event.name());
        doHandler(params);
    }
    protected abstract void doHandler(Object params);
}

// Read事件的处理器类
public class ReadEventHandler extends AbstractEventHandler {
     
    @Override
    public IOEvent support() {
     
        return IOEvent.READ;
    }
    @Override
    protected void doHandler(Object params) {
     
        System.out.println("I am ReadEventHandler: " + params);
    }
}

// Write事件的处理器类
public class WriteEventHandler extends AbstractEventHandler {
     
    @Override
    public IOEvent support() {
     
        return IOEvent.WRITE;
    }
    @Override
    protected void doHandler(Object params) {
     
        System.out.println("I am WriteEventHandler: " + params);
    }
}
  1. 应用工厂方法,对事件的处理器做一个顶层的抽象,屏蔽底层实现细节。如果是基于Spring的应用,此处可以用Spring的Bean容器来管理。这里我们通过HashMap来管理这些工厂对象。同时结合上面提到的单例实现,实现一个单例的工厂类。
// 此处如果通过Spring,能够更好的管理这些对象
public class EventHandlerFactory {
     

    private final Map<IOEvent, AbstractEventHandler> handlerMapping;
    
    private EventHandlerFactory(){
     
        handlerMapping = new HashMap<>();
        handlerMapping.put(IOEvent.READ, new ReadEventHandler());
        handlerMapping.put(IOEvent.WRITE, new WriteEventHandler());
    }

    private static class Placehold {
     
        public static final EventHandlerFactory INSTANCE = new EventHandlerFactory();
    }

    public static EventHandlerFactory getInstance(){
     
        return Placehold.INSTANCE;
    }
    
    public AbstractEventHandler getHandler(IOEvent event){
     
        return handlerMapping.get(event);
    }
}

最后,进入我们这个委派模式的主角,定义一个委派类,相当于此时需要定义Boss了,这些的底层工人已经准备完毕,等待Boss上场了。

public class EventHandlerDispatcher {
     

    private EventHandlerFactory handlerFactory = EventHandlerFactory.getInstance();

    public void handler(IOEvent event, Object params){
     
        handlerFactory.getHandler(event).handler(event, params);
    }
}
  1. 示例代码:
/*
* common code: the event is READ
* I am ReadEventHandler: drink tea
* common code: the event is WRITE
* I am WriteEventHandler: drink tea
**/
public class DispatcherDemo {
     

    public static void main(String[] args){
     
        String param = "drink tea";
        
        EventHandlerDispatcher dispatcher = new EventHandlerDispatcher();
        dispatcher.handler(IOEvent.READ, param);
        dispatcher.handler(IOEvent.WRITE, param);
    }
}

通过上面的示例,我们可以看到,Boss(派发器)做的事情很少,基本遵循如下几步:

  1. 找到一个小秘(工厂类),然后把任务(Event事件)丢给小秘(工厂类)处理。典型的”有事小秘干,没事…“。
  2. 小秘通过Boss安排的任务类型,选择一个最合适的人选(Handler处理器)来具体的执行这个任务。

策略模式

策略模式是指定义了算法家族,并分别封装起来,让它们之间可以互相替换,此模式使得算法的变化不会影响使用算法的用户。

策略模式的一般使用场景如下:

  1. 系统中有很多类,而它们的区别仅仅在于行为不同。
  2. 一个系统需要动态地在几种算法中选择一种。

下面我将结合我们工作中经常遇到的一个例子来演示代码:

工作中,手机号+密码登录是很常见的例子了,但是随着OAuth2.0协议的兴起,越来越多的公司会选择接入微信,QQ等第三方账号进行快捷登录,但是其实底层我们自己的账号体系还是一直的,在我们自身的账号体系中,还是一个账号。这个时候,就可以很好的通过策略模式来实现:

  1. 定义一个枚举,用于表示用户选择的登录方式。**注意,实际工作中,枚举经常被用来区分策略的类型。**同样,为了方便起见,我们这里只列举微信和QQ两种类型。
public enum  LoginMode {
     
    WECHAT,
    QQ;
}
  1. 定义一个接口,以及各个登录方式的鉴权过程。注意:三方登录其实就是通过OAuth协议,通过http请求的方式,从第三方平台获取用户基本信息,包括用户昵称,性别等。当然还有更重要的就是用户在该第三方平台的唯一标识openId
// 定义用户通过三方鉴权之后获取的信息
@Data
public class AuthInfo {
     
	// 用户唯一标识
    private String openId;
	// 三方账号昵称
    private String userName;
}

// 遵循面向接口编程,定义顶层接口
public interface AuthStrategy {
     

    public LoginMode getMode();

    public AuthInfo auth();
}
// QQ的http鉴权过程
public class QQAuthStrategy implements AuthStrategy {
     
    
    @Override
    public LoginMode getMode() {
     
        return LoginMode.QQ;
    }

    @Override
    public AuthInfo auth() {
     
        System.out.println("通过http请求QQ三方认证平台。。。。");
        AuthInfo authInfo = new AuthInfo();
        authInfo.setOpenId("qq-2222222222");
        authInfo.setUserName("小花^_^");
        return authInfo;
    }
}

// 微信的http鉴权过程
public class WechatAuthStrategy implements AuthStrategy {
     

    @Override
    public LoginMode getMode() {
     
        return LoginMode.WECHAT;
    }

    @Override
    public AuthInfo auth() {
     
        System.out.println("通过http请求微信开发平台。。。。");
        AuthInfo authInfo = new AuthInfo();
        authInfo.setOpenId("wechat-11111111");
        authInfo.setUserName("Xiaoming");
        return authInfo;
    }
}
  1. 和派发模式类似,我们通常也需要建立一个策略的工厂。同样可以通过Spring和HashMap两种方式实现。所以在上一章节中,并没有过多的介绍工厂模式,就是因为工厂模式往往不会单独使用,通常会结合策略模式,模板方法描述等一起使用。
// 此处通过静态工厂的方式提供。同样可以使用Spring更好的管理这些对象
public class AuthStrategyFactory {
     
    
    public static Map<LoginMode, AuthStrategy> strategyMap;
    
    static {
     
        strategyMap  = new HashMap<>();
        strategyMap.put(LoginMode.WECHAT, new WechatAuthStrategy());
        strategyMap.put(LoginMode.QQ, new QQAuthStrategy());
    }
    
    public static AuthStrategy getAuthStrategy(LoginMode mode){
     
        return strategyMap.get(mode);
    }
}
  1. 模拟用户登录场景:
/*
======login by wechat======
通过http请求微信开发平台。。。。
authInfo: AuthInfo(openId=wechat-11111111, userName=Xiaoming)
========login by QQ========
通过http请求QQ三方认证平台。。。。
authInfo: AuthInfo(openId=qq-2222222222, userName=小花^_^)
**/
public class StrategyDemo {
     
    
    public static void main(String[] args){
     
        System.out.println("======login by wechat======");
        AuthInfo wechatInfo = AuthStrategyFactory.getAuthStrategy(LoginMode.WECHAT).auth();
        System.out.println("authInfo: " + wechatInfo);

        System.out.println("========login by QQ========");
        AuthInfo qqInfo = AuthStrategyFactory.getAuthStrategy(LoginMode.QQ).auth();
        System.out.println("authInfo: " + qqInfo);
    }
}

实际应用中,多种策略之间可以继承使用。如Spring中的CglibSubclassingInstantiationStrategy继承了SimpleInstantiationStrategy
优点:

  1. 符合开闭原则
  2. 可避免使用多重条件语句。当遇到使用switch语句时,大概率可以通过策略模式替换。
  3. 使用策略模式还可以提高算法的保密性和安全性。

缺点:

  1. 客户端必须知道所有的策略,并且自行决定使用哪个策略类。
  2. 代码中会产生非常多的策略类,增加了代码的维护难度。

你可能感兴趣的:(Spring,设计模式,java)