目錄
前言
“Clean Code That Works”,來自於Ron Jeffries這句箴言指導我們寫的代碼要整潔有效,Kent Beck把它作爲TDD(Test Driven Development)追求的目標,BoB大叔(Robert C. Martin)甚至寫了一本書來闡述他的理解。
整潔的代碼不一定能帶來更好的性能,更優的架構,但它卻更容易找到性能瓶頸,更容易理解業務需求,驅動出更好的架構。整潔的代碼是寫代碼者對自己技藝的在意,是對讀代碼者的尊重。
本文是對BOB大叔《Clen Code》[1] 一書的一個簡單抽取、分層,目的是整潔代碼可以在團隊中更容易推行,本文不會重複書中內容,僅提供對模型的一個簡單解釋,如果對於模型中的細節有疑問,請參考《代碼整潔之道》[1] 。
III 高階級
高階部分包括函數、類、系統設計相關的一些設計原則和實現模式。需要特別指出的是,我們編寫的代碼不是靜態的,而是在不斷變化的,當我們發現代碼有明顯的壞味道時,要大膽的對其進行重構,保持整潔。
3.1 函數
遵循原則:
- 別重複自己(DRY)
- 單一職責(SRP)
- 函數內所有語句在同一抽象層次
注意事項:
- 避免強行規定函數的長度
- 避免打着性能的幌子拒絕提取函數
- 避免函數名名不副實,隱藏函數真正意圖
- 避免一開始就考慮抽取函數,建議先完成業務邏輯,再重構
3.1.1 每個函數只做一件事
每個函數只做一件事,做好這件事,是單一職責在函數設計中的體現。只做一件事最難理解的是要做哪件事,怎麼樣的函數就是隻做一件事的函數呢?
提供如下建議:
- 函數名不存在and,or等連接詞,且函數名錶達意思與函數完成行爲一致
- 函數內所有語句都在同一抽象層次
- 無法再拆分出另外一個函數
反例:
WORD32 GetTotalCharge(T_Customer* tCustomer)
{
BYTE byIndex = 0;
WORD32 dwTotalAmount = 0;
WORD32 dwThisAmount = 0;
for (byIndex = 0; byIndex < MAX_NUM_RENTALS; byIndex++)
{
dwThisAmount = 0;
switch (tCustomer->atRentals[byIndex].byPriceCode)
{
case REGULAR:
dwThisAmount += 2;
if (tCustomer->atRentals[byIndex].byDaysRented > 2)
{
dwThisAmount += (tCustomer->atRentals[byIndex].byDaysRented - 2) * 2;
}
break;
case NEW_RELEASE:
dwThisAmount += tCustomer->atRentals[byIndex].byDaysRented * 3;
break;
case CHILDRENS:
dwThisAmount += 1;
if (tCustomer->atRentals[byIndex].byDaysRented > 3)
{
dwThisAmount += (tCustomer->atRentals[byIndex].byDaysRented - 3) * 3;
}
break;
default:
break;
}
dwTotalAmount += dwThisAmount;
}
return dwTotalAmount;
}
正例:
static WORD32 getRegularCharge(BYTE daysRented)
{
WORD32 price = 2;
if (daysRented > 2)
{
price += (daysRented - 2) * 2;
}
return price;
}
static WORD32 getNewReleaseCharge(BYTE daysRented)
{
return daysRented * 3;
}
static WORD32 getChildrensCharge(BYTE daysRented)
{
WORD32 price = 1;
if (daysRented > 3)
{
price += (daysRented - 3) * 3;
}
return price;
}
static WORD32 getCharge(Rental* rental)
{
typedef WORD32 (*GetChargeFun)(BYTE daysRented);
static GetChargeFun getCharge[] =
{
getRegularCharge,
getNewReleaseCharge,
getChildrensCharge,
};
return getCharge[rental->movieType](rental->daysRented);
}
#define _MIN(a,b) ((a) < (b) ? (a) : (b))
WORD32 GetTotalCharge(Customer* tCustomer)
{
BYTE index = 0;
WORD32 totalAmount = 0;
BYTE maxNum = _MIN(tCustomer->rentalNum, MAX_NUM_RENTALS);
for (index = 0; index < maxNum; index++)
{
totalAmount += getCharge(&tCustomer->rentals[index]);
}
return totalAmount;
}
3.1.2 函數內語句同一抽象層次
抽象層次是業務概念,即函數內業務邏輯在同一層級,不能把抽象與細節進行混雜。可以通過提取函數(Extract Method)或者分解函數(Compose Method)的方法將將函數重構到同一抽象層次。
反例:
static Status verify(const Erab* erab, SuccessErabList* succList, FailedErabList* failList)
{
if(!isErabIdValid(erabId)) return E_INVALID_ERAB_ID;
if( containsInSuccList(erabId, succList) ||
containsInFailList(erabId, failList)) return E_DUP_ERAB_ID;
ASSERT_SUCC_CALL(verifyQosPara(&erab->qosPara));
return SUCCESS;
}
Status filterErabs(const Erab* erab, SuccessErabList* succList, FailedErabList* failList)
{
Status status = verify(erab, succList, failList);
if(status != SUCCESS)
{
ASSERT_TRUE(failList->num < MAX_ERAB_NUM_PER_UE);
ASSERT_TRUE(!containsInFailList(erab->erabId, failList));
FailedErab failedErab = {erab->erabId, status};
failList->erabs[failList->num++] = failedErab;
return SUCCESS;
}
ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));
succList->erabs[succList->num++] = *erab;
return SUCCESS;
}
正例:
...
static Status verifyErabId( BYTE erabId
, const SuccessErabList* succList
, const FailedErabList* failList)
{
if(!isErabIdValid(erabId)) return E_INVALID_ERAB_ID;
if( containsInSuccList(erabId, succList) ||
containsInFailList(erabId, failList)) return E_DUP_ERAB_ID;
return SUCCESS;
}
static Status verify( const Erab* erab
, const SuccessErabList* succList
, const FailedErabList* failList)
{
ASSERT_SUCC_CALL(verifyErabId(erab->erabId, succList, failList));
ASSERT_SUCC_CALL(verifyQosPara(&erab->qosPara));
return SUCCESS;
}
static Status addToSuccessErabList(const Erab* erab, SuccessErabList* succList)
{
ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));
succList->erabs[succList->num++] = *erab;
return SUCCESS;
}
static Status addToFailedErabList(const Erab* erab, Status status, FailedErabList* failList)
{
ASSERT_TRUE(failList->num < MAX_ERAB_NUM_PER_UE);
ASSERT_TRUE(!containsInFailList(erab->erabId, failList));
FailedErab failedErab = {erab->erabId, status};
failList->erabs[failList->num++] = failedErab;
return SUCCESS;
}
Status filterErabs(const Erab* erab, SuccessErabList* succList, FailedErabList* failList)
{
Status status = verify(erab, succList, failList);
if(status != SUCCESS)
{
return addToFailedErabList(&failedErab, status, failList);
}
return addToSuccessErabList(erab, succList);
}
3.1.3 儘量避免三個以上的函數參數
函數最好無參數,然後是一個參數,其次兩個,儘量避免超過三個[1]。太多參數往往預示着函數職責不單一,也很難進行自動化測試覆蓋。遇到參數過多函數,考慮拆分函數或將強相關參數封裝成參數對象來減少參數。
反例:
static Status addToErabList( const Erab* erab
, Status status
, SuccessErabList* succList
, FailedErabList* failList)
{
if(status != SUCCESS)
{
ASSERT_TRUE(failList->num < MAX_ERAB_NUM_PER_UE);
ASSERT_TRUE(!containsInFailList(erab->erabId, failList));
FailedErab failedErab = {erab->erabId, status};
failList->erabs[failList->num++] = failedErab;
return SUCCESS;
}
ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));
succList->erabs[succList->num++] = *erab;
return SUCCESS;
}
正例:
static Status addToSuccessErabList(const Erab* erab, SuccessErabList* succList)
{
ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));
succList->erabs[succList->num++] = *erab;
return SUCCESS;
}
static Status addToFailedErabList(const Erab* erab, Status status, FailedErabList* failList)
{
ASSERT_TRUE(failList->num < MAX_ERAB_NUM_PER_UE);
ASSERT_TRUE(!containsInFailList(erab->erabId, failList));
FailedErab failedErab = {erab->erabId, status};
failList->erabs[failList->num++] = failedErab;
return SUCCESS;
}
3.1.4 區分查詢函數與指令函數
從數據的狀態是否被修改,可以將函數分爲兩大類:查詢函數和指令函數。查詢函數不會改變數據的狀態;指令函數會修改數據的狀態。區分二者,需要注意如下:
- 查詢函數使用
is
,should
,need
等詞增強其查詢語義 - 指令函數使用
set
,update
,add
等詞增強其指令語義 - 查詢函數往往無參數或僅有入參,考慮使用
const
關鍵詞明確查詢語義 - 忌在查詢函數體內修改數據,造成極大迷惑
- 指令函數忌用查詢語義詞彙
反例:
static Status processErabs( Erab* erab
, SuccessErabList* succList
, FailedErabList* failList)
{
ASSERT_SUCC_CALL(verifyErabId(erab->erabId, succList, failList));
ASSERT_SUCC_CALL(verifyQosPara(&erab->qosPara));
ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));
succList->erabs[succList->num++] = *erab;
return SUCCESS;
}
正例:
static Status verify( const Erab* erab
, const SuccessErabList* succList
, const FailedErabList* failList)
{
ASSERT_SUCC_CALL(verifyErabId(erab->erabId, succList, failList));
ASSERT_SUCC_CALL(verifyQosPara(&erab->qosPara));
return SUCCESS;
}
static Status addToSuccessErabList(const Erab* erab, SuccessErabList* succList)
{
ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));
succList->erabs[succList->num++] = *erab;
return SUCCESS;
}
3.1.5 消除重複的函數
“函數的第一規則是短小,第二規則是更短小[1]”,但短小是結果,不是目的,也沒有必要刻意追求短小的函數。消除代碼中的重複,自然會得到長度可觀的函數。重複可謂一切軟件腐化的萬惡之源,能識別重複並消除重複是我們軟件設計的一項基本功。
3.2 類
本節不會涉及太多關於擴展性建議,主要關注類設計的整潔、可理解性。
遵循原則:
- 別重複自己(DRY)
- S.O.L.I.D原則[2]
注意事項:
- 避免公開成員變量
- 避免類過多的方法(上帝類)
- 避免過深的繼承層次
- 避免將父類強轉爲子類
- 區分接口實現與泛化
3.2.1 設計職責單一的類
單一職責是類設計中最基本、最簡單的原則,也是最難正確使用的原則。職責單一的類必然是一些內聚的小類,內聚的小類進一步簡化了類與類之間的依賴關係,從而簡化了設計。軟件設計在一定程度上就是分離對象職責,管理對象間依賴關係。
那麼什麼是類的職責呢?Bob大叔把它定義爲“變化的原因”,職責單一的類即僅有一個引起它變化的原因的類。如何判斷一個類是否職責單一呢?給出一些建議:
- 類中數據具有相同生命週期
- 類中數據相互依賴、相互結合成一個整體概念
- 類中方法總是在操作類中數據
反例:
enum orientation {N, E, S, W};
struct Position
{
Position(int x, int y, int z, const Orientation& d);
bool operator==(const Position& rhs) const;
Position up() const;
Position down() const;
Position forward(const Orientation&) const;
Position turnLeft() const;
Position moveOn(int x, int y, int z) const;
private:
int x;
int y;
int z;
Orientation o;
};
正例:
struct Coordinate
{
Coordinate(int x, int y, int z);
Coordinate up() const;
Coordinate down() const;
Coordinate forward(const Orientation&) const;
bool operator==(const Coordinate& rhs) const;
private:
int x;
int y;
int z;
};
struct Orientation
{
Orientation turnLeft() const;
Coordinate moveOn(int x, int y, int z) const;
bool operator==(const Orientation&) const;
static const Orientation north;
static const Orientation east;
static const Orientation south;
static const Orientation west;
private:
Orientation(int order, int xFactor, int yFactor);
private:
int order;
int xFactor;
int yFactor;
};
struct Position : Coordinate, Orientation
{
Position(int x, int y, int z, const Orientation& d);
bool operator==(const Position& rhs) const;
IMPL_ROLE(Coordinate);
IMPL_ROLE(Orientation);
};
3.2.3 避免方法過多的接口
接口隔離原則(ISP)[2]就是避免接口中綁定一些用戶不需要的方法,避免用戶代碼與該接口之間產生不必要的耦合。接口中雖然沒有數據,可以根據用戶依賴或者接口職責對其拆分。清晰的接口定義不但可以減少不必要的編譯依賴,還可以改善程序的可理解性。
反例:
struct Modem
{
virtual void dial(std::string& pno) = 0;
virtual void hangup() = 0;
virtual void send(char c) = 0;
virtual void receive() = 0;
};
struct ModemImpl : Modem
{
virtual void dial(std::string& pno);
virtual void hangup();
virtual void send(char c);
virtual void receive();
};
正例:
struct DataChannel
{
virtual void send(char c) = 0;
virtual void receive() = 0;
};
struct Connection
{
virtual void dial(std::string& pno) = 0;
virtual void hangup() = 0;
};
struct ModemImpl : DataChannel, Connection
{
virtual void dial(std::string& pno);
virtual void hangup();
virtual void send(char c);
virtual void receive();
};
3.2.3 避免方法過多的類(上帝類)
方法過多的類,預示該類包含過多職責,行爲存在着大量的重複,不便於設計的組合,需要對該其進行抽象,拆分成更多職責單一,功能內聚的小類。
//java
public class SuperDashboard extends JFrame implements MetaDataUser
{
public String getCustomizerLanguagePath();
public void setSystemConfigPath(String systemConfigPath);
public String getSystemConfigDocument();
public void setSystemConfigDocument(String systemConfigDocument);
public boolean getGuruState();
public boolean getNoviceState();
public boolean getOpenSourceState();
public void showObject(MetaObject object);
public void showProgress(String s);
public boolean isMetadataDirty();
public void setIsMetadataDirty(boolean isMetadataDirty);
public Component getLastFocusedComponent();
public void setLastFocused(Component lastFocused);
public void setMouseSelectState(boolean isMouseSelected);
public boolean isMouseSelected();
public LanguageManager getLanguageManager();
public Project getProject();
public Project getFirstProject();
public Project getLastProject();
public String getNewProjectName();
public void setComponentSizes(Dimension dim);
public String getCurrentDir();
public void setCurrentDir(String newDir);
public void updateStatus(int dotPos, int markPos);
...
};
3.2.4 避免過深的繼承層次
繼承關係包括接口繼承(實現)、類繼承(泛化)兩種,接口繼承即依賴於抽象,方便程序的擴展;類繼承便於複用代碼,消除重複,但是設計中過多的繼承層次,往往導致設計邏輯的不清晰,建議繼承層次不要太深,另外,可以考慮使用組合方案替代繼承方案(比如使用策略模式替代模版方法[3])。
//Template Method
struct Removable
{
virtual ~Removable() {}
virtual Position move(Position) = 0;
private:
virtual Position doMove(Position p) const
{
return p;
}
};
struct Up : Removable
{
private:
virtual Position move(Position p) const
{
Position up = p.up();
return doMove(up);
}
};
struct UpLeft : Up
{
private:
virtual Position doMove(Position p) const
{
return p.left();
}
};
//Strategy
struct Removable
{
virtual ~Removable() {}
virtual Position move(Position) = 0;
};
struct Up : Removable
{
private:
virtual Position move(Position p) const
{
return p.up();
}
};
struct Left : Removable
{
private:
virtual Position move(Position p) const
{
return p.left();
}
};
struct JoinMovable : Removable
{
JoinMovable(const Removable&, const Removable&);
virtual Position move(Position) const;
private:
const Removable& left;
const Removable& right;
};
#define UpLeft JoinMovable(Up, Left)
3.3 系統
本節主要是系統設計中CleanCode的應用,暫不用代碼呈現。
遵循原則:
- 分而治之
- 層次清晰
注意事項:
- 避免認爲Demo就是真實的系統,二者差異很大
- 避免盲目套用流行架構,根據需求選用合適架構
- 考慮系統彈性,避免過度設計,用迭代完善架構
- 設計時考慮系統性能
3.3.1 合理的對系統進行分層
一個設計良好的系統,必然是一個層次清晰的系統。分層方法可以參考業界常用方法,比如領域驅動設計[4](DDD)將其分爲:表示層、應用層、領域層、基礎設施層。
3.3.2 定義清晰的模塊邊界及職責
分層結構還依賴於層與層之間邊界、接口、職責清晰。
3.3.3 分離構造與使用
分離構造與使用,即將對象的創建與對象的使用分離,是降低軟件複雜度的常用方法,對應領域驅動設計(DDD)中使用工廠(Factory)創建對象,使用倉庫(Repository)存儲對象。
3.3.4 考慮系統性能
系統性能是不同與功能的另一個維度,在軟件設計過程中,把性能作爲一個重要指標考慮。編碼過程中不易過早的考慮性能優化,但也不要進行明顯的性能劣化。
Clean Code Style 基礎篇
Clean Code Style 進階篇
參考文獻: