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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 设计模式 - 创建型模式_抽象工厂模式


创建型模式

创建型模式提供创建对象的机制, 能够提升已有代码的灵活性和可复⽤性。

类型 实现要点
工厂方法 定义⼀个创建对象的接⼝,让其⼦类⾃⼰决定实例化哪⼀个⼯⼚类,⼯⼚模式使其创建过程延迟到⼦类进⾏。
抽象工厂 提供⼀个创建⼀系列相关或相互依赖对象的接⼝,⽽⽆需指定它们具体的类。
建造者 将⼀个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示
原型 ⽤原型实例指定创建对象的种类,并且通过拷⻉这些原型创建新的对象。
单例 保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。

概述

抽象⼯⼚模式与⼯⼚⽅法模式虽然主要意图都是为了解决,接⼝选择问题。但在实现上,抽象工厂是⼀个中心工厂,创建其他工厂的模式。

举个例子:

不同系统内的回⻋换⾏

  1. Unix系统⾥,每⾏结尾只有 <换⾏>,即 \n ;
  2. Windows系统⾥⾯,每⾏结尾是 <换⾏><回⻋>,即 \n\r ;
  3. Mac系统⾥,每⾏结尾是 <回⻋>

IDEA 开发⼯具的差异展示(Win\Mac)

除了这样显⽽易⻅的例⼦外,我们的业务开发中时常也会遇到类似的问题,需要兼容做处理。但⼤部分经验不⾜的开发⼈员,常常直接通过添加 ifelse ⽅式进⾏处理了。


Case

随着业务超过预期的快速发展,系统的负载能⼒也要随着跟上。原有的单机 Redis 已经满⾜不了系统需求。这时候就需要更换为更为健壮的Redis集群服务,虽然需要修改但是不能影响⽬前系统的运⾏,还要平滑过渡过去。

随着这次的升级,可以预⻅的问题会有;

  1. 很多服务⽤到了Redis需要⼀起升级到集群。
  2. 需要兼容集群A和集群B,便于后续的灾备。
  3. 两套集群提供的接⼝和⽅法各有差异,需要做适配。
  4. 不能影响到⽬前正常运⾏的系统。

场景模拟工程

  • 业务初期,单机Redis服务工具类RedisUtils
  • 业务初期,单机Redis服务功能类CacheService接口及其实现类
  • 业务发展,新增的两套Redis集群 EGM、IIR, 作为互备使用

这三套Redis服务在使用上会有一些不同: 接口名称、入参信息等等,这些也是在使用设计模式时需要优化处理的点。


接下来介绍下Redis服务提供的缓存功能,以及初期的使用方法。

【模拟单机服务 RedisUtils】

  • 模拟Redis功能,也就是假定⽬前所有的系统都在使⽤的服务
  • 类和⽅法名称都固定写死到各个业务系统中,改动略微麻烦

【Redis集群服务EGM】

模拟第一个Redis集群服务, 需要注意观察这里的方法名称和入参信息不同。 有点像你mac,我⽤win。做⼀样的事,但有不同的操作。


【Redis集群IIR

这是另外⼀套集群服务,有时候很有可能出现两套服务,这⾥我们也是为了做模拟案例,所以添加两套实现同样功能的不同服务,来学习抽象⼯⼚模式.


综上可以看到,⽬前的系统中已经在⼤量的使⽤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);
    }
}

⽬前的代码对于当前场景下的使⽤没有什么问题,也⽐较简单。但是所有的业务系统都在使⽤同时,需要改造就不那么容易了。因为此时所有的业务系统都有同样的使用方式,所以如果每一个系统都通过硬编码的方式进行改造就不那么容易了。

此时,可以先思考怎样从单体Redis的使用升级到Redis集群的使用。


Bad Impl

如果不从全局的升级改造考虑,仅仅是升级自己的系统,那么最快的方式就是ifelse, 把Redis集群的使用添加进去。 在通过接口添加一个使用类型,判断当下调用Redis该使用哪个集群。

