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

news2024/10/6 2:25:16

文章目录

  • 创建型模式
  • 概述
  • Case
  • 场景模拟工程
  • 模拟早期单机Redis的使用
  • Bad Impl
  • Better Impl (抽象⼯⼚模式重构代码)
    • 定义适配接⼝
    • 实现集群适配器接口
    • 代理方式的抽象工厂类
    • 单元测试
  • 小结

在这里插入图片描述


创建型模式

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

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

概述

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

举个例子:

不同系统内的回⻋换⾏

  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<T> cacheClazz : 在模拟场景中,不同的系统使用不同的Redis服务名,通过这样的方式编译实例化后的注入操作
  • Class<? extends ICacheAdapter> 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集群、操作系统)情况下,接⼝选择的问题。⽽这种场景在业务开发中也是⾮常多⻅的,只不过可能有时候没有将它们抽象化出来。
  • 当你知道什么场景下何时可以被抽象⼯程优化代码,那么你的代码层级结构以及满⾜业务需求上,都可以得到很好的完成功能实现并提升扩展性和优雅度
  • 这个设计模式满⾜了;单⼀职责、开闭原则、解耦等优点,但如果说随着业务的不断拓展,可能会造成类实现上的复杂度。但也可以说算不上缺点,因为可以随着其他设计⽅式的引⼊和代理类以及⾃动⽣成加载的⽅式降低此项缺点。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/179823.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

0、Spring工程构建Spring快速入门Spring配置文件详解注入Sprint相关API

1、Spring工程构建 创建工程项目目录文件夹 IDEA选择项目new一个module 配置案例 aop创建 创建并下载完毕后&#xff0c;点击file选择projert 选择按照的jdk版本 output选择当前目录&#xff0c; 点击右下方apply 选择facets&#xff0c;点击""号选择web 选择当前…

Pinia状态管理

1、Pinia和Vuex的对比 1.1、什么是Pinia呢&#xff1f; Pinia&#xff08;发音为/piːnjʌ/&#xff0c;如英语中的“peenya”&#xff09;是最接近pia&#xff08;西班牙语中的菠萝&#xff09;的词&#xff1b; Pinia开始于大概2019年&#xff0c;最初是作为一个实验为Vue…

Linux使用操作

文章目录各类小技巧&#xff08;快捷键&#xff09;软件安装systemctl软连接日期、时区IP地址、主机名IP地址和主机名虚拟机配置固定IP网络传输下载和网络请求端口进程管理主机状态环境变量上传、下载压缩、解压各类小技巧&#xff08;快捷键&#xff09; 强制停止 Linux某些程…

python语法 dot函数

dot是numpy里的函数&#xff0c;主要用于求向量相乘&#xff0c;矩阵乘法&#xff0c;矩阵与向量乘法一、一维向量相乘要求元素个数相同&#xff0c;相当于求内积&#xff0c;对应元素相乘再相加&#xff0c;“1*3 2*4 11”二、矩阵和矩阵相乘遵循矩阵乘法法则“左行 * 右列”…

高通平台开发系列讲解(WIFI篇)什么是WLAN无线局域网

文章目录 一、什么是WLAN1.1、WLAN发展史1.2、WLAN工作频段二、高通相关文件2.1、配置文件2.2、开机启动2.3、wpa_supplicant沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本文将基于高通平台介绍什么是无线局域网。 一、什么是WLAN 在WLAN领域被大规模推广和商用的是…

【编程入门】开源记事本(鸿蒙Java版)

背景 前面已输出多个系列&#xff1a; 《十余种编程语言做个计算器》 《十余种编程语言写2048小游戏》 《17种编程语言10种排序算法》 《十余种编程语言写博客系统》 《十余种编程语言写云笔记》 本系列对比云笔记&#xff0c;将更为简化&#xff0c;去掉了网络调用&#xff0…

WebSocket 入门:简易聊天室

大家好&#xff0c;我是前端西瓜哥&#xff0c;今天我们用 WebSocket 来实现一个简单的聊天室。 WebSocket 是一个应用层协议&#xff0c;有点类似 HTTP。但和 HTTP 不一样的是&#xff0c;它支持真正的全双工&#xff0c;即不仅客户端可以主动发消息给服务端&#xff0c;服务…

基于Tkinter制作简易的串口bootloader上位机

文章目录前言1.测试设备1.1 UART Bootloaer软件架构图1.2 UART Bootloader流程图1.3 通信数据处理1.3.1 S19文件的简单介绍1.3.2 S19文件的传输方式1.3.2 接收数据之后的处理1.4 链接文件设置1.4.1 Bootloader设置1.4.2 Application设置2.上位机2.1 参考资料2.2 Tkinter简介2.3…

C++初阶:vector类

