再談如何優雅修改程式碼

木宛城主發表於2024-10-07

書接上回( https://www.cnblogs.com/OceanEyes/p/18450799)再做下擴充套件

上文談到:“基於抽象介面程式設計確實是最佳實踐:把易於變動的功能點透過定義抽象介面的形式暴露出來,不同的實現做到隔離和擴充套件,這體現了開閉原則”
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();
}

可以看到“方法覆蓋”不管用何總方式實現,它不會在當前舊方法裡增加邏輯,而是透過使用新方法作為入口,這樣避免新老邏輯耦合在一起;

“方法覆蓋”可以再進階一步,使用獨立的類來隔離,也就是裝飾者模式。通常情況下原有的類已經非常複雜了,已經不想在它上做功能迭代了,考慮使用裝飾者來解耦:

classDecoratedFooextendsFoo{

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(){}
}

相關文章