当然了,这种方案可以说非常的不好,因为这样会需要所有的研发人员改动代码升级。 不仅工作量大,而且存在非常高的风险。

此时的只有两个类,类结构⾮常简单。⽽我们需要的补充扩展功能也只是在 CacheServiceImpl中实现。

没有什么是ifelse解决不了的逻辑,如果有就在加⼀⾏!

【 ifelse实现需求】

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);
    }
   .....
   .....
   .....
}

这种方式的代码升级并不复杂,看上去比较简单,主要包括以下几个方面:

  • 给接口添加Redis集群使用类型,以便控制使用哪套集群服务
  • 实现过程⾮常简单,主要根据类型判断是哪个Redis集群。 1 – EGM 集群, 2 — IIR集群
  • 因为要体现升级的过程,所以保留了单体Redis的使用方式, 如果RedisType是不存在的,则使用单机Redis, 这也是一种兼容逻辑,兼容升级过程。
  • 虽然实现简单,但是对使⽤者来说很麻烦了,并且也很难应对后期的拓展和不停的维护。

【单元测试】

接下来我们通过junit单元测试的⽅式验证接⼝服务,强调⽇常编写好单测可以更好的提⾼系统的健壮度。

@Test
    public void test_CacheServiceAfterImpl() {
        CacheService cacheService = new CacheClusterServiceImpl();
        cacheService.set("user_name_01", "小工匠", 1);
        String val01 = cacheService.get("user_name_01", 1);
        logger.info("缓存集群升级,测试结果:{}", val01);
        cacheService.set("user_name_01", "小工匠", 2);
        String val02 = cacheService.get("user_name_01", 2);
        logger.info("缓存集群升级,测试结果:{}", val02);
    }

从结果上看运⾏正常,并没有什么问题。但这样的代码只要到⽣成运⾏起来以后,想再改就很难了 也增加了测试难度和未知风险。



Better Impl (抽象⼯⼚模式重构代码)

接下来使⽤抽象⼯⼚模式来进⾏代码优化,也算是⼀次很⼩的重构

这⾥的抽象⼯⼚的创建和获取⽅式,会采⽤代理类的⽅式进⾏实现。所被代理的类就是⽬前的Redis操作⽅法类,让这个类在不需要任何修改下,就可以实现调⽤集群A和集群B的数据服务。

并且这⾥还有⼀点⾮常重要,由于集群A和集群B在部分⽅法提供上是不同的,因此需要做⼀个接⼝适配⽽这个适配类就相当于⼯⼚中的⼯⼚,⽤于创建把不同的服务抽象为统⼀的接⼝做相同的业务,这里可以参考 ⼯⼚⽅法模型 类型。

【工程结构】

【抽象⼯⼚模型结构】

结合抽象工厂和工程结构和类关系,整个工程可以分为三块:

  • 工厂包(Factory): 代理类和代理类的实现, 主要通过代理类和反射调用的方式获取工厂及方法调用 。 JDKProxy 、 JDKInvocationHandler ,是代理类的定义和实现,这部分也就是抽象⼯⼚的另外⼀种实现⽅式。通过这样的⽅式可以很好的把原有操作Redis的⽅法进⾏代理操作,通过控制不同的⼊参对象,控制缓存的使⽤。
  • 工具包 (util):用于支撑反射方法调用中参数的处理
  • 车间包(workshop):ICacheAdapter ,定义了适配接⼝,分别包装两个集群中差异化的接⼝名称。 EGMCacheAdapter 、 IIRCacheAdapter。 Adapter主要是通过适配器的方式使用两个集群服务。 把这两个集群服务当做不同的车间, 再通过抽象的代理工厂服务把每个车间转换为对应的工厂。 (当然了,抽象工厂并不一定必须使用这种实现方式。 这里使用的代理和反射的方式是为了实现一个中间件服务,给所有需要升级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);
}

这个类的主要作⽤是包装两个集群服务, 前面我们提到这两个集群服务在一些接口名称和入参方面各不相同,所以需要进行适配。 引入适配器后,让所有集群的提供⽅,能在统⼀的⽅法名称下进⾏操作。也⽅⾯后续的拓展。


实现集群适配器接口

  • EGM集群【EGMCacheAdapter】
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);
    }
}

  • IIR集群:【IIRCacheAdapter】
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服务和实现类的情况下,即原来的CacheServiceImpl。 通过一个代理类的方式实现一个集群服务处理类, 就可以非常方便的在Spring等框架中通过注入的方式替换CacheServiceImpl的实现。 这样中间件设计思路的实现方式具备良好的插拔性,并可以达到多组集群同时使用和平滑切换的目的。

getProxy的两个入参

  • Class cacheClazz : 在模拟场景中,不同的系统使用不同的Redis服务名,通过这样的方式编译实例化后的注入操作
  • Class 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集群中的方法

抽象工厂搭建完成了,这部分抽象工厂属于从中间件设计中抽取出来的最核心的内容,实际业务开发中还需要扩充相应的代码。


单元测试

@Test
    public void test_CacheService() throws Exception {
        CacheService proxy_EGM = JDKProxyFactory.getProxy(CacheService.class, EGMCacheAdapter.class);
        proxy_EGM.set("user_name_01", "小工匠");
        String val01 = proxy_EGM.get("user_name_01");
        logger.info("缓存服务 EGM 测试,proxy_EGM.get 测试结果:{}", val01);
        CacheService proxy_IIR = JDKProxyFactory.getProxy(CacheService.class, IIRCacheAdapter.class);
        proxy_IIR.set("user_name_01", "小工匠");
        String val02 = proxy_IIR.get("user_name_01");
        logger.info("缓存服务 IIR 测试,proxy_IIR.get 测试结果:{}", val02);
    }

  • 在测试的代码中通过传⼊不同的集群类型,就可以调⽤不同的集群下的⽅
    法。 JDKProxy.getProxy(CacheServiceImpl.class, new EGMCacheAdapter());获取相应的工厂, 在实际使用过程中交给Spring进行Bean注入,通过这样的方式升级服务几区,就不需要所有的研发人员硬编码了。即使有问题,也可以回退到原有的实现方式里。
  • 如果后续有扩展的需求,也可以按照这样的类型⽅式进⾏补充,同时对于改造上来说并没有改动原来的⽅法,降低了修改成本。 这种可插拔服务易于维护和扩展。

小结

  • 抽象⼯⼚模式,所要解决的问题就是在⼀个产品族,存在多个不同类型的产品(Redis集群、操作系统)情况下,接⼝选择的问题。⽽这种场景在业务开发中也是⾮常多⻅的,只不过可能有时候没有将它们抽象化出来。
  • 当你知道什么场景下何时可以被抽象⼯程优化代码,那么你的代码层级结构以及满⾜业务需求上,都可以得到很好的完成功能实现并提升扩展性和优雅度
  • 这个设计模式满⾜了;单⼀职责、开闭原则、解耦等优点,但如果说随着业务的不断拓展,可能会造成类实现上的复杂度。但也可以说算不上缺点,因为可以随着其他设计⽅式的引⼊和代理类以及⾃动⽣成加载的⽅式降低此项缺点。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
设计模式 架构师 Java
设计模式之 5 大创建型模式,万字长文深剖 ,近 30 张图解!
设计模式是写出优秀程序的保障,是让面向对象保持结构良好的秘诀,与架构能力与阅读源码的能力息息相关,本文深剖设计模式之 5 大创建型模式。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
设计模式之 5 大创建型模式,万字长文深剖 ,近 30 张图解!
|
3月前
|
设计模式 Java
Java设计模式-抽象工厂模式(5)
Java设计模式-抽象工厂模式(5)
|
4月前
|
设计模式 存储 负载均衡
【五】设计模式~~~创建型模式~~~单例模式(Java)
文章详细介绍了单例模式(Singleton Pattern),这是一种确保一个类只有一个实例,并提供全局访问点的设计模式。文中通过Windows任务管理器的例子阐述了单例模式的动机,解释了如何通过私有构造函数、静态私有成员变量和公有静态方法实现单例模式。接着,通过负载均衡器的案例展示了单例模式的应用,并讨论了单例模式的优点、缺点以及适用场景。最后,文章还探讨了饿汉式和懒汉式单例的实现方式及其比较。
【五】设计模式~~~创建型模式~~~单例模式(Java)
|
4月前
|
设计模式 Java
Java 设计模式之谜:工厂模式与抽象工厂模式究竟隐藏着怎样的神奇力量?
【8月更文挑战第30天】在Java编程中,设计模式为常见问题提供了高效解决方案。工厂模式与抽象工厂模式是常用的对象创建型设计模式,能显著提升代码的灵活性、可维护性和可扩展性。工厂模式通过定义创建对象的接口让子类决定实例化哪个类;而抽象工厂模式则进一步提供了一个创建一系列相关或相互依赖对象的接口,无需指定具体类。这种方式使得系统更易于扩展和维护。
44 1
|
4月前
|
设计模式 XML 存储
【三】设计模式~~~创建型模式~~~抽象工厂模式(Java)
文章详细介绍了抽象工厂模式,这是一种创建型设计模式,用于提供一个接口以创建一系列相关或相互依赖的对象,而不指定它们具体的类。通过代码示例和结构图,文章展示了抽象工厂模式的动机、定义、结构、优点、缺点以及适用场景,并探讨了如何通过配置文件和反射机制实现工厂的动态创建。
【三】设计模式~~~创建型模式~~~抽象工厂模式(Java)
|
4月前
|
设计模式 XML 存储
【二】设计模式~~~创建型模式~~~工厂方法模式(Java)
文章详细介绍了工厂方法模式(Factory Method Pattern),这是一种创建型设计模式,用于将对象的创建过程委托给多个工厂子类中的某一个,以实现对象创建的封装和扩展性。文章通过日志记录器的实例,展示了工厂方法模式的结构、角色、时序图、代码实现、优点、缺点以及适用环境,并探讨了如何通过配置文件和Java反射机制实现工厂的动态创建。
【二】设计模式~~~创建型模式~~~工厂方法模式(Java)
|
4月前
|
设计模式 XML Java
【一】设计模式~~~创建型模式~~~简单工厂模式(Java)
文章详细介绍了简单工厂模式(Simple Factory Pattern),这是一种创建型设计模式,用于根据输入参数的不同返回不同类的实例,而客户端不需要知道具体类名。文章通过图表类的实例,展示了简单工厂模式的结构、时序图、代码实现、优缺点以及适用环境,并提供了Java代码示例和扩展应用,如通过配置文件读取参数来实现对象的创建。
【一】设计模式~~~创建型模式~~~简单工厂模式(Java)
|
4月前
|
设计模式 Java C语言
设计模式-----------工厂模式之抽象工厂模式(创建型)
抽象工厂模式是一种创建型设计模式,它提供了一个接口用于创建一系列相关或相互依赖的对象,而无需指定具体类,从而增强了程序的可扩展性并确保客户端只使用同一产品族的产品。
设计模式-----------工厂模式之抽象工厂模式(创建型)
|
4月前
|
设计模式 存储 XML
[设计模式]创建型模式-抽象工厂模式
[设计模式]创建型模式-抽象工厂模式
|
4月前
|
设计模式 测试技术 Go
[设计模式]创建型模式-简单工厂模式
[设计模式]创建型模式-简单工厂模式