現實場景往往是這樣,我們應對一個需求,很快就會有一個處理方法了,然後根據需求做了一個還不錯的實現。因為實現了功能,業務很happy,老闆很開心,all the world is beatiful.
但隨著公司的發展,有人實現了一套底層的標準元件,按要求你必須要接入他那個,他的功能與你類似,但你必須要切換成那個。且不論其實現的質量怎麼樣,但他肯定是有一些優勢的,不過他作為標準套件,不可能完全同你的需求一致。因此,這必定涉及到改造的問題。
一般這種情況下,我們是不太願意接的,畢竟程式碼跑得好好的,誰願意動呢?而且別人的實現如何,還沒有經過考驗,冒然接入,可能帶來比較大的鍋呢。(從0到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,如此迴圈往復,直到棧溢位。也叫出現了死迴圈。
上一節我們已經知道為什麼出現載入失敗的問題,其實就是一個迴圈依賴問題。如何解決呢?
其實就是簡單地移動下程式碼,不要將判斷放在預設構造器中,由具體的外觀類進行處理,載入策略由外觀類決定,而非具體的實現類或抽象類。
具體操作如下:
// 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到抽象類中,而新增的一個原有實現類,則什麼也不用做,只需新增一個空繼承抽象類即可。而新的實現,則完全覆蓋現有的具體實現就可以了。從而達到一個最小的改動,而且順利接入一個新實現的效果。
但是如果依賴於抽象類的具體實現的話,會帶來一個問題,那就是如果我們的子類實現得不完善,比如遺漏了一些實現時,程式碼本身並不會報錯提示。這就給我們帶來了潛在的風險,因為那樣就會變成,一部分是舊有實現,另一部分是新的實現。這可能會有兩個問題:一是兩個實現有一個報錯一個正常;二是無法正常切換回滾,兩種實現耦合在了一起。
怎麼辦呢?我們可以再抽象一層介面出來,各實現針對介面處理,只有外觀類繼承了抽象類,而且抽象類同時也實現了介面定義。這樣的話,就保證了各實現的完整性,以及外觀類的統一性了。這裡,我利用的是語法的強制特性,即介面必須得到實現的語意,進行程式碼準確性的保證。(當然了,所有的現實場景,介面都必須有相應的實現,因為外部可見只有介面,如果不實現則必定不合法)
具體實現如下:
//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... }
看起來是多增加了一個介面類,但是實際上整個程式碼更加清晰易讀了。實際上,一個好的設計,最初應該也是基於介面的(即面向介面程式設計),而我們在這裡重新抽象出一個介面類來,實際上就是彌補之前設計的不足,也算是一種重構了。所有的實現都基於介面,一個實現都不能少,從而減少了出錯的概率。
如此,我們就可以放心的進行生產切換了。
文章原創釋出微信公眾號地址: 一個快速切換一個底層實現的思路分享