一、开发环境

  1. JDK 1.8
    2. Idea + Maven

    ⼆、抽象工厂模式介绍

    1631527240(1).png
    抽象工厂模式与工厂方法模式虽然主要意图都是为了解决,接口选择问题。但在实现上,抽象工厂是一
    个中心工厂,创建其他工厂的模式。
    可能在平常的业务开发中很少关注这样的设计模式或者类似的代码结构,但是这种场景确一直在我们身
    边,例如;
    不同系统内的回车换行
    1. Unix系统里,每行结尾只有 <换行>,即 \n ;
    2. Windows系统里面,每行结尾是 <换行><回车>,即 \n\r ;
    3. Mac系统里,每行结尾是 <回车>
    IDEA 开发⼯工具的差异展示(Win\Mac)
    1631527661(1).png
    除了这样显而易见的例子外,我们的业务开发中时常也会遇到类似的问题,需要兼容做处理。但大部分经验不足的开发人员,常常直接通过添加 ifelse 方式进行处理了。

    三、案例例场景模拟

    1631527730(1).png
    很多时候初期业务的蛮荒发展,也会牵动着研发对系统的建设。
    预估 QPS较低 、 系统压力较小 、 并发访问不大 、 近一年没有大动作 等等,在考虑时间投入成本的前提前,并不会投入特别多的人力去构建非常完善的系统。就像对 Redis 的使用,往可能只要是单机的就可以满足现状。
    但随着业务超过预期的快速发展,系统的负载能力也要随着跟上。原有的单机 Redis 已经满足不了系统需求。这时候就需要更换为更为健壮的Redis集群服务,虽然需要修改但是不能影响目前系统的运行,还要平滑过渡过去。
    随着这次的升级,可以预见的问题会有;
    1. 很多服务用到了Redis需要一起升级到集群。
    2. 需要兼容集群A和集群B,便于后续的灾备。
    3. 两套集群提供的接口和方法各有差异,需要做适配。
    4. 不能影响到目前正常运行的系统。

    1. 场景模拟工程

    1631528083(1).png

    2. 场景简述

    2.1 模拟单机服务 RedisUtils

    ```java public class RedisUtils {

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

    private Map dataMap = new ConcurrentHashMap();

    public String get(String key) {

    1. logger.info("Redis获取数据 key:{}", key);
    2. return dataMap.get(key);

    }

    public void set(String key, String value) {

    1. logger.info("Redis写入数据 key:{} val:{}", key, value);
    2. dataMap.put(key, value);

    }

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

    1. logger.info("Redis写入数据 key:{} val:{} timeout:{} timeUnit:{}", key, value, timeout, timeUnit.toString());
    2. dataMap.put(key, value);

    }

    public void del(String key) {

    1. logger.info("Redis删除数据 key:{}", key);
    2. dataMap.remove(key);

    }

}

  1. 模拟Redis功能,也就是假定目前所有的系统都在使用的服务<br />类和方法名次都固定写死到各个业务系统中,改动略微麻烦
  2. <a name="rna94"></a>
  3. ### 2.2 模拟集群 EGM
  4. ![1631528283(1).png](https://cdn.nlark.com/yuque/0/2021/png/22376009/1631528293152-7816cdce-ff4e-45fc-a60f-076b0a07f484.png#clientId=u1e73d8d8-4699-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=455&id=u557e4658&margin=%5Bobject%20Object%5D&name=1631528283%281%29.png&originHeight=455&originWidth=645&originalType=binary&ratio=1&rotation=0&showTitle=false&size=107174&status=done&style=none&taskId=u6a8cd477-ab22-4031-bd77-8dec6fed99d&title=&width=645)<br />模拟一个集群服务,但是方法名与各业务系统中使用的方法名不同。有点像你mac,我用win。做一样的事,但有不不同的操作。<br />2.3 模拟集群 IIR<br />![1631528308(1).png](https://cdn.nlark.com/yuque/0/2021/png/22376009/1631528312835-1d992181-e987-4636-83af-a8b1c6948ff9.png#clientId=u1e73d8d8-4699-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=453&id=u0243af5d&margin=%5Bobject%20Object%5D&name=1631528308%281%29.png&originHeight=453&originWidth=657&originalType=binary&ratio=1&rotation=0&showTitle=false&size=94266&status=done&style=none&taskId=u6eb51eb0-c5fa-41a1-9bbe-53ef65d296b&title=&width=657)<br />这是另外一套集群服务,有时候在企业开发中就很有可能出现两套服务,这里我们也是为了做模拟案例,所以添加两套实现同样功能的不同服务,来学习抽象工厂模式。<br />综上可以看到,我们目前的系统中已经在大量的使用redis服务,但是因为系统不能满足业务的快速发展,因此需要迁移到集群服务中。而这时有两套集群服务需要兼容使用,又要满足所有的业务系统改造的同时不影响线上使用。
  5. <a name="aQLS7"></a>
  6. ## 3. 单集群代码使用
  7. 以下是案例模拟中原有的单集群Redis使用方式,后续会通过对这里的代码进行改造。<br />![1631528481(1).png](https://cdn.nlark.com/yuque/0/2021/png/22376009/1631528486515-9c11fffa-2891-462c-b55c-712e966a2fb6.png#clientId=u1e73d8d8-4699-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=363&id=uf3d041d3&margin=%5Bobject%20Object%5D&name=1631528481%281%29.png&originHeight=363&originWidth=560&originalType=binary&ratio=1&rotation=0&showTitle=false&size=46530&status=done&style=none&taskId=u79b19eef-ae1f-41a3-bbdc-7243a5dec78&title=&width=560)
  8. <a name="TruT7"></a>
  9. ### 3.1 定义使用接口
  10. ```java
  11. public interface CacheService {
  12. String get(final String key);
  13. void set(String key, String value);
  14. void set(String key, String value, long timeout, TimeUnit timeUnit);
  15. void del(String key);
  16. }

