学习设计模式之抽象工厂模式

Posted 南淮北安

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习设计模式之抽象工厂模式相关的知识,希望对你有一定的参考价值。

首先需要了解下工厂模式和抽象工厂的区别:

  • 工厂方法模式是生产单个同类型的不同产品,例如戴尔电脑,苹果电脑
  • 而抽象工厂模式生产的是多个不同类型的不同产品,所以必须将共同点抽象出来,例如戴尔CPU,苹果CPU,抽象的接口就是CPU。(再比如:戴尔GPU,苹果GPU,抽象的接口就是GPU)。
    这是为了遵守面向对象的原则之一,面向接口编程而不是内容编程。


简单工厂:提供方法的工厂,比如排队去买面,只有一个窗口,具体什么面交由窗口内打饭阿姨决定,通过 if-else 判断得到
工厂方法:提供工厂的方法,比如买面,买什么面去什么窗口,选择权交由用户决定
组合窗口:提供组合工厂的方法,将不同类型的不同方法的共同点抽取出来进行划分

一、抽象工厂

抽象工厂也可以称作其他工厂的工厂,它可以在抽象工厂中创建出其他工厂,与工厂模式一样,都是用来解决接口选择的问题,同样都属于创建型模式,如图所示,五菱公司既可以生产汽车也可以生产口罩。

二、问题背景

很多初创团队的蛮荒期,并没有完整的底层服务。
团队在初建时业务体量不大,在预估的系统服务 QPS 较低、系统压力较小、并发访问量少、近一年没有大动作等条件下,结合快速起步、时间紧迫、成本投入的因素,并不会投入特别多的研发资源构建出非常完善的系统架构。

就像对Redis的使用,可能最开始只需要一个单机就可以满足现状。但随着业务超预期的快速发展,系统的负载能力也要随之跟上,原有的单机Redis已经无法满足系统的需要。

这时就需要建设或者更换更为健壮的Redis集群服务,在这个升级的过程中是不能停系统的,并且需要平滑过渡。

随着系统的升级,可以预见的问题有如下几种:

  • 很多服务用到了Redis,需要一起升级到集群。
  • 需要兼容集群A和集群B,便于后续的灾备,并及时切换集群。
  • 两套集群提供的接口和方法各有差异,需要进行适配。
  • 不能影响目前正常运行的系统。

虽然升级是必须要做的,但怎样执行却显得非常重要。

该场景工程包含如下信息:

在业务初期,单机Redis服务工具类RedisUtils主要负责的是提供早期 Redis的使用。
在业务初期,单机 Redis 服务功能类 CacheService 接口以及它对应的实现类CacheServiceImpl。
随着后续业务的发展,新增加两套Redis集群EGM、IIR,作为互备使用。

Redis 单机服务 RedisUtils:

/**
 * 模拟最开始使用的Redis服务,单机的。
 */
public class RedisUtils 

    private Logger logger = LoggerFactory.getLogger(RedisUtils.class);

    private Map<String, String> dataMap = new ConcurrentHashMap<String, String>();

    public String get(String key) 
        logger.info("Redis获取数据 key:", key);
        return dataMap.get(key);
    

    public void set(String key, String value) 
        logger.info("Redis写入数据 key: val:", key, value);
        dataMap.put(key, value);
    

    public void set(String key, String value, long timeout, TimeUnit timeUnit) 
        logger.info("Redis写入数据 key: val: timeout: timeUnit:", key, value, timeout, timeUnit.toString());
        dataMap.put(key, value);
    

    public void del(String key) 
        logger.info("Redis删除数据 key:", key);
        dataMap.remove(key);
    


Redis集群服务EGM:

/**
 * 模拟Redis缓存服务,EGM
 */
