遺留代碼
假如存在一段遺留代碼,使用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í),countUnflaggedCells與countFlaggedCells之間遭遇重復(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直接提供。但是,count與getAliveCells之間依然存在重復(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ì),及其AliveCellCounter與FlaggedCellCounter的結(jié)構(gòu)性重復(fù)。
更穩(wěn)定的抽象
仔細(xì)觀(guān)察AliveCellCounter與FlaggedCellCounter之間的結(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)部,CellStore與CellCollector之間滿(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)緩存的功能。