3.2 实现调用代码

  1. public class CacheServiceImpl implements CacheService {
  2. private RedisUtils redisUtils = new RedisUtils();
  3. @Override
  4. public String get(String key) {
  5. return redisUtils.get(key);
  6. }
  7. @Override
  8. public void set(String key, String value) {
  9. redisUtils.set(key, value);
  10. }
  11. @Override
  12. public void set(String key, String value, long timeout, TimeUnit timeUnit) {
  13. redisUtils.set(key, value, timeout, timeUnit);
  14. }
  15. @Override
  16. public void del(String key) {
  17. redisUtils.del(key);
  18. }
  19. }

目前的代码对于当前场景下的使用没有什么问题,也比较简单。但是所有的业务系统都在使用同时,需要改造就不那么容易了。这里可以思考下,看如何改造才是合理的。

四、用一坨坨代码实现

讲道理没有ifelse解决不了的逻辑,不行就在加一行!
此时的实现方式并不会修改类结构图,也就是与上面给出的类层级关系一致。通过在接口中添加类型字段区分当前使用的是哪个集群,来作为使用的判断。可以说目前的方式非常难用,其他使用方改动颇多,这里只是做为例子。

1. 工程结构

1631529043(1).png
此时的只有两个类,类结构非常简单。而我们需要的补充扩展功能也只是在 CacheServiceImpl中实现。

2. ifelse实现需求

  1. public class CacheServiceImpl implements CacheService {
  2. private RedisUtils redisUtils = new RedisUtils();
  3. private EGM egm = new EGM();
  4. private IIR iir = new IIR();
  5. @Override
  6. public String get(String key, int redisType) {
  7. if (1 == redisType) {
  8. return egm.gain(key);
  9. }
  10. if (2 == redisType) {
  11. return iir.get(key);
  12. }
  13. return redisUtils.get(key);
  14. }
  15. @Override
  16. public void set(String key, String value, int redisType) {
  17. if (1 == redisType) {
  18. egm.set(key, value);
  19. return;
  20. }
  21. if (2 == redisType) {
  22. iir.set(key, value);
  23. return;
  24. }
  25. redisUtils.set(key, value);
  26. }
  27. @Override
  28. public void set(String key, String value, long timeout, TimeUnit timeUnit, int redisType) {
  29. if (1 == redisType) {
  30. egm.setEx(key, value, timeout, timeUnit);
  31. return;
  32. }
  33. if (2 == redisType) {
  34. iir.setExpire(key, value, timeout, timeUnit);
  35. return;
  36. }
  37. redisUtils.set(key, value, timeout, timeUnit);
  38. }
  39. @Override
  40. public void del(String key, int redisType) {
  41. if (1 == redisType) {
  42. egm.delete(key);
  43. return;
  44. }
  45. if (2 == redisType) {
  46. iir.del(key);
  47. return;
  48. }
  49. redisUtils.del(key);
  50. }
  51. }

这里的实现过程非常简单,主要根据类型判断是哪个Redis集群。
虽然实现是简单了,但是对使用者来说就麻烦了,并且也很难应对后期的拓展和不停的维护。

3. 测试验证

接下来我们通过junit单元测试的方式验证接口服务,强调日常编写好单测可以更好的提高系统的健壮度。
编写测试类:

  1. public class ApiTest {
  2. @Test
  3. public void test_CacheService() {
  4. CacheService cacheService = new CacheServiceImpl();
  5. cacheService.set("user_name_01", "pxz", 1);
  6. String val01 = cacheService.get("user_name_01", 1);
  7. System.out.println("测试结果:" + val01);
  8. }
  9. }

结果:
1631529507(1).png
从结果上看运行正常,并没有什么问题。但这样的代码只要到生成运行起来以后,想再改就真的难了!

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

接下来使用抽象工厂模式来进行代码优化,也算是一次很小的重构。
这里的抽象工厂的创建和获取方式,会采用代理类的方式进行实现。所被代理的类就是目前的Redis操作方法类,让这个类在不需要任何修改下,就可以实现调用集群A和集群B的数据服务。
并且这里还有一点非常重要,由于集群A和集群B在部分方法提供上是不同的,因此需要做一个接口适配,而这个适配类就相当于工厂中的工厂,用于创建把不同的服务抽象为统一的接口做相同的业务。这一块与我们上一章节中的 工厂方法模型 类型,可以翻阅参考。