public class EGM 

    private Logger logger = LoggerFactory.getLogger(EGM.class);

    private Map<String, String> dataMap = new ConcurrentHashMap<String, String>();

    public String gain(String key) 
        logger.info("EGM获取数据 key:", key);
        return dataMap.get(key);
    

    public void set(String key, String value) 
        logger.info("EGM写入数据 key: val:", key, value);
        dataMap.put(key, value);
    

    public void setEx(String key, String value, long timeout, TimeUnit timeUnit) 
        logger.info("EGM写入数据 key: val: timeout: timeUnit:", key, value, timeout, timeUnit.toString());
        dataMap.put(key, value);
    

    public void delete(String key) 
        logger.info("EGM删除数据 key:", key);
        dataMap.remove(key);
    

这里模拟第一个Redis集群服务,需要注意观察这里的方法名称及入参信息,与使用单体Redis服务时是不同的。有点像A用mac系统,B用Windows系统,虽然可以做一样的事,但操作方法不同

Redis集群服务IIR:

/**
 * 模拟Redis缓存服务,IIR
 */
public class IIR 

    private Logger logger = LoggerFactory.getLogger(IIR.class);

    private Map<String, String> dataMap = new ConcurrentHashMap<String, String>();

    public String get(String key) 
        logger.info("IIR获取数据 key:", key);
        return dataMap.get(key);
    

    public void set(String key, String value) 
        logger.info("IIR写入数据 key: val:", key, value);
        dataMap.put(key, value);
    

    public void setExpire(String key, String value, long timeout, TimeUnit timeUnit) 
        logger.info("IIR写入数据 key: val: timeout: timeUnit:", key, value, timeout, timeUnit.toString());
        dataMap.put(key, value);
    

    public void del(String key) 
        logger.info("IIR删除数据 key:", key);
        dataMap.remove(key);
    

这是另一套Redis集群服务,有时在企业开发中可能有两套服务做互相备份。这里也是为了模拟,所以添加两套实现同样功能的不同服务,主要体现抽象工厂模式在这里发挥的作用。

综上可以看到,目前的系统中已经在大量地使用Redis服务,但是因为系统不能满足业务的快速发展,因此需要迁移到集群服务中。而这时有两套集群服务需要兼容使用,又要满足所有的业务系统改造且不能影响线上使用。

接下来介绍在模拟的案例中,对单体Redis服务的使用方式。后续会通过两种方式将这部分代码扩展为使用Redis集群服务。

定义Redis使用接口:

public interface CacheService 

    String get(final String key);

    void set(String key, String value);

    void set(String key, String value, long timeout, TimeUnit timeUnit);

    void del(String key);

实现Redis的使用接口:

public class CacheServiceImpl implements CacheService 

    private RedisUtils redisUtils = new RedisUtils();

    public String get(String key) 
        return redisUtils.get(key);
    

    public void set(String key, String value) 
        redisUtils.set(key, value);
    

    public void set(String key, String value, long timeout, TimeUnit timeUnit) 
        redisUtils.set(key, value, timeout, timeUnit);
    

    public void del(String key) 
        redisUtils.del(key);
    

三、违背设计模式实现

如果不从全局的升级改造考虑,仅仅是升级自己的系统,那么最快的方式是添加if…else,把Redis集群的使用添加进去。

再通过在接口中添加一个使用的Redis集群类型,判断当下调用Redis时应该使用哪个集群。

可以说这样的改造非常不好,因为这样会需要所有的研发人员改动代码升级。不仅工作量非常大,而且可能存在非常高的风险。这里为了对比代码结构,会先用这种方式升级Redis集群服务。


在这个工程结构中只有两个类,一个是定义缓存使用的接口CacheService,另一个是它的实现类CacheServiceImpl。

因为这里选择的是在接口中添加集群类型,判断使用哪个集群,所以需要重新定义接口,并实现新的集群服务类。

if…else实现需求

/**
 * 升级后,使用多套Redis集群服务,同时兼容以前单体Redis服务
 */
