• 一个快速切换一个底层实现的思路分享


    🚀 优质资源分享 🚀

    学习路线指引(点击解锁)知识定位人群定位
    🧡 Python实战微信订餐小程序 🧡进阶级本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
    💛Python量化交易实战💛入门级手把手带你打造一个易扩展、更安全、效率更高的量化交易系统
    目录

    现实场景往往是这样,我们应对一个需求,很快就会有一个处理方法了,然后根据需求做了一个还不错的实现。因为实现了功能,业务很happy,老板很开心,all the world is beatiful.

    但随着公司的发展,有人实现了一套底层的标准组件,按要求你必须要接入他那个,他的功能与你类似,但你必须要切换成那个。且不论其实现的质量怎么样,但他肯定是有一些优势的,不过他作为标准套件,不可能完全同你的需求一致。因此,这必定涉及到改造的问题。

    一般这种情况下,我们是不太愿意接的,毕竟代码跑得好好的,谁愿意动呢?而且别人的实现如何,还没有经过考验,冒然接入,可能带来比较大的锅呢。(从0到1没人关注准确性,但从1到到1.1就会有人关注准确性了,换句话说这叫兼容性)

    但是,往往迫于压力,我们又不得不接。

    这时候我们有两种做法,一种是硬着头皮直接改代码为别人的方式。这种处理简单粗暴,而且没有后顾之忧。不过,随之而来的,就是大面积的回归测试,以及一些可能测试不到的点,意味着代码的回滚。对于一些线上运维比较方便的地方,也许我们是可以这样干。但这并不是本文推荐的做法,也不做更多讨论。

    更稳妥的做法,应该是在保有现有实现的情况下,进行新实现的接入,至少你还可以对照嘛。进可攻,退可守。


    返回顶部### 1. 快速接入新实现1:抽象类

    既然我们不敢直接替换现有的实现,那么就得保留两种实现,所以可以用抽象类的方式,保持原有实现的同时,切入新的实现。是个比较直观的想法了,具体实现如下:

    1. 抽象一个公共类出来

    public abstract class AbstractRedisOperate {
    
     private AbstractRedisOperate impl;
    
     public AbstractRedisOperate() {
     String strategy = "a";  // from config
            if("a".equals(strategy)) {
     impl = new RedisOperateA1Imp();
     }
     else {
     impl = new RedisOperateB2Imp();
     }
     }
    
     // 示例操作接口
        public void set(String key, String value);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    1. 实现两个具体类
    // 实现1,完全依赖于抽象类实现(旧有功能)
    public class RedisOperateOldImp extends AbstractRedisOperate {
    
    }
    
    // 实现2,新接入的实现
    public class RedisOperateB2Imp extends AbstractRedisOperate {
    
     @Override
     public void set(String key, String value) {
     System.out.println("this is b's implement...");
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    1. 保持原有的实现类入口,将其实现变成一个外观类或者叫适配器类
    // 加载入口
    @Service
    public class RedisOperateFacade extends AbstractRedisOperate {
    
     public RedisOperateFacade() {
     // case1. 直接交由父类处理
            super();
     }
    
     @Override
     public void set(String key, String value) {
     // fake impl
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    以上实现有什么好处呢?首先,现有的实现被抽离,且不用做改动被保留了下来。新的实现类自行实现一个新的。通过一个公共的切换开关,进行切换处理。这样一来,既可以保证接入了新实现,而且也保留了旧实现,在出未知故障时,可以回切实现。

    以上实现有什么问题?

    当我们运行上面的代码时,发现报错了,为什么?因为出现了死循环。虽然我们只加载了一个 Facade 的实现,但是在调用super时,super会反过来加载具体的实现,具体的实现又会去加载抽象类super,如此循环往复,直到栈溢出。也叫出现了死循环。


    返回顶部### 2. 解决简单抽象带来的问题

    上一节我们已经知道为什么出现加载失败的问题,其实就是一个循环依赖问题。如何解决呢?

    其实就是简单地移动下代码,不要将判断放在默认构造器中,由具体的外观类进行处理,加载策略由外观类决定,而非具体的实现类或抽象类。

    具体操作如下:

    // 1. 外观类控制加载
    @Service
    public class RedisOperateFacade extends AbstractRedisOperate {
    
     public RedisOperateFacade() {
     // case1. 直接交由父类处理
     // super();
     // case2. 决定加载哪个实现
            String strategy = "a";  // from config center
            if("a".equals(strategy)) {
     setImpl(new RedisOperateOldImp());
     }
     else {
     setImpl(new RedisOperateB2Imp());
     }
     }
    
    }
    // 2. 各实现保持自身不动
    public class RedisOperateOldImp extends AbstractRedisOperate {
     // old impl...
    }
    
    public class RedisOperateB2Imp extends AbstractRedisOperate {
    
     // new impl...
     @Override
     public void set(String key, String value) {
     System.out.println("this is b's implement...");
     }
    }
    
    // 3. 抽象类不再进行加载策略处理
    public abstract class AbstractRedisOperate {
     // 持有具体实现
        private AbstractRedisOperate impl;
    
     public AbstractRedisOperate() {
     }
    
     protected void setImpl(AbstractRedisOperate impl) {
     this.impl = impl;
     }
    
     // 示例操作接口, old impl...
        public abstract void set(String key, String value);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    做了微小的改动,将加载策略从抽象类中转移到外观类中,就可以达到正确的加载效果了。实际上,为了简单起见,我们甚至可以将原有的实现全部copy到抽象类中,而新增的一个原有实现类,则什么也不用做,只需新增一个空继承抽象类即可。而新的实现,则完全覆盖现有的具体实现就可以了。从而达到一个最小的改动,而且顺利接入一个新实现的效果。

    但是如果依赖于抽象类的具体实现的话,会带来一个问题,那就是如果我们的子类实现得不完善,比如遗漏了一些实现时,代码本身并不会报错提示。这就给我们带来了潜在的风险,因为那样就会变成,一部分是旧有实现,另一部分是新的实现。这可能会有两个问题:一是两个实现有一个报错一个正常;二是无法正常切换回滚,两种实现耦合在了一起。


    返回顶部### 3. 更完善的方案:基于接口的不同实现

    怎么办呢?我们可以再抽象一层接口出来,各实现针对接口处理,只有外观类继承了抽象类,而且抽象类同时也实现了接口定义。这样的话,就保证了各实现的完整性,以及外观类的统一性了。这里,我利用的是语法的强制特性,即接口必须得到实现的语义,进行代码准确性的保证。(当然了,所有的现实场景,接口都必须有相应的实现,因为外部可见只有接口,如果不实现则必定不合法)

    具体实现如下:

    //1. 统一接口定义
    public interface UnifiedRedisOperate {
    
     void set(String key, String value, int ttl);
    
     // more interface definitions...
    }
    // 2. 各子实现类
    public class RedisOperateOldImp implements UnifiedRedisOperate {
    
     @Override
     public void set(String key, String value) {
     System.out.println("this is a's implement...");
     }
    }
    public class RedisOperateB2Imp implements UnifiedRedisOperate {
    
     @Override
     public void set(String key, String value) {
     System.out.println("this is b's implement...");
     }
    }
    // 3. 外观类的实现
    @Service
    public class RedisOperateFacade extends AbstractRedisOperate {
    
     public RedisOperateFacade() {
     // case1. 直接交由父类处理
     // super();
     // case2. 外观类控制加载
            String strategy = "a";  // from config center
            if("a".equals(strategy)) {
     setImpl(new RedisOperateOldImp());
     }
     else {
     setImpl(new RedisOperateB2Imp());
     }
     }
    
    }
    public abstract class AbstractRedisOperate implements UnifiedRedisOperate {
    
     private UnifiedRedisOperate impl;
    
     protected void setImpl(UnifiedRedisOperate impl) {
     this.impl = impl;
     }
    
     // 接口委托
        public void set(String key, String value) {
     impl.set(key, value);
     }
    
     // more delegates...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    看起来是多增加了一个接口类,但是实际上整个代码更加清晰易读了。实际上,一个好的设计,最初应该也是基于接口的(即面向接口编程),而我们在这里重新抽象出一个接口类来,实际上就是弥补之前设计的不足,也算是一种重构了。所有的实现都基于接口,一个实现都不能少,从而减少了出错的概率。

    如此,我们就可以放心的进行生产切换了。

    文章原创发布微信公众号地址: 一个快速切换一个底层实现的思路分享

  • 相关阅读:
    中华人民共和国网络安全法
    Flask-SQLAlchemy 快速上手
    C++之if-else基本应用
    docker--基础(一)
    【用四道刁钻例题带你理解】数据在内存中存储的方式
    深入理解Docker
    MySQL SELECT 的执行顺序
    nodejs调用python 实现方案整理
    215. 数组中的第K个最大元素 java解决
    IllusionDiffusion:OpenAI 推出的图像生成新工具
  • 原文地址:https://blog.csdn.net/weixin_45566993/article/details/125468891