一個快速切換一個底層實現的思路分享

等你歸去來發表於2022-06-26

    現實場景往往是這樣,我們應對一個需求,很快就會有一個處理方法了,然後根據需求做了一個還不錯的實現。因為實現了功能,業務很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);
}

 

2. 實現兩個具體類

// 實現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...");
    }
}

 

3. 保持原有的實現類入口,將其實現變成一個外觀類或者叫介面卡類

// 載入入口
@Service
public class RedisOperateFacade extends AbstractRedisOperate {

    public RedisOperateFacade() {
        // case1. 直接交由父類處理
        super();
    }

    @Override
    public void set(String key, String value) {
        // fake impl
    }
}

 

    以上實現有什麼好處呢?首先,現有的實現被抽離,且不用做改動被保留了下來。新的實現類自行實現一個新的。通過一個公共的切換開關,進行切換處理。這樣一來,既可以保證接入了新實現,而且也保留了舊實現,在出未知故障時,可以回切實現。

    以上實現有什麼問題?

    當我們執行上面的程式碼時,發現報錯了,為什麼?因為出現了死迴圈。雖然我們只載入了一個 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);
}

 

    做了微小的改動,將載入策略從抽象類中轉移到外觀類中,就可以達到正確的載入效果了。實際上,為了簡單起見,我們甚至可以將原有的實現全部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...
}

 

    看起來是多增加了一個介面類,但是實際上整個程式碼更加清晰易讀了。實際上,一個好的設計,最初應該也是基於介面的(即面向介面程式設計),而我們在這裡重新抽象出一個介面類來,實際上就是彌補之前設計的不足,也算是一種重構了。所有的實現都基於介面,一個實現都不能少,從而減少了出錯的概率。

    如此,我們就可以放心的進行生產切換了。

 文章原創釋出微信公眾號地址: 一個快速切換一個底層實現的思路分享

相關文章