public class CacheClusterServiceImpl implements CacheService 

    private RedisUtils redisUtils = new RedisUtils();

    private EGM egm = new EGM();

    private IIR iir = new IIR();

    public String get(String key, int redisType) 

        if (1 == redisType) 
            return egm.gain(key);
        

        if (2 == redisType) 
            return iir.get(key);
        

        return redisUtils.get(key);
    

    public void set(String key, String value, int redisType) 

        if (1 == redisType) 
            egm.set(key, value);
            return;
        

        if (2 == redisType) 
            iir.set(key, value);
            return;
        

        redisUtils.set(key, value);
    

    public void set(String key, String value, long timeout, TimeUnit timeUnit, int redisType) 

        if (1 == redisType) 
            egm.setEx(key, value, timeout, timeUnit);
            return;
        

        if (2 == redisType) 
            iir.setExpire(key, value, timeout, timeUnit);
            return;
        

        redisUtils.set(key, value, timeout, timeUnit);
    

    public void del(String key, int redisType) 

        if (1 == redisType) 
            egm.delete(key);
            return;
        
        if (2 == redisType) 
            iir.del(key);
            return;
        
        redisUtils.del(key);
    

这种方式的代码升级并不复杂,看上去也比较简单。

主要包括如下内容:

  • 给接口添加Redis集群使用类型,以控制使用哪套集群服务。
  • 如果类型是1,则使用EGM集群;如果类型是2,则使用IIR集群,这在各方法中都有所体现。
  • 因为要体现出Redis集群升级的过程,所以这里保留了单体Redis的使用方式。如果用户传递的redisType是不存在的,则会使用RedisUtils的方式调用Redis服务。这也是一种兼容逻辑,兼容升级过程。

测试验证:

public class ApiTest 

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test_CacheServiceAfterImpl() 
        CacheService cacheService = new CacheClusterServiceImpl();

        cacheService.set("user_name_01", "Yolo", 1);
        String val01 = cacheService.get("user_name_01", 1);
        logger.info("缓存集群升级,测试结果:", val01);
    


这样的方式需要整个研发组一起硬编码,不易于维护,也增加了测试难度和未知风险

四、抽象工厂模式重构代码

接下来使用抽象工厂模式优化代码,也是一次代码重构。

在前文介绍过,抽象工厂的实质就是用于创建工厂的工厂。

可以理解为有三个物料加工车间,其中任意两个都可以组合出一个新的生产工厂,用于装备汽车或缝纫机。

另外,这里会使用代理类的方式实现抽象工厂的创建过程。

而两个 Redis 集群服务相当于两个车间,两个车间可以构成两个工厂。

通过代理类的实现方式,可以非常方便地实现Redis服务的升级,并且可以在真实的业务场景中做成一个引入的中间件,给各个需要升级的系统使用。

这里还有非常重要的一点,集群EGM和集群IIR在部分方法提供上略有不同,如方法名和参数,因此需要增加一个适配接口。

最终使用这个适配接口承接两套集群服务,做到统一的服务输出。


抽象工厂代码类关系图:

结合以上抽象工厂的工程结构和类关系,简要介绍这部分代码包括的核心内容。

整个工程包结构分为三块:工厂包(factory)、工具包(util)和车间包(workshop)。

  • 工厂包:JDKProxyFactory、JDKInvocationHandler两个类是代理类的定义和实现,这部分代码主要通过代理类和反射调用的方式获取工厂及方法调用。
  • 工具包:ClassLoaderUtils类主要用于支撑反射方法调用中参数的处理。
  • 车间包:EGMCacheAdapter、IIRCacheAdapter两个类主要是通过适配器的方式使用两个集群服务。把两个集群服务作为不同的车间,再通过抽象的代理工厂服务把每个车间转换为对应的工厂。

这里需要强调一点,抽象工厂并不一定必须使用目前的方式实现。这种使用代理和反射的方式是为了实现一个中间件服务,给所有需要升级 Redis 集群的系统使用。在不同的场景下,会有很多不同的变种方式实现抽象工厂。

定义集群适配器接口:

/**
 * 车间适配器
 */
public interface ICacheAdapter 

    String get(String key);

    void set(String key, String value);

    void set(String key, String value, long timeout, TimeUnit timeUnit);

    void del(String key);


