書接上回為什麼需要依賴注入再做下擴充套件
上文談到:“基於抽象介面程式設計確實是最佳實踐:把易於變動的功能點透過定義抽象介面的形式暴露出來,不同的實現做到隔離和擴充套件,這體現了開閉原則”
public class Foo {
private Bar bar ;
@Inject
public Foo(Bar bar) {
this.bar = bar;
}
public String doSomething(int key) {
//Bar#getResult 體驗了程式碼的複雜性,透過注入不同的 Bar 實現物件,做到功能點的隔離和擴充套件
return bar.getResult(key);
}
}
但在真實專案裡,往往是多人協作一起開發,一些歷史原因導致某些程式碼片段的實現往往“千奇百怪”,既不能很好的單側覆蓋,同時也充斥著違反了開閉原則的“程式碼壞味道”;
而此時的你,作為“被選中的人”,需要對其功能迭代;
或許經過你的評估後,可以去大刀闊斧的架構演進,這是點讚的;
但有時也要全域性 ROI 去評估大刀闊斧重構收益是否足夠大,有時候我們只能妥協(trade-off
)。即:如何在緊張的交付週期內做到比較好的重構,不讓程式碼繼續腐化;
所以這次繼續介紹兩種修改程式碼的藝術:方法新增和方法覆蓋
策略 1:方法新增
透過新增方法來隔離舊邏輯,即:在舊方法裡橫切“縫隙”,注入新的業務邏輯被呼叫;
拿之前的 Case 舉例,一個歷史老方法,需要對返回的資料集合過濾掉空物件:
public class Foo {
private Bar bar;
public Foo() {
bar = new Bar();
}
public List<Data> doSomething(int key) {
//依賴三方服務,RPC 呼叫結果集
List<Data> result = bar.getResult(key);
//過濾掉空物件
return result.stream().filter(Objects::nonNull).collect(Collectors.toList());
}
}
此處邏輯很簡單,使用了Java Lambda
表示式做了過濾,但這樣的寫法無疑雪上加霜:確實原先方法已經很 Low 了,也無法單側。本次只是在最後加了一段簡單的邏輯。已經駕輕就熟了,可能不少人都會這樣搞;
但作為好的程式設計師,眼前現狀確實我們只能妥協,但後續的每一行程式碼,需要做到保質保量,努力做到不影響原有業務邏輯下做到可測試;
“方法新增”:透過新增方法 getDataIfNotNull
來隔離舊邏輯:
public List<Data> doSomething(int key) {
//依賴三方服務,RPC 呼叫結果集
List<Data> result = bar.getResult(key);
return getDataIfNotNull(result);
}
如下 getDataIfNotNull
作為新增方法,很容易對其進行獨立測試,同時原有的方法 doSomething
也沒有繼續腐化
public List<Data> getDataIfNotNull(List<Data> result) {
return result.stream().filter(Objects::nonNull).collect(Collectors.toList());
}
可以看到優點很明顯:新老程式碼清晰隔離;當然為了更加職責分明,使用新增類隔離
會更好;
策略 2:方法覆蓋
將待修改的方法重新命名,並建立一個新方法和原方法名和簽名一致,同時在新方法中呼叫重新命名後的原方法;
假設有新需求:針對 doSomething
方法做一個訊息通知操作,那麼“方法覆蓋”即:將原方法 doSomething
重新命名為 doSomethingAndFilterData
,再建立一個與原方法同名的新方法 doSomething
,最後在新方法中呼叫更名後的原方法:
//將原方法 doSomething 重新命名為 doSomethingAndFilterData
public List<Data> doSomethingAndFilterData(int key) {
//依賴三方服務,RPC 呼叫結果集
List<Data> result = bar.getResult(key);
return getDataIfNotNull(result);
}
//建立一個與原方法同名的新方法 doSomething
public List<Data> doSomething(int key) {
//呼叫舊方法
List<Data> data = this.doSomethingAndFilterData(key);
//呼叫新方法
doNotifyMsg(data);
return data;
}
//新的擴充套件方法符合隔離擴充套件,不影響舊方法,也支援單側覆蓋
public void doNotifyMsg(List<Data> data){
//
}
方法覆蓋的另一種寫法:通常是再定義一個新的方法,然後在新的方法依次呼叫新老業務邏輯;
一般在架構演進的時候,用於切流新老邏輯;例如:基於客戶端版本,大於 3.10.x
的客戶端切流使用新的邏輯——我們建立一個新的方法呼叫新舊兩個方法。
//老的歷史程式碼,不做改造
public List<Data> doSomething(int key) {
//依賴三方服務,RPC 呼叫結果集
List<Data> result = bar.getResult(key);
List<Data> data = getDataIfNotNull(result);
return data;
}
//新建立一個方法,聚合呼叫新老邏輯
public List<Data> doSomethingWithNotifyMsg(int key) {
List<Data> data = this.doSomething(key);
//呼叫新方法
doNotifyMsg(data);
return data;
}
//新的擴充套件方法符合隔離擴充套件,不影響舊方法,也支援單側覆蓋
public void doNotifyMsg(List<Data> data){
//
}
這樣的好處是顯然易見的,不針對舊方法做修改,同時在更高維度的“上層”切流:保證新功能正常迭代演進,老功能維持不變
boolean enableFunc=getClientVersion()>DEFAULT_CLIENT_VERSION;
if (enableFunc){
return doSomethingWithNotifyMsg();
} else {
return doSomething();
}
可以看到“方法覆蓋”不管用何總方式實現,它不會在當前舊方法裡增加邏輯,而是透過使用新方法作為入口,這樣避免新老邏輯耦合在一起;
“方法覆蓋”可以再進階一步,使用獨立的類來隔離,也就是裝飾者模式。通常情況下原有的類已經非常複雜了,已經不想在它上做功能迭代了,考慮使用裝飾者來解耦:
class DecoratedFoo extends Foo{
private Foo foo;
public DecoratedFoo(Foo foo){
}
@Override
public List<Data> doSomething(int key) {
List<Data> data = super.doSomething(key);
notifyMsg();
return data;
}
private void notifyMsg(){
}
}