遺留代碼
假如存在一段遺留代碼粟害,使用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ù)量時,countUnflaggedCells
與countFlaggedCells
之間遭遇重復設計的苦惱迂卢。某弦。
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
直接提供睛竣。但是,count
與getAliveCells
之間依然存在重復邏輯求摇,但重復的差異更易于觀察和對比射沟。
分離變化
觀察兩者之間的重復邏輯嫉你,程序存在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
之間存在重復設計,及其AliveCellCounter
與FlaggedCellCounter
的結(jié)構(gòu)性重復家坎。
更穩(wěn)定的抽象
仔細觀察AliveCellCounter
與FlaggedCellCounter
之間的結(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)部茫死,CellStore
與CellCollector
之間滿足李氏替換原則,堪稱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)緩存的功能衷咽。