适配器接口的作用是包装两个集群服务,在前面已经提到这两个集群服务在一些接口名称和入参方面各不相同,所以需要进行适配。同时在引入适配器后,也可以非常方便地扩展。

实现集群适配器接口:

public class EGMCacheAdapter implements ICacheAdapter 

    private EGM egm = new EGM();

    public String get(String key) 
        return egm.gain(key);
    

    public void set(String key, String value) 
        egm.set(key, value);
    

    public void set(String key, String value, long timeout, TimeUnit timeUnit) 
        egm.setEx(key, value, timeout, timeUnit);
    

    public void del(String key) 
        egm.delete(key);
    

public class IIRCacheAdapter implements ICacheAdapter 

    private IIR iir = new IIR();

    public String get(String key) 
        return iir.get(key);
    

    public void set(String key, String value) 
        iir.set(key, value);
    

    public void set(String key, String value, long timeout, TimeUnit timeUnit) 
        iir.setExpire(key, value, timeout, timeUnit);
    

    public void del(String key) 
        iir.del(key);
    


如果是两个集群服务的统一包装,可以看到这些方法名称或入参都已经统一。例如,IIR集群的iir.setExpire和EGM集群的egm.setEx都被适配成一个方法名称——set方法。

代理抽象工厂JDKProxyFactory:

public class JDKProxyFactory 

    public static <T> T getProxy(Class<T> cacheClazz, Class<? extends ICacheAdapter> cacheAdapter) throws Exception 
        InvocationHandler handler = new JDKInvocationHandler(cacheAdapter.newInstance());
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        return (T) Proxy.newProxyInstance(classLoader, new Class[]cacheClazz, handler);
    


这里有一点非常重要,就是为什么选择代理方式实现抽象工厂。

因为要把原单体Redis服务升级为两套 Redis 集群服务,在不破坏原有单体Redis服务和实现类的情况下,也就是cn-bugstack-design-5.0-0 的CacheServiceImpl,通过一个代理类的方式实现一个集群服务处理类,就可以非常方便地在Spring、SpringBoot等框架中通过注入的方式替换原有的CacheServiceImpl实现。

这样中间件设计思路的实现方式具备了良好的插拔性,并可以达到多组集群同时使用和平滑切换的目的。

getProxy方法的两个入参的作用如下:

  • Class cacheClazz:在模拟的场景中,不同的系统使用的 Redis 服务类名可能有所不同,通过这样的方式便于实例化后的注入操作。
  • Class<?extendsICacheAdapter>cacheAdapter:这个参数用于决定实例化哪套集群服务使用Redis功能。

反射调用方法JDKInvocationHandler:

public class JDKInvocationHandler implements InvocationHandler 

    private ICacheAdapter cacheAdapter;

    public JDKInvocationHandler(ICacheAdapter cacheAdapter) 
        this.cacheAdapter = cacheAdapter;
    

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
        return ICacheAdapter.class.getMethod(method.getName(), ClassLoaderUtils.getClazzByArgs(args)).invoke(cacheAdapter, args);
    


这部分是工厂被代理实现后的核心处理类,主要包括如下功能:

  • 相同适配器接口 ICacheAdapter 的不同 Redis 集群服务实现,其具体调用会在这里体现。
  • 在反射调用过程中,通过入参获取需要调用的方法名称和参数,可以调用对应Redis集群中的方法。

抽象工厂搭建完成了,这部分抽象工厂属于从中间件设计中抽取出来的最核心的内容,如果需要在实际的业务中使用,则需要扩充相应的代码,如注入的设计、配置的读取、相关监控和缓存使用开关等。

测试验证:

public class ApiTest 

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public #yyds干货盘点#-设计模式分享-抽象工厂模式

设计模式01 创建型模式 - 抽象工厂

设计模式抽象工厂模式 ( 简介 | 适用场景 | 优缺点 | 产品等级结构和产品族 | 代码示例 )

设计模式之抽象工厂模式

抽象工厂模式的优缺点和适用场景

Java设计模式——抽象工厂模式