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

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

 

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章