一次正交設(shè)計(jì)之旅

遺留代碼

假如存在一段遺留代碼,使用vector<vector<int>>表示了一個(gè)復(fù)雜的領(lǐng)域?qū)ο?。程序包括?jì)數(shù)與緩存兩種基本特性,其中計(jì)數(shù)因規(guī)則變化而變化。

static vector<vector<int>> getFlaggedCells(vector<vector<int>>& board) {
  vector<vector<int>> result;
  for(auto x : board) {
    if(x[0] == 4) {   
      result.add(x);
    }   
  }
  return result;
}

int countFlaggedCells(vector<vector<int>>& board) {
  vector<vector<int>> result = getFlaggedCells(board);
  return result.size();
}

static vector<vector<int>> getUnflaggedCells(vector<vector<int>>& board) {
  vector<vector<int>> result;
  for(auto x : board) {
    if(x[0] == 3) {   
      result.add(x);
    }   
  }
  return result;
}

int countUnflaggedCells(vector<vector<int>>& board) {
  vector<vector<int>> result = getUnflaggedCells(board);
  return result.size();
}

static vector<vector<int>> getAliveCells(vector<vector<int>>& board) {
  vector<vector<int>> result;
  for(auto x : board) {
    if(x[0] == 2) {   
      result.add(x);
    }   
  }
  return result;
}

int countAliveCells(vector<vector<int>>& board) {
  vector<vector<int>> result = getAliveCells(board);
  return result.size();
}

// global variable
vector<vector<int>> store;

void saveAliveCells(vector<vector<int>>& board) {
  store = getAliveCells(board);
}

壞味道

很明顯,這段代碼存在很多的壞味道。

  • 多級(jí)容器:vector<vector<int>>的復(fù)雜語(yǔ)法令人抓狂;
  • 重復(fù)設(shè)計(jì):函數(shù)名getFlaggedCells, getUnflaggedCells, getAliveCells之間存在明顯的重復(fù)代碼;
  • 全局變量:草率地將store實(shí)現(xiàn)為全局變量,有待進(jìn)一步斟酌;
  • 幻數(shù):0, 2, 3, 4所代表的具體業(yè)務(wù)含義,有待進(jìn)一步明確;
  • 性能:result作為中間結(jié)果,可能存在無(wú)畏的拷貝開(kāi)銷(xiāo)。

重構(gòu)不僅僅涉及命名,函數(shù)提取等基本原子操作,指導(dǎo)重構(gòu)背后的邏輯更多地是軟件設(shè)計(jì)本身。例如,封裝不穩(wěn)定的變化,隔離客戶(hù)與實(shí)現(xiàn)間的耦合等等。

多級(jí)容器

在遺留系統(tǒng)中傳遞或返回多級(jí)的容器早已司空見(jiàn)慣。復(fù)雜的數(shù)據(jù)結(jié)構(gòu)定義本身就非?;逎?,而其處理代碼也往往互相交織在一起,不僅難以理解,還極其脆弱。因?yàn)槠渲腥魏我粋€(gè)級(jí)別的容器發(fā)生變化,都會(huì)給整個(gè)數(shù)據(jù)結(jié)構(gòu)的處理代碼帶來(lái)影響。

對(duì)于多級(jí)容器,其處理方法非常簡(jiǎn)單,將每一級(jí)容器都進(jìn)行封裝。封裝數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)細(xì)節(jié),并暴露更穩(wěn)定的API,可以使得客戶(hù)的代碼更加穩(wěn)定。用戶(hù)通過(guò)擴(kuò)展算子的方式來(lái)獲取或操作數(shù)據(jù)子集,畢竟數(shù)據(jù)子集與客戶(hù)的關(guān)系更加緊密。

消除幻數(shù)

int重構(gòu)為枚舉類(lèi)型State,消除幻數(shù)。目前不能保證State將來(lái)可以被抽象為更具有彈性的類(lèi);但是,此處實(shí)現(xiàn)為枚舉類(lèi)型已經(jīng)足夠。

