目录
前言
“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 进阶篇
参考文献: