重学 Java 设计模式:实战抽象工厂模式

做者:小傅哥
博客:https://bugstack.cn - 本文章已收录到系列原创专题html

沉淀、分享、成长,让本身和他人都能有所收获!😄

1、前言

代码一把梭,兄弟来背锅。java

大部分作开发的小伙伴初心都但愿把代码写好,除了把编程看成工做之外他们仍是具有工匠精神的从业者。但不少时候又很难让你把初心坚持下去,就像;接了个烂手的项目、产品功能要的急、我的能力不足,等等缘由致使工程代码臃肿不堪,线上频出事故,最终离职走人。git

看了不少书、学了不少知识,多线程能玩出花,可最后我仍是写很差代码!程序员

这就有点像家里装修完了买物件,我几十万的实木沙发,怎么放这里就很差看。一样代码写的很差并不必定是基础技术不足,也不必定是产品要得急 怎么实现我无论明天上线。而不少时候是咱们对编码的经验的不足和对架构的把控能力不到位,我相信产品的第一个需求每每都不复杂,甚至所见所得。但若是你不考虑后续的是否会拓展,未来会在哪些模块继续添加功能,那么后续的代码就会随着你种下的第一颗恶性的种子开始蔓延。github

学习设计模式的心得有哪些,怎么学才会用!redis

设计模式书籍,有点像考驾驶证的科1、家里装修时的手册、或者单身狗的恋爱宝典。但!你只要不实操,必定能搞的七糟。由于这些指导思想都是从实际经验中提炼的,没有通过提炼的小白,很难驾驭这样的知识。因此在学习的过程当中首先要有案例,以后再结合案例与本身实际的业务,尝试重构改造,慢慢体会其中的感觉,从而也就学会了若是搭建出优秀的代码。编程

2、开发环境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三个,能够经过关注公众号bugstack虫洞栈,回复源码下载获取
工程 描述
itstack-demo-design-2-00 场景模拟工程,模拟出使用Redis升级为集群时类改造
itstack-demo-design-2-01 使用一坨代码实现业务需求,也是对ifelse的使用
itstack-demo-design-2-02 经过设计模式优化改造代码,产生对比性从而学习

3、抽象工厂模式介绍

抽象工厂模式,图片来自 refactoringguru.cn

抽象工厂模式与工厂方法模式虽然主要意图都是为了解决,接口选择问题。但在实现上,抽象工厂是一个中心工厂,建立其余工厂的模式。设计模式

可能在日常的业务开发中不多关注这样的设计模式或者相似的代码结构,可是这种场景确一直在咱们身边,例如;缓存

  1. 不一样系统内的回车换行多线程

    1. Unix系统里,每行结尾只有 <换行>,即 \n
    2. Windows系统里面,每行结尾是 <换行><回车>,即 \n\r
    3. Mac系统里,每行结尾是 <回车>
  2. IDEA 开发工具的差别展现(WinMac)

    不一样系统下,IDEA 开发工具的展现差别点

除了这样显而易见的例子外,咱们的业务开发中时常也会遇到相似的问题,须要兼容作处理但大部分经验不足的开发人员,经常直接经过添加ifelse方式进行处理了。

4、案例场景模拟

模拟企业级双套Redis集群升级

不少时候初期业务的蛮荒发展,也会牵动着研发对系统的建设。

预估QPS较低系统压力较小并发访问不大近一年没有大动做等等,在考虑时间投入成本的前提早,并不会投入特别多的人力去构建很是完善的系统。就像对 Redis 的使用,每每可能只要是单机的就能够知足现状。

不吹牛的讲百度首页我上学时候一天就能写完,等毕业工做了就算给我一年都完成不了!

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

随着此次的升级,能够预见的问题会有;

  1. 不少服务用到了Redis须要一块儿升级到集群。
  2. 须要兼容集群A和集群B,便于后续的灾备。
  3. 两套集群提供的接口和方法各有差别,须要作适配。
  4. 不能影响到目前正常运行的系统。

