单例模式+简单工厂模式+模板方法模式在实际场景中的应用

单例模式

单例的概念

  • 单例类只能有一个实例。
  • 单例类必须自己创建自己的唯一实例。
  • 单例类必须给所有其他对象提供这一实例。

实现方法

懒汉模式

//非线程安全
public class Singleton1 {
    private Singleton1() {
    }
    private static Singleton1 single = null;
    public static Singleton1 getInstance() {
        if (single == null) {
            single = new Singleton1();
        }
        return single;
    }
}
//线程安全 锁 性能低
public class Singleton2 {
    private static Singleton2 instance = null;
    private Singleton2() {
    }
    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

饿汉模式

//静态常量 线程安全 可能浪费资源
public class Singleton1 {
    private static final Singleton1 single = new Singleton1();
    private Singleton1() {
    }
    public static Singleton1 getInstance() {
        return single;
    }
}

静态内部类

//静态内部类,线程安全,推荐
//利用了classloader的机制来保证初始化instance时只有一个线程
public class Singleton3 {
    private static class LazyHolder {
        private static final Singleton3 INSTANCE = new Singleton3();
    }
    private Singleton3() {
    }
    public static Singleton3 getInstance() {
        return LazyHolder.INSTANCE;
    }
}

工厂模式

工厂模式的概念

又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

工厂模式结构

  • Factory :工厂角色负责实现创建所有实例的内部逻辑

  • Product :抽象产品是所创建的所有对象的父类,负责描述所有实例所共有的公共接口

  • ConcreteProduct :具体产品角色是创建的目标,所有创建的对象都充当这个角色的某个具体类的实例。

  • 结构图

    • 结构图

优势

将对象的创建和对象本身业务处理分离可以降低系统的耦合度,使得两者修改起来都相对容易。
在调用工厂类的工厂方法时,由于工厂方法是静态方法,使用起来很方便,可通过类名直接调用,而且只需要传入一个简单的参数即可,在实际开发中,还可以在调用时将所传入的参数保存在XML等格式的配置文件中,修改参数时无须修改任何源代码。

模板方法模式

模板方法模式概念

定义一个操作中算法的骨架,而将这些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。

模板方法模式结构

  • Abstract Template 抽象模板
    • 定义了一个或多个抽象操作,以便让子类实现。
    • 定义并实现了一个模板方法。这个模板方法一般是一个具体方法,它给出了一个顶级逻辑的骨架,而逻辑的组成步骤在相应的抽象操作中,推迟到子类实现。顶级逻辑也有可能调用一些具体方法。
  • Concrete Template 具体模板
    • 实现父类所定义的一个或多个抽象方法。
    • 每一个抽象模板角色都可以有任意多个具体模板角色与之对应,而每一个具体模板角色都可以给出这些抽象方法(也就是顶级逻辑的组成步骤)的不同实现,从而使得顶级逻辑的实现各不相同。
  • 结构图
    • 结构图

实际的应用

关于 view 短信服务代码的启发

现有代码

/**
 * 发送短信
 *
 * @param phone       电话
 * @param content     内容
 * @param mobileStat  短链接短信是否统计 默认false不统计(只对云片平台有效)
 * @param smsPlatform 短信发送平台 1:云片 2:三通
 * @param isBatchSend 是否是批量发送 0:false 1:true
 * @return
 */
public ResultInfo sendSmsWithText(String phone,
                                  String content,
                                  String mobileStat,
                                  String smsPlatform,
                                  String isBatchSend) {
    return smsService.sendSmsWithText(phone, content, mobileStat, smsPlatform, isBatchSend);
}

...

if (smsPlatform.equals("1")) {
    //todo 云片
} else {
    //todo 三通
}

发现的问题

  • 每增加一个渠道,就要修改主流程的逻辑,代码变得越来越难以维护
  • 代码耦合高,牵一发而动全身
  • 如果某一短信渠道挂掉,不得不紧急上线修改

解决问题的思路

  • 云片和三通做的事情都是发送短信,他们的输入输出应该保持一致,即可抽象出短信 Product

