创建型模式
创建型模式提供创建对象的机制, 能够提升已有代码的灵活性和可复⽤性。
类型 | 实现要点 |
工厂方法 | 定义⼀个创建对象的接⼝,让其⼦类⾃⼰决定实例化哪⼀个⼯⼚类,⼯⼚模式使其创建过程延迟到⼦类进⾏。 |
抽象工厂 | 提供⼀个创建⼀系列相关或相互依赖对象的接⼝,⽽⽆需指定它们具体的类。 |
建造者 | 将⼀个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示 |
原型 | ⽤原型实例指定创建对象的种类,并且通过拷⻉这些原型创建新的对象。 |
单例 | 保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。 |
概述
抽象⼯⼚模式与⼯⼚⽅法模式虽然主要意图都是为了解决,接⼝选择问题。但在实现上,抽象工厂是⼀个中心工厂,创建其他工厂的模式。
举个例子:
不同系统内的回⻋换⾏
- Unix系统⾥,每⾏结尾只有 <换⾏>,即 \n ;
- Windows系统⾥⾯,每⾏结尾是 <换⾏><回⻋>,即 \n\r ;
- Mac系统⾥,每⾏结尾是 <回⻋>
IDEA 开发⼯具的差异展示(Win\Mac)
除了这样显⽽易⻅的例⼦外,我们的业务开发中时常也会遇到类似的问题,需要兼容做处理。但⼤部分经验不⾜的开发⼈员,常常直接通过添加 ifelse
⽅式进⾏处理了。
Case
随着业务超过预期的快速发展,系统的负载能⼒也要随着跟上。原有的单机 Redis 已经满⾜不了系统需求。这时候就需要更换为更为健壮的Redis集群服务,虽然需要修改但是不能影响⽬前系统的运⾏,还要平滑过渡过去。
随着这次的升级,可以预⻅的问题会有;
- 很多服务⽤到了Redis需要⼀起升级到集群。
- 需要兼容集群A和集群B,便于后续的灾备。
- 两套集群提供的接⼝和⽅法各有差异,需要做适配。
- 不能影响到⽬前正常运⾏的系统。
场景模拟工程
- 业务初期,单机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集群、操作系统)情况下,接⼝选择的问题。⽽这种场景在业务开发中也是⾮常多⻅的,只不过可能有时候没有将它们抽象化出来。
- 当你知道什么场景下何时可以被抽象⼯程优化代码,那么你的代码层级结构以及满⾜业务需求上,都可以得到很好的完成功能实现并提升扩展性和优雅度
- 这个设计模式满⾜了;单⼀职责、开闭原则、解耦等优点,但如果说随着业务的不断拓展,可能会造成类实现上的复杂度。但也可以说算不上缺点,因为可以随着其他设计⽅式的引⼊和代理类以及⾃动⽣成加载的⽅式降低此项缺点。