1. 场景模拟工程

itstack-demo-design-2-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── matter
                │   ├── EGM.java
                │   └── IIR.java
                └── RedisUtils.java

工程中的全部代码能够经过关注公众号:bugstack虫洞栈,回复源码下载进行获取。

2. 场景简述

2.1 模拟单机服务 RedisUtils

Redis单机服务

  • 模拟Redis功能,也就是假定目前全部的系统都在使用的服务
  • 类和方法名次都固定写死到各个业务系统中,改动略微麻烦

2.2 模拟集群 EGM

模拟集群 EGM

  • 模拟一个集群服务,可是方法名与各业务系统中使用的方法名不一样。有点像你mac,我用win。作同样的事,但有不一样的操做。

2.3 模拟集群 IIR

模拟集群 IIR

  • 这是另一套集群服务,有时候在企业开发中就颇有可能出现两套服务,这里咱们也是为了作模拟案例,因此添加两套实现一样功能的不一样服务,来学习抽象工厂模式。

综上能够看到,咱们目前的系统中已经在大量的使用redis服务,可是由于系统不能知足业务的快速发展,所以须要迁移到集群服务中。而这时有两套集群服务须要兼容使用,又要知足全部的业务系统改造的同时不影响线上使用。

3. 单集群代码使用

如下是案例模拟中原有的单集群Redis使用方式,后续会经过对这里的代码进行改造。

当前功能的类图结构

3.1 定义使用接口

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

}

3.2 实现调用代码

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

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

5、用一坨坨代码实现

讲道理没有ifelse解决不了的逻辑,不行就在加一行!

此时的实现方式并不会修改类结构图,也就是与上面给出的类层级关系一致。经过在接口中添加类型字段区分当前使用的是哪一个集群,来做为使用的判断。能够说目前的方式很是难用,其余使用方改动颇多,这里只是作为例子。

1. 工程结构

itstack-demo-design-2-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── impl
                │   └── CacheServiceImpl.java
                └── CacheService.java
  • 此时的只有两个类,类结构很是简单。而咱们须要的补充扩展功能也只是在 CacheServiceImpl 中实现。

2. ifelse实现需求

public class CacheServiceImpl 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集群。
  • 虽然实现是简单了,可是对使用者来讲就麻烦了,而且也很难应对后期的拓展和不停的维护。

3. 测试验证

接下来咱们经过junit单元测试的方式验证接口服务,强调平常编写好单测能够更好的提升系统的健壮度。

编写测试类:

@Test
public void test_CacheService() {
    CacheService cacheService = new CacheServiceImpl();
    cacheService.set("user_name_01", "小傅哥", 1);
    String val01 = cacheService.get("user_name_01",1);
    System.out.println(val01);
}

结果:

22:26:24.591 [main] INFO  org.itstack.demo.design.matter.EGM - EGM写入数据 key:user_name_01 val:小傅哥
22:26:24.593 [main] INFO  org.itstack.demo.design.matter.EGM - EGM获取数据 key:user_name_01
测试结果:小傅哥

Process finished with exit code 0
  • 从结果上看运行正常,并无什么问题。但这样的代码只要到生成运行起来之后,想再改就真的难了!

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

接下来使用抽象工厂模式来进行代码优化,也算是一次很小的重构。

这里的抽象工厂的建立和获取方式,会采用代理类的方式进行实现。所被代理的类就是目前的Redis操做方法类,让这个类在不须要任何修改下,就能够实现调用集群A和集群B的数据服务。

而且这里还有一点很是重要,因为集群A和集群B在部分方法提供上是不一样的,所以须要作一个接口适配,而这个适配类就至关于工厂中的工厂,用于建立把不一样的服务抽象为统一的接口作相同的业务。这一块与咱们上一章节中的工厂方法模型类型,能够翻阅参考。