  • 每一个短信渠道它只关心自己的内部实现,而不关心外部的事情,程序只要正确的输入输出即可。不难发现,每个短信渠道 实则是一个 ConcreteProduct 具体的产品, ConcreteProduct 之间应该是完全解耦的。

  • 有了 ConcreteProduct 必然要有短信 Factory ,即每个短信渠道 ConcreteProduct 的创建工厂。

  • 发送短信的整个流程大致是固定的,即检查参数,发送短信,解析结果。模板方法的也就可以引进。

  • Factory 在实际中确保唯一,即保持单例。ConcreteProduct 也应确保单例。

开始改善

名词解释

  • Factory 短信工厂
  • Product 短信抽象产品
  • ConcreteProduct 短信具体产品(渠道)
  • Abstract Template 短信抽象模板

创建 Product ConcreteProduct

短信抽象产品 Product

/**
 * 短信接口
 *
 * @author cuishilei
 * @date 2018/9/26
 */
public interface SmsProduct {

    String send(Map param);

}

短信渠道实例 ConcreteProduct

/**
 * @author cuishilei
 * @date 2018/9/26
 */
public class SanTongSmsProduct implements SmsProduct {
    @Override
    public String send(Map param) {
        //三通短信发送的业务逻辑
        return "success";
    }
}

/**
 * @author cuishilei
 * @date 2018/9/26
 */
public class YunPianSmsProduct implements SmsProduct {
    @Override
    public String send(Map param) {
        //云片短信发送的业务逻辑
        return "success";
    }
}

引入模板方法

相同的业务的不同渠道做业务时可能会有统一的处理方法,比如参数检查、渠道状态检查、记流水等。此时可以引入模板方法:

public abstract class AbstractSmsProduct implements SmsProduct {

    @Override
    public String send(Map param) {
        //1.参数检查
        System.out.println("---参数检查---");
        //2.入流水
        System.out.println("---入流水---");
        //3.执行
        doSend(param);
        //4.后续操作
        System.out.println("---更新流水状态---");
        return "success";
    }

    abstract String doSend(Map param);

}

public class YunPianSmsProductV2 extends AbstractSmsProduct {

    @Override
    String doSend(Map param) {
        //云片短信发送的业务逻辑
        System.out.println("---云片发送短信---");
        return "success";
    }
}

public class SanTongSmsProductV2 extends AbstractSmsProduct {

    @Override
    String doSend(Map param) {
        //三通短信发送的业务逻辑
        System.out.println("---三通发送短信---");
        return "success";
    }
}

创建 Factory

初步设计

public class SmsFactory {

    public static SmsProduct getProduct(String arg) {
        SmsProduct product = null;
        if ("A".equalsIgnoreCase(arg)) {
            product = new SanTongSmsProductV2();
        } else if ("B".equalsIgnoreCase(arg)) {
            product = new YunPianSmsProductV2();
        }
        return product;
    }

}
SmsFactory 的问题
  1. Factory 每增加一个 Product ,则要改变一下 Factory 的生产方法。
  2. Product 是 new 出来的,不是单例。

再次升级

试想一下有没有一种办法让 ConcreteProduct 自动注册到 Factory 。
联想到 java 开发的主流框架 spring 对 java bean 的管理,我们可以借助 spring 管理来让 ConcreteProduct 主动注册到 Factory 。

升级 Factory

  • 注册到 Factory 中必须有个标识 key 来表明这个是“三通”、那个是“云片”,很熟悉 key-value
  • Factory 不在使用静态类静态方法而是使用单例模式

升级后的 Factory 如下:

public class SmsFactory {
    private static SmsFactory instance = null;

    private static Map<String, SmsProduct> productMap = null;

    private SmsFactory() {
        productMap = new HashMap<>();
    }

    private static class LazyHolder {
        private static final SmsFactory INSTANCE = new SmsFactory();
    }

    public static SmsFactory getInstance() {
        return LazyHolder.INSTANCE;
    }

    /**
     * 渠道注册
     *
     * @param key
     * @param product
     * @author cuishilei
     * @date 2018/10/31
     */
    public void register(String key, SmsProduct product) {
        productMap.put(key, product);
    }

