一次正交設計之旅

遺留代碼

假如存在一段遺留代碼粟害,使用vector<vector<int>>表示了一個復雜的領域?qū)ο蟆3绦虬ㄓ嫈?shù)與緩存兩種基本特性仗岖,其中計數(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);
}

壞味道

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

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

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

多級容器

在遺留系統(tǒng)中傳遞或返回多級的容器早已司空見慣甫题。復雜的數(shù)據(jù)結(jié)構(gòu)定義本身就非常晦澀涂召,而其處理代碼也往往互相交織在一起坠非,不僅難以理解,還極其脆弱果正。因為其中任何一個級別的容器發(fā)生變化炎码,都會給整個數(shù)據(jù)結(jié)構(gòu)的處理代碼帶來影響。

對于多級容器秋泳,其處理方法非常簡單潦闲,將每一級容器都進行封裝。封裝數(shù)據(jù)結(jié)構(gòu)的實現(xiàn)細節(jié)迫皱,并暴露更穩(wěn)定的API歉闰,可以使得客戶的代碼更加穩(wěn)定。用戶通過擴展算子的方式來獲取或操作數(shù)據(jù)子集卓起,畢竟數(shù)據(jù)子集與客戶的關系更加緊密和敬。

消除幻數(shù)

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

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

封裝第一級容器

提取Cell私杜,封裝第一級容器。提取一個master的查詢接口救欧,順帶消除幻數(shù)0衰粹,使其具有更加明確的業(yè)務含義。

#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;
};

封裝第二級容器

提取GameBoard笆怠,封裝第二級容器铝耻。

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

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

此處,暫且將cells直接返回給客戶蹬刷,下文再仔細觀察客戶將遭受哪些困惑瓢捉。

客戶實現(xiàn)

計算"已標記"的單元格的數(shù)量時,實現(xiàn)非常簡單办成。

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

但是泡态,當用戶計算"未標記"的單元數(shù)量時,countUnflaggedCellscountFlaggedCells之間遭遇重復設計的苦惱迂卢。某弦。

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

參數(shù)化設計

為了消除兩者之間的重復桐汤,可以提取一個公共的函數(shù)。應用“參數(shù)化設計”靶壮,消除兩者之間的重復怔毛。

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而消除重復,可能有損程序的可讀性腾降。畢竟從用戶角度看拣度,區(qū)別true/false的確不夠清晰。但是螃壤,按照“簡單設計”的四個基本原則抗果,“消除重復”的優(yōu)先級,要高于“可讀性”的優(yōu)先級映穗;按照這個原則窖张,自然不必糾結(jié)。

簡單設計原則蚁滋,它們的優(yōu)先級和重要程度依次降低宿接。

  • 通過測試
  • 消除重復
  • 易于理解
  • 沒有冗余

重復再現(xiàn)

按照業(yè)務需求,為了實現(xiàn)應用程序的高容錯性辕录,應用程序需要暫存所有“蹦丽活”的單元格,當程序崩潰時可以據(jù)此恢復GameBoard的狀態(tài)走诞。

提取CellSaver副女,消除遺留系統(tǒng)中的全局變量。事實上蚣旱,按照DDD(領域驅(qū)動設計)的設計思維碑幅,最終CellSaver的實例需要聚集在與應用程序生命周期一致的高層對象上,在此不再冗述塞绿。

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ù)之間存在重復邏輯,程序再次引入了重復設計的壞味道异吻。

搬遷職責

為了更好地觀察兩者之間的重復裹赴,及其減少用戶調(diào)用GameBoard計數(shù)邏輯的復雜度,將遍歷的邏輯搬遷回GameBoard诀浪,并試圖在其內(nèi)部消除它們之間的重復實現(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)過一系列職責搬遷的重構(gòu)過程,用戶獲取計數(shù)的功能雷猪,將直接由GameBoard直接提供睛竣。但是,countgetAliveCells之間依然存在重復邏輯求摇,但重復的差異更易于觀察和對比射沟。

分離變化

觀察兩者之間的重復邏輯嫉你,程序存在2個變化的方向。

  • 線性遍歷的算法實現(xiàn)躏惋;
  • 用戶邏輯:計數(shù),緩存嚷辅。

提取CellCollector抽象接口簿姨,隔離GameBoard遍歷算法與其客戶邏輯(計數(shù),緩存)之間的耦合簸搞。GameBoard作為Cell的生產(chǎn)者扁位,客戶作為Cell的消費者,通過CellCollector將它們之間的邏輯相互隔離趁俊,使其它們可以相互獨立地變化域仇,相互正交。

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;
};

客戶按照CellCollector的契約寺擂,擴展定義消費單個Cell的實現(xiàn)邏輯暇务。

計數(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實現(xiàn)的邏輯與“已標記/未標記”強度耦合怔软。為了實現(xiàn)“笨严福活”的計數(shù),此時必然導致計數(shù)邏輯的重復設計挡逼。

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

顯然括改,公開的countAliveCells與私有的count之間存在重復設計,及其AliveCellCounterFlaggedCellCounter的結(jié)構(gòu)性重復家坎。

更穩(wěn)定的抽象

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

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

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

緩存邏輯

最后衩侥,緩存邏輯搬遷至客戶代碼,因為其目標存儲在客戶側(cè)矛物。

private繼承: 在CellStore::save內(nèi)部茫死,CellStoreCellCollector之間滿足李氏替換原則,堪稱C++的必殺技之一履羞。反觀諸如Java之流峦萎,不得不聲明為public繼承屡久,無論是從邏輯上,還是語義上都存在明顯的缺陷爱榔。

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)被环,存在明顯的重復設計、全局變量的依賴性详幽、晦澀的多級容器的復雜數(shù)據(jù)結(jié)構(gòu)筛欢、計數(shù)與緩存邏輯不能復用、及其諸如幻數(shù)唇聘、命名等低級編程水平版姑。

應用封裝技術,將復雜的多級容器的數(shù)據(jù)結(jié)構(gòu)分拆到GameBoard, Cell等領域?qū)ο蟪倮桑褂弥祵ο?code>State表示Cell的狀態(tài)邏輯剥险。最后,應用“分離關注點”宪肖,將遍歷算法搬遷回GameBoard實現(xiàn)代碼的高度復用表制。

在客戶端,為了降低客戶調(diào)用計數(shù)的邏輯控乾,應用好萊塢原則夫凸,搬遷計數(shù)的邏輯到GameBoard中;與之相反阱持,緩存功能因為目標存儲由客戶自身維護夭拌,應用private繼承,擴展實現(xiàn)緩存的功能衷咽。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸽扁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子镶骗,更是在濱河造成了極大的恐慌桶现,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鼎姊,死亡現(xiàn)場離奇詭異骡和,居然都是意外死亡,警方通過查閱死者的電腦和手機相寇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門慰于,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人唤衫,你說我怎么就攤上這事婆赠。” “怎么了佳励?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵休里,是天一觀的道長蛆挫。 經(jīng)常有香客問我,道長妙黍,這世上最難降的妖魔是什么悴侵? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮拭嫁,結(jié)果婚禮上畜挨,老公的妹妹穿的比我還像新娘。我一直安慰自己噩凹,他們只是感情好,可當我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布毡咏。 她就那樣靜靜地躺著驮宴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呕缭。 梳的紋絲不亂的頭發(fā)上堵泽,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天,我揣著相機與錄音恢总,去河邊找鬼迎罗。 笑死,一個胖子當著我的面吹牛片仿,可吹牛的內(nèi)容都是我干的纹安。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼砂豌,長吁一口氣:“原來是場噩夢啊……” “哼厢岂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起阳距,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤塔粒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后筐摘,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卒茬,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年咖熟,在試婚紗的時候發(fā)現(xiàn)自己被綠了圃酵。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡馍管,死狀恐怖辜昵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咽斧,我是刑警寧澤堪置,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布躬存,位于F島的核電站,受9級特大地震影響舀锨,放射性物質(zhì)發(fā)生泄漏岭洲。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一坎匿、第九天 我趴在偏房一處隱蔽的房頂上張望盾剩。 院中可真熱鬧,春花似錦替蔬、人聲如沸告私。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驻粟。三九已至,卻和暖如春凶异,著一層夾襖步出監(jiān)牢的瞬間蜀撑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工剩彬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留酷麦,地道東北人。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓喉恋,卻偏偏與公主長得像沃饶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子轻黑,可洞房花燭夜當晚...
    茶點故事閱讀 45,107評論 2 356