1. 工程结构

itstack-demo-design-2-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── factory    
    │           │   ├── impl
    │           │   │   ├── EGMCacheAdapter.java 
    │           │   │   └── IIRCacheAdapter.java
    │           │   ├── ICacheAdapter.java
    │           │   ├── JDKInvocationHandler.java
    │           │   └── JDKProxy.java
    │           ├── impl
    │           │   └── CacheServiceImpl.java    
    │           └── CacheService.java 
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

抽象工厂模型结构

抽象工厂模型结构

  • 工程中涉及的部分核心功能代码,以下;

    • ICacheAdapter,定义了适配接口,分别包装两个集群中差别化的接口名称。EGMCacheAdapterIIRCacheAdapter
    • JDKProxyJDKInvocationHandler,是代理类的定义和实现,这部分也就是抽象工厂的另一种实现方式。经过这样的方式能够很好的把原有操做Redis的方法进行代理操做,经过控制不一样的入参对象,控制缓存的使用。

,那么接下来会分别讲解几个类的具体实现。

2. 代码实现

2.1 定义适配接口

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

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

2.2 实现集群使用服务

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

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

}
  • 以上两个实现都很是容易,在统一方法名下进行包装。

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

JDKProxy

public static <T> T getProxy(Class<T> interfaceClass, ICacheAdapter cacheAdapter) throws Exception {
    InvocationHandler handler = new JDKInvocationHandler(cacheAdapter);
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    Class<?>[] classes = interfaceClass.getInterfaces();
    return (T) Proxy.newProxyInstance(classLoader, new Class[]{classes[0]}, handler);
}
  • 这里主要的做用就是完成代理类,同时对于使用哪一个集群有外部经过入参进行传递。

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

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

2. 测试验证

编写测试类:

@Test
public void test_CacheService() throws Exception {
    CacheService proxy_EGM = JDKProxy.getProxy(CacheServiceImpl.class, new EGMCacheAdapter());
    proxy_EGM.set("user_name_01","小傅哥");
    String val01 = proxy_EGM.get("user_name_01");
    System.out.println(val01);
    
    CacheService proxy_IIR = JDKProxy.getProxy(CacheServiceImpl.class, new IIRCacheAdapter());
    proxy_IIR.set("user_name_01","小傅哥");
    String val02 = proxy_IIR.get("user_name_01");
    System.out.println(val02);
}
  • 在测试的代码中经过传入不一样的集群类型,就能够调用不一样的集群下的方法。JDKProxy.getProxy(CacheServiceImpl.class, new EGMCacheAdapter());
  • 若是后续有扩展的需求,也能够按照这样的类型方式进行补充,同时对于改造上来讲并无改动原来的方法,下降了修改为本。

结果:

23:07:06.953 [main] INFO  org.itstack.demo.design.matter.EGM - EGM写入数据 key:user_name_01 val:小傅哥
23:07:06.956 [main] INFO  org.itstack.demo.design.matter.EGM - EGM获取数据 key:user_name_01
测试结果:小傅哥
23:07:06.957 [main] INFO  org.itstack.demo.design.matter.IIR - IIR写入数据 key:user_name_01 val:小傅哥
23:07:06.957 [main] INFO  org.itstack.demo.design.matter.IIR - IIR获取数据 key:user_name_01
测试结果:小傅哥

Process finished with exit code 0
  • 运行结果正常,这样的代码知足了此次拓展的需求,同时你的技术能力也给老板留下了深入的印象。
  • 研发自我能力的提高远不是外接的压力就是编写一坨坨代码的接口,若是你已经熟练了不少技能,那么能够在即便紧急的状况下,也能作出完善的方案。

7、总结

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

8、推荐阅读

9、彩蛋

CodeGuide | 程序员编码指南 Go!
本代码库是做者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为你们提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。若是本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!

CodeGuide | 程序员编码指南

相关文章
相关标签/搜索