1. 工程结构

1631529781(1).png
抽象工厂模型结构
1631529846(1).png
工程中涉及的部分核心功能代码,如下;
ICacheAdapter ,定义了适配接口,分别包装两个集群中差异化的接口名称。 EGMCacheAdapter 、 IIRCacheAdapter
JDKProxy 、 JDKInvocationHandler ,是代理类的定义和实现,这部分也就是抽象工厂的另外一种实现方式。通过这样的方式可以很好的把原有操作Redis的方法进行代理操作,通过控制不同的入参对象,控制缓存的使用。
接下来会分别讲解几个类的具体实现

2. 代码实现

2.1 定义适配接口

  1. public interface ICacheAdapter {
  2. String get(String key);
  3. void set(String key, String value);
  4. void set(String key, String value, long timeout, TimeUnit timeUnit);
  5. void del(String key);
  6. }

这个类的主要作用是让所有集群的提供方,能在统一的方法名称下进行操作。也方面后续的拓展。

2.2 实现集群使用服务

  1. public class EGMCacheAdapter implements ICacheAdapter {
  2. private EGM egm = new EGM();
  3. @Override
  4. public String get(String key) {
  5. return egm.gain(key);
  6. }
  7. @Override
  8. public void set(String key, String value) {
  9. egm.set(key, value);
  10. }
  11. @Override
  12. public void set(String key, String value, long timeout, TimeUnit timeUnit) {
  13. egm.setEx(key, value, timeout, timeUnit);
  14. }
  15. @Override
  16. public void del(String key) {
  17. egm.delete(key);
  18. }
  19. }
  1. public class IIRCacheAdapter implements ICacheAdapter {
  2. private IIR iir = new IIR();
  3. @Override
  4. public String get(String key) {
  5. return iir.get(key);
  6. }
  7. @Override
  8. public void set(String key, String value) {
  9. iir.set(key, value);
  10. }
  11. @Override
  12. public void set(String key, String value, long timeout, TimeUnit timeUnit) {
  13. iir.setExpire(key, value, timeout, timeUnit);
  14. }
  15. @Override
  16. public void del(String key) {
  17. iir.del(key);
  18. }
  19. }

以上两个实现都非常容易,在统一方法名下进行包装。

2.3 定义抽象工程代理类和实现

  1. public class JDKProxy {
  2. public static <T> T getProxy(Class<T> interfaceClass, ICacheAdapter cacheAdapter) throws Exception {
  3. InvocationHandler handler = new JDKInvocationHandler(cacheAdapter);
  4. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
  5. Class<?>[] classes = interfaceClass.getInterfaces();
  6. return (T) Proxy.newProxyInstance(classLoader, new Class[]{classes[0]}, handler);
  7. }
  8. }

这里主要的作用就是完成代理类,同时对于使用哪个集群有外部通过入参进行传递。

  1. public class JDKInvocationHandler implements InvocationHandler {
  2. private ICacheAdapter cacheAdapter;
  3. public JDKInvocationHandler(ICacheAdapter cacheAdapter) {
  4. this.cacheAdapter = cacheAdapter;
  5. }
  6. @Override
  7. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  8. return ICacheAdapter.class.getMethod(method.getName(), ClassLoaderUtils.getClazzByArgs(args)).invoke(cacheAdapter, args);
  9. }
  10. }

在代理类的实现中其实也非常简单,通过穿透进来的集群服务进行方法操作。
另外在 invoke 中通过使用获取方法名称反射方式,调用对应的方法功能,也就简化了整体的使用。
到这我们就已经将整体的功能实现完成了,关于抽象工厂这部分也可以使用非代理的方式进行实现。

3. 测试验证

编写测试类:

  1. public class ApiTest {
  2. @Test
  3. public void test_CacheService() throws Exception {
  4. CacheService proxy_EGM = JDKProxy.getProxy(CacheServiceImpl.class, new EGMCacheAdapter());
  5. proxy_EGM.set("user_name_01", "pxz");
  6. String val01 = proxy_EGM.get("user_name_01");
  7. System.out.println("测试结果:" + val01);
  8. CacheService proxy_IIR = JDKProxy.getProxy(CacheServiceImpl.class, new IIRCacheAdapter());
  9. proxy_IIR.set("user_name_01", "pxz");
  10. String val02 = proxy_IIR.get("user_name_01");
  11. System.out.println("测试结果:" + val02);
  12. }
  13. }

在测试的代码中通过传入不同的集群类型,就可以调用不同的集群下的方法 JDKProxy.getProxy(CacheServiceImpl.class, new EGMCacheAdapter());
如果后续有扩展的需求,也可以按照这样的类型方式进行补充,同时对于改造上来说并没有改动原来的方法,降低了修改成本。
结果:
1631530920(1).png
运行结果正常,这样的代码满足了这次拓展的需求,同时你的技术能力给老板留下了深刻的印象。
研发自我能力的提升远不是外接的压力就是编写一坨代码的接口,如果你已经熟练了很多技能,那么可以在即使紧急的情况下,也能做出完善的方案。

六、总结

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