文章目录1 vector介绍2 实现vector2.1 类的定义2.2 默认成员函数2.2.1 构造函数2.2.2 析构函数2.2.3 拷贝构造2.2.4 赋值重载2.3访问接口2.4 容量接口2.5 修改接口2.5.1 尾插尾删2.5.2 任意位置插入2.5.3 任意位置删除2.6 其他接口1 vector介绍 1 vector是表示可变大小数组的序…

每日学术速递1.26

CV - 计算机视觉 今天带来的是北航IRIP实验室被国际人工智能联合会议IJCAI-ECAI 2022接收的3篇论文。 IJCAI 是人工智能领域中最主要的学术会议之一&#xff0c;原为单数年召开&#xff0c;自2015年起改为每年召开&#xff0c;本次IJCAI与ECAI一起召开。IJCAI官网显示&#xf…

【Linux】冯诺依曼体系结构与操作系统概念理解

&#x1f451;作者主页&#xff1a;安 度 因 &#x1f3e0;学习社区&#xff1a;StackFrame &#x1f4d6;专栏链接&#xff1a;Linux 文章目录一、前言二、冯诺依曼体系结构1、体系简述2、内存的重要性3、硬件方案解释软件行为4、体系结构中的数据流动5、拓展三、操作系统简述…

ch1 操作系统启动

lab1 实验准备 按照实验解压后进入oslab中&#xff0c;按照make编译。 cd /home/shiyanlou/oslab/ tar -zxvf hit-oslab-linux-20110823.tar.gz \-C /home/shiyanlou/ ./run cd ./linux-0.11/ make all make clean ..... make all运行脚本即可启动内核 调试 汇编级调试和C语…

贪心算法的题目

每一步都做出一个局部最优的选择&#xff0c;最终的结果就是全局最优 只有一部分问题才能用贪心算法&#xff08;严格来讲&#xff0c;一个问题能不能用贪心算法需要证明的&#xff09; 2022.8.30 蔚来笔试题&#xff1a; 有a个y,b个o,c个u,用这些字母拼成一个字符串&#xf…

Anaconda软件中的 Environments 及 Jupyter Lab使用方法介绍

来源&#xff1a;投稿 作者&#xff1a;助教-Frank 编辑&#xff1a;学姐 本篇是打造舒适的AI开发环境系列-软件篇1 上期内容&#xff1a;学人工智能电脑&主机八大件配置选择指南 本文的重点&#xff1a; (1)Environments使用中如何安装python包.; (2)Jupyter Lab如何在…

Kettle(6):表输入组件——mysql转mysql

1 需求 前面我们已经将Excel中数据抽取到了MySQL的t_user表中。 现在有了新需求&#xff0c;要将MySQL数据库中的 t_user 表中的数据抽取出来&#xff0c;装载到另外一张表 t_user1中。 2 构建Kettle数据流图 2.1 从核心对象的输入组件中&#xff0c;将「表输入」组件拖拽到中…

电脑下载软件用什么软件好?安卓手机下载软件用哪个软件好?IDM下载器说:在做的都是弟弟

大年初五&#xff0c;迎财神&#xff0c;先祝大家新的一年财源滚滚&#xff0c;接下来为大家分享超级经典的IDM下载器&#xff0c;电脑端毫无争议的下载工具&#xff0c;安卓平台idm也是力压群雄&#xff0c;下面就为大家详细分享下&#xff1a; 1&#xff1a;1DM下载器&#x…

微服务统一登陆认证怎么做

[微服务统一登陆认证怎么做}&#xff1f;JWT 无状态登录原理 1.1.什么是有状态&#xff1f; 有状态服务&#xff0c;即服务端需要记录每次会话的客户端信息&#xff0c;从而识别客户端身份&#xff0c;根据用户身份进行请求的处理&#xff0c;典型的设计如tomcat中的session…

notepad++在行首行尾添加字符 | 选中列

目录 1、首行/尾行添加字符 1【使用快捷键 CtrlH】 2【^为行首、$为行尾】 3、查找模式选中正则表达式 2、Notepad中列选(竖选&#xff09; 1、首行/尾行添加字符 1【使用快捷键 CtrlH】 或者鼠标 2【^为行首、$为行尾】 3、查找模式选中正则表达式 2、Notepad中列选(竖…

深度学习入门(一)感知机

该文将介绍感知机A&#xff08;perceptron&#xff09;这一算法。感知机是由美国学者Frank Rosenblatt在1957年提出来的。为何我们现在还要学习这一很久以前就有的 算 法 呢 &#xff1f; 因 为 感 知 机 也 是 作 为 神 经 网 络&#xff08;深 度 学 习&#xff09;的起源的算…

详解Windows通过命令行查看电脑连接过的WIfI密码

CONTENT打开命令行进入命令行下的netsh工具查看连接过的WiFi名称指定WiFi名称查看密码在Windows操作系统中&#xff08;PS&#xff1a;Windows Vista及以后的Windows系统&#xff09;可以通过命令行工具netsh查看和更改电脑的无线连接设置&#xff0c;包括WiFi。本篇博客将详细…