    /**
     * 获取渠道实例
     *
     * @param key
     * @return com.lalala.cc.sms.SmsProduct
     * @author cuishilei
     * @date 2018/10/31
     */
    public SmsProduct getProduct(String key) {
        return productMap.get(key);
    }

}

升级 ConcreteProduct

ConcreteProduct 需要自动注册进 Factory ,这里我们以 javaEE spring 来举例

@Service
public class YunPianSmsProductV2 extends AbstractSmsProduct {

    @PostConstruct
    private void init() {
        SmsFactory.getInstance().register("SMS_YUNPIAN", this);
    }

    @Override
    String doSend(Map param) {
        //云片短信发送的业务逻辑
        System.out.println("---云片发送短信---");
        return "success";
    }
}

@Service
public class SanTongSmsProductV2 extends AbstractSmsProduct {

    @PostConstruct
    private void init() {
        SmsFactory.getInstance().register("SMS_SANTONG", this);
    }

    @Override
    String doSend(Map param) {
        //三通短信发送的业务逻辑
        System.out.println("---三通发送短信---");
        return "success";
    }
}

可以看到关键就在 @PostConstruct 标签下的方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器调用一次。这样我们就可以通过下面的代码

SmsFactory.getInstance().register("SMS_SANTONG", this);

来“注册”进 Factory 。

测试一下

代码到这里差不多就完成了,我们来测试一下。

项目目录结构如下:

结构图

测试类中的方法:

@Service
public class TestService {

    void test() {
        System.out.println("开始执行发送短信");
        SmsProduct product = SmsFactory.getInstance().getProduct("SMS_YUNPIAN");
        if (product == null) {
            System.out.println("渠道实例不存在,请检查");
        } else {
            product.send(new HashMap());
        }
        System.out.println("结束执行发送短信");
    }
}

执行结果:

结构图

动态路由

作为第三方服务的短信渠道是不可以完全相信的,这就是为什么接这些渠道必须大于等于2的原因。那如何在某一个渠道挂掉的情况下让程序不在走这个渠道呢,可以通过数据库配置来实现:

@Service
public class TestService {
    @Resource
    private SmsFactoryV2 smsFactoryV2;
    @Resource
    private SmsDao smsDao;

    void test() {
        System.out.println("开始执行发送短信");

        //从数据库获取渠道 key
        String avaliableSmsKey = smsDao.queryAvaliableSmsKey();

        SmsProduct product = SmsFactory.getInstance().getProduct(avaliableSmsKey);
        if (product == null) {
            System.out.println("渠道实例不存在,请检查");
        } else {
            product.send(new HashMap());
        }
        System.out.println("结束执行发送短信");
    }
}

动态路由可以设计的很复杂,可以通过渠道流水的数据统计分析达到动态切换的目的,这里不做叙述了。

总结

到这里代码就改善完成了,这样下来的好处有什么呢?

  • 短信工厂建立完毕以后极少的修改
  • 短信渠之间的完全解耦
  • 动态路由的敏捷性好处

一张图表示一下

一张图表示一下

完全的交给 spring 管理

Factory 可以这样升级

@Service
public class SmsFactoryV2 {
    @Resource
    private SmsProduct[] products;

    private Map<String, SmsProduct> serviceMap = null;

    @PostConstruct
    public void init() {
        serviceMap = new HashMap<>();
        for (SmsProduct service : products) {
            serviceMap.put(service.key(), service);
        }
    }

    public SmsProduct getService(String key) {
        return serviceMap.get(key);
    }

}

Product 可以这样升级

public interface SmsProduct {

    String send(Map param);

    String key();

}

@Service
public class SanTongSmsProductV2 extends AbstractSmsProduct {

    @Override
    String doSend(Map param) {
        //三通短信发送的业务逻辑
        System.out.println("---三通发送短信---");
        return "success";
    }

    @Override
    public String key() {
        return "SMS_SANTONG";
    }
}

结束!


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 rockeycui@163.com

文章标题:单例模式+简单工厂模式+模板方法模式在实际场景中的应用

文章字数:2.6k

本文作者:崔石磊(RockeyCui)

发布时间:2018-10-11, 18:46:54

原始链接:https://cuishilei.com/单例模式-简单工厂在实际场景中的应用.html

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