enum State {
  INIT, SELECTED, ALIVE, FLAGGED
};

封裝第一級(jí)容器

提取Cell,封裝第一級(jí)容器。提取一個(gè)master的查詢(xún)接口,順帶消除幻數(shù)0,使其具有更加明確的業(yè)務(wù)含義。

#include <vector>

struct Cell {
  bool flagged() const {
    return master() == FLAGGED; 
  }

  bool alive() const {
    return master() == ALIVE; 
  }

private:
  State master() const {
    return states.front();
  }

private:
  std::vector<State> states;
};

封裝第二級(jí)容器

提取GameBoard,封裝第二級(jí)容器。

struct GameBoard {
  const std::vector<Cell>& getCells() const {
    return cells;
  }

private:
  std::vector<Cell> cells;
};

此處,暫且將cells直接返回給客戶(hù),下文再仔細(xì)觀(guān)察客戶(hù)將遭受哪些困惑。

客戶(hù)實(shí)現(xiàn)

計(jì)算"已標(biāo)記"的單元格的數(shù)量時(shí),實(shí)現(xiàn)非常簡(jiǎn)單。

int countFlaggedCells(const GameBoard& board) {
    int num = 0;
    for (auto& cell : board.getCells()) {
      if (cell.flagged()) {
        num++;
      }
    }
    return num;
}

但是,當(dāng)用戶(hù)計(jì)算"未標(biāo)記"的單元數(shù)量時(shí),countUnflaggedCellscountFlaggedCells之間遭遇重復(fù)設(shè)計(jì)的苦惱。。

int countUnflaggedCells(const GameBoard& board) {
    int num = 0;
    for (auto& cell : board.getCells()) {
      if (!cell.flagged()) {
        num++;
      }
    }
    return num;
}

參數(shù)化設(shè)計(jì)

為了消除兩者之間的重復(fù),可以提取一個(gè)公共的函數(shù)。應(yīng)用“參數(shù)化設(shè)計(jì)”,消除兩者之間的重復(fù)。

namespace {
int count(const GameBoard& board, bool flagged) {
    int num = 0;
    for (auto& cell : board.getCells()) {
      if (!cell.flagged() == flagged) {
        num++;
      }
    }
    return num;
}
} // end namespace

int countFlaggedCells(const GameBoard& board) {
  return count(board, true);
}

int countUnflaggedCells(const GameBoard& board) {
  return count(board, false);
}

傳遞差異化的true/false而消除重復(fù),可能有損程序的可讀性。畢竟從用戶(hù)角度看,區(qū)別true/false的確不夠清晰。但是,按照“簡(jiǎn)單設(shè)計(jì)”的四個(gè)基本原則,“消除重復(fù)”的優(yōu)先級(jí),要高于“可讀性”的優(yōu)先級(jí);按照這個(gè)原則,自然不必糾結(jié)。

簡(jiǎn)單設(shè)計(jì)原則,它們的優(yōu)先級(jí)和重要程度依次降低。

  • 通過(guò)測(cè)試
  • 消除重復(fù)
  • 易于理解
  • 沒(méi)有冗余

重復(fù)再現(xiàn)

按照業(yè)務(wù)需求,為了實(shí)現(xiàn)應(yīng)用程序的高容錯(cuò)性,應(yīng)用程序需要暫存所有“?;睢钡膯卧瘢?dāng)程序崩潰時(shí)可以據(jù)此恢復(fù)GameBoard的狀態(tài)。

提取CellSaver,消除遺留系統(tǒng)中的全局變量。事實(shí)上,按照DDD(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì))的設(shè)計(jì)思維,最終CellSaver的實(shí)例需要聚集在與應(yīng)用程序生命周期一致的高層對(duì)象上,在此不再冗述。

struct CellSaver {
  void save(const GameBoard& board) {
    for (Cell& cell : board.getCells()) {
      if (cell.alive()) {
        cache.push_back(cell);
      }
    }
  }

private:
  std::vector<Cells> cache;
};

不幸的是,CellSaver::save與上文匿名命名空間中的count函數(shù)之間存在重復(fù)邏輯,程序再次引入了重復(fù)設(shè)計(jì)的壞味道。

搬遷職責(zé)

為了更好地觀(guān)察兩者之間的重復(fù),及其減少用戶(hù)調(diào)用GameBoard計(jì)數(shù)邏輯的復(fù)雜度,將遍歷的邏輯搬遷回GameBoard,并試圖在其內(nèi)部消除它們之間的重復(fù)實(shí)現(xiàn)。

好萊塢原則:Tell, Don't Ask

struct GameBoard {
  int countFlaggedCells() const {
    return count(true);
  }

  int countUnflaggedCells() const {
    return count(false);
  }

  std::vector<Cell> getAliveCells() const {
    std::vector<Cell> result;
    for (Cell& cell : cells) {
      if (cell.alive()) {
        result.push_back(cell);
      }
    }
    return result;
  }

private:
  int count(bool flagged) const {
    int num;
    for (Cell& cell : cells) {
      if (cell.flagged() == flagged) {
        num++;
      }
    }
    return num;
  }

private:
  std::vector<Cell> cells;
};

經(jīng)過(guò)一系列職責(zé)搬遷的重構(gòu)過(guò)程,用戶(hù)獲取計(jì)數(shù)的功能,將直接由GameBoard直接提供。但是,countgetAliveCells之間依然存在重復(fù)邏輯,但重復(fù)的差異更易于觀(guān)察和對(duì)比。

分離變化

觀(guān)察兩者之間的重復(fù)邏輯,程序存在2個(gè)變化的方向。

  • 線(xiàn)性遍歷的算法實(shí)現(xiàn);
  • 用戶(hù)邏輯:計(jì)數(shù),緩存。

提取CellCollector抽象接口,隔離GameBoard遍歷算法與其客戶(hù)邏輯(計(jì)數(shù),緩存)之間的耦合。GameBoard作為Cell的生產(chǎn)者,客戶(hù)作為Cell的消費(fèi)者,通過(guò)CellCollector將它們之間的邏輯相互隔離,使其它們可以相互獨(dú)立地變化,相互正交。

struct CellCollector {
  virtual void add(const Cell&) = 0;
  virtual ~CellCollector() {}
};

struct GameBoard {
  void list(CellCollector& col) const {
    for (auto& cell : cells) {
      col.add(cell);
    }
  }

private:
  std::vector<Cell> cells;
};

客戶(hù)按照CellCollector的契約,擴(kuò)展定義消費(fèi)單個(gè)Cell的實(shí)現(xiàn)邏輯。

計(jì)數(shù)邏輯

namespace {
struct FlaggedCellCounter : CellCollector {
  FlaggedCellCounter(bool flagged) : flagged(flagged) {
  }

  int get() const {
    return num;
  }

private:
  void add(const Cell& cell) override {
    if (cell.flagged() == flagged) {
      num++;
    }
  }

  int num = 0;
};
} // end namespace 

// private, 在此忽略頭文件的聲明
inline int GameBoard::count(bool flagged) {
  FlaggedCellCounter counter(flagged);
  list(counter);
  return counter.get(); 
}

int GameBoard::countFlaggedCells() const {
  return count(true); 
}

int GameBoard::countUnflaggedCells() const {
  return count(false); 
}

但是,FlaggedCellCounter實(shí)現(xiàn)的邏輯與“已標(biāo)記/未標(biāo)記”強(qiáng)度耦合。為了實(shí)現(xiàn)“保活”的計(jì)數(shù),此時(shí)必然導(dǎo)致計(jì)數(shù)邏輯的重復(fù)設(shè)計(jì)。

namespace {
struct AliveCellCounter : CellCollector {
  int get() const {
    return num;
  }

private:
  void add(const Cell& cell) override {
    if (cell.alive()) {
      num++;
    }
  }

  int num = 0;
};
} // end namespace 

int GameBoard::countAliveCells() const {
  AliveCellCounter counter;
  list(counter);
  return counter.get(); 
}

顯然,公開(kāi)的countAliveCells與私有的count之間存在重復(fù)設(shè)計(jì),及其AliveCellCounterFlaggedCellCounter的結(jié)構(gòu)性重復(fù)。

更穩(wěn)定的抽象

仔細(xì)觀(guān)察AliveCellCounterFlaggedCellCounter之間的結(jié)構(gòu)性重復(fù),它們都存在相同的計(jì)數(shù)規(guī)則和結(jié)果返回的邏輯,僅僅計(jì)數(shù)的前置謂詞存在差異。因此,使用泛型提取前置謂詞,使得謂詞邏輯與Counter的具體實(shí)現(xiàn)相互解耦。

編譯時(shí)多態(tài):C++的模板技術(shù)是一種典型的“編譯時(shí)多態(tài)”技術(shù),遵循樸素的“鴨子編程”的設(shè)計(jì)思維。

namespace {

template <typename Pred>
struct CellCounter : CellCollector {
  CellCounter(Pred pred) : pred(pred) {
  }

  int get() const {
    return num;
  }

private:
  void add(const Cell& cell) override {
    if (pred(cell)) {
      num++;
    }
  }

  int  num = 0;
  Pred pred;
};
} // end namespace 

// private, 此處略去頭文件中的聲明。
template <typename Pred>
inline int GameBoard::count(Pred pred) const {
  CellCounter counter(pred);
  list(counter);
  return counter.get();
}

int GameBoard::countAliveCells() const {
  return count([](auto& cell) {
    return cell.alive();
  }); 
}

int GameBoard::countUnflaggedCells() const {
  return count([](auto& cell) {
    return !cell.flagged();
  }); 
}

int GameBoard::countAliveCells() const {
  return count([](auto& cell) {
    return cell.flagged();
  }); 
}

緩存邏輯

最后,緩存邏輯搬遷至客戶(hù)代碼,因?yàn)槠淠繕?biāo)存儲(chǔ)在客戶(hù)側(cè)。

private繼承: 在CellStore::save內(nèi)部,CellStoreCellCollector之間滿(mǎn)足李氏替換原則,堪稱(chēng)C++的必殺技之一。反觀(guān)諸如Java之流,不得不聲明為public繼承,無(wú)論是從邏輯上,還是語(yǔ)義上都存在明顯的缺陷。

struct CellStore : private CellCollector {
  void save(const GameBoard& board) {
    board.list(*this);
  }

private:
  void add(const Cell& cell) override {
    if (cell.alive()) {
      cache.push_back(cell);
    }
  }

private:
  std::vector<Cell> cache;
};

總結(jié)

回顧既有的遺留系統(tǒng),存在明顯的重復(fù)設(shè)計(jì)、全局變量的依賴(lài)性、晦澀的多級(jí)容器的復(fù)雜數(shù)據(jù)結(jié)構(gòu)、計(jì)數(shù)與緩存邏輯不能復(fù)用、及其諸如幻數(shù)、命名等低級(jí)編程水平。

應(yīng)用封裝技術(shù),將復(fù)雜的多級(jí)容器的數(shù)據(jù)結(jié)構(gòu)分拆到GameBoard, Cell等領(lǐng)域?qū)ο?,使用值?duì)象State表示Cell的狀態(tài)邏輯。最后,應(yīng)用“分離關(guān)注點(diǎn)”,將遍歷算法搬遷回GameBoard實(shí)現(xiàn)代碼的高度復(fù)用。

在客戶(hù)端,為了降低客戶(hù)調(diào)用計(jì)數(shù)的邏輯,應(yīng)用好萊塢原則,搬遷計(jì)數(shù)的邏輯到GameBoard中;與之相反,緩存功能因?yàn)槟繕?biāo)存儲(chǔ)由客戶(hù)自身維護(hù),應(yīng)用private繼承,擴(kuò)展實(shí)現(xiàn)緩存的功能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容