Clean Code Style - 进阶篇
目录
前言
“Clean Code That Works”,来自于Ron Jeffries这句箴言指导我们写的代码要整洁有效,Kent Beck把它作为TDD(Test Driven Development)追求的目标,BoB大叔(Robert C. Martin)甚至写了一本书来阐述他的理解。
整洁的代码不一定能带来更好的性能,更优的架构,但它却更容易找到性能瓶颈,更容易理解业务需求,驱动出更好的架构。整洁的代码是写代码者对自己技艺的在意,是对读代码者的尊重。
本文是对BOB大叔《Clen Code》[1] 一书的一个简单抽取、分层,目的是整洁代码可以在团队中更容易推行,本文不会重复书中内容,仅提供对模型的一个简单解释,如果对于模型中的细节有疑问,请参考《代码整洁之道》[1] 。
II 进阶级
进阶级主要包括命名、测试设计、数据结构及对象设计,该部分要求编码时关注到更多细节,从语义层次提升代码的可理解性。
2.1 命名
命名是提高代码表达力最有效的方式之一。我们都应该抱着谨慎的态度,像给自己孩子取名字一样,为其命名。好的名字,总能令人眼前一亮,令阅读者拍案叫绝,但好的名字往往意味着更多的思考,更多次尝试,体现着我们对代码的一种态度。随着我们对业务的进一步了解,发现名字不合适时,要大胆的重构他。
遵循原则:
- Baby Names,宁思三分,不强一秒
- Min-length + Max-information
- 结构体/类名用名词或名词短语
- 接口使用名词或形容词
- 函数/方法使用动词或动词短语
注意事项:
- 避免使用汉语拼音
- 避免使用前缀
- 避免包含数据结构
- 避免使用数字序列
- 善用词典
- 善用重构工具
- 避免使用不常用缩写
2.1.1 关注点
- 文件夹|包
- 文件
- 函数|类方法|类
- 参数|变量
2.1.2 风格统一的命名规范
社区有很多种类的命名规范,很难找到一种令所有人都满意,如下规范仅供参考:
Type | Examples |
---|---|
namespace/package | std, details, lang |
struct/union/class | List, Map, HttpServlet |
function/method | add, binarySearch, lastIndexOfSubList |
macro/enum/constant | MAX_ERAB_NUM, IDLE, UNSTABLE |
variable | i, key, expectedTimer |
type | T, KEY, MESSAGE |
团队可以根据实际情况进行改动,但团队内命名风格要一致。
2.1.3 避免在命名中使用编码
在程序设计的历史中,在命名中使用编码曾风靡一时,最为出名的为匈牙利命名法,把类型编码到名字中,使用变量时默认携带了它的类型,使程序员对变量的类型和属性有更直观的了解。
基于如下原因,现代编码习惯,不建议命名中使用编码:
- 现代编码习惯更倾向于短的函数、短的类,变量尽量在视野的控制范围内;
- 业务频繁的变化,变量的类型可能随之变化,变量中的编码信息就像过时的注释信息一样误导人;
- 携带编码的变量往往不可读
- 现代IDE具有强大的着色功能,局部变量与成员变量容易区分
由于历史原因,很多遗留代码仍然使用匈牙利命名法,修改代码建议风格一致,新增代码建议摒弃
- 匈牙利命名示例
反例:
正例:void AddRental(T_Customer* tCustomer, BYTE byPriceCode, BYTE byDaysRented) { tCustomer->atRentals[tCustomer->byNum].byPriceCode = byPriceCode; tCustomer->atRentals[tCustomer->byNum].byDaysRented = byDaysRented; tCustomer->byNum++; }
static void doAddRental(Rental* rental, BYTE movieType, BYTE daysRented) { rental->movieType = movieType; rental->daysRented = daysRented; } void AddRental(Customer* customer, BYTE movieType, BYTE daysRented) { doAddRental(customer->rentals[customer->rentalNum++], movieType, daysRented); }
- 成员变量前缀示例
反例:
正例: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 m_x; int m_y; int m_z; };
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 IInstruction { virtual void exec(CCoordinate&, COrientation&) const = 0; virtual ~Instruction() {} }; struct CRepeatableInstruction : IInstruction { CRepeatableInstruction(const IInstruction&, int n); private: virtual void exec(CCoordinate&, COrientation&) const; bool isOutOfBound() const; private: const IInstruction& ins; const int n; };
struct Instruction { virtual void exec(Coordinate&, Orientation&) const = 0; virtual ~Instruction() {} }; struct RepeatableInstruction : Instruction { RepeatableInstruction(const Instruction&, int n); private: virtual void exec(Coordinate&, Orientation&) const; bool isOutOfBound() const; private: const Instruction& ins; const int n; };
2.1.3 名称区分问题域与实现域
-
现代程序设计期望程序能很好的描述领域知识、业务场景,让开发者和领域专家可以更好的交流,该部分的命名要更贴近问题域。
#define _up Direction::up() #define _down Direction::down() #define _left Direction::left() #define _right Direction::right() #define _left_up JoinMovable(_left, _up) #define _left_down JoinMovable(_left, _down) #define _right_up JoinMovable(_right, _up) #define _right_down JoinMovable(_right, _down) const Positions Reversi::gitAvailablePositions(Position p) { Positions moves; moves = find(p, _up) + find(p, _down) + find(p, _left) + find(p, _right) + find(p, _left_up) + find(p, _left_down) + find(p, _right_up) + find(p, _right_down); return moves; }
-
对于操作实现层面,尽量使用计算机术语、模式名、算法名,毕竟大部分维护工作都是程序员完成。
template <class ForwardIter, class Tp> bool binary_search( ForwardIter first , ForwardIter last , const Tp& val) { ForwardIter i = boost::detail::lower_bound(first, last, val); return i != last && !(val < *i); }
2.2 测试
整洁的测试是开发过程中比较难做到的,很多团队把测试代码视为二等公民,对待测试代码不想工程代码那样严格要求,于是出现大量重复代码、名称名不副实、测试函数冗长繁杂、测试用例执行效率低下,某一天发现需要花费大量精力维护测试代码,开始抱怨测试代码。
遵循原则:
- F.I.R.S.T原则
- 测试用例单一职责,每个测试一个概念
- 测试分层(UT, CT, FT, ST...),不同层间用例互补,同一层内用例正交
- 像对待工程代码一样对待测试用例
注意事项:
- 善用测试框架管理测试用例
- 选择具有可移植性测试框架
- 选择业务表达力更强的测试框架
- 关注测试用例有效性
- 关注测试用例执行速度
2.2.1 风格统一的测试场景描述
- Given-When-Then风格
- (Given) some context
- (When) some action is carried out
- (Then) a particular set of observable consequences should obtain
TEST(BoardTest, given_position_a1_placed_WHITE_when_turn_over_then_a1_change_status_to_BLACK)
{
Board board;
board.place(a1, WHITE);
board.turnOver(a1);
ASSERT_TRUE(board.at(a1).isOccupied());
ASSERT_TRUE(board.at(a1).isBlack());
}
- Should-When-Given风格
TEST(BoardTest, should_a1_status_change_to_BLACK_when_turn_over_given_a1_placed_WHITE)
{
Board board;
board.place(a1, WHITE);
board.turnOver(a1);
ASSERT_TRUE(board.at(a1).isOccupied());
ASSERT_TRUE(board.at(a1).isBlack());
}
2.2.2 每个测试用例测试一个场景
好的测试用例更像一份功能说明文档,各种场景的描述应该职责单一,并完整全面。每个测试用例一个测试场景,既利于测试失败时,问题排查,也可以避免测试场景遗留。
反例:
TEST_F(UnmannedAircraftTest, when_receive_a_instruction_aircraft_should_move_a_step)
{
aircraft.on(UP);
ASSERT_TRUE(Position(0,0,1,N) == aircraft.getPosition());
aircraft.on(DOWN);
ASSERT_TRUE(Position(0,0,0,N) == aircraft.getPosition());
}
正例:
TEST_F(UnmannedAircraftTest, when_receive_instruction_UP_aircraft_should_up_a_step)
{
aircraft.on(UP);
ASSERT_TRUE(Position(0,0,1,N) == aircraft.getPosition());
}
TEST_F(UnmannedAircraftTest, when_receive_instruction_DOWN_aircraft_should_down_a_step)
{
aircraft.on(UP);
aircraft.on(DOWN);
ASSERT_TRUE(Position(0,0,0,N) == aircraft.getPosition());
}
2.2.3 一组测试场景封装为一个测试套
所有测试用例不应该平铺直叙,在同一个层次,可以使用测试套将其分层,便于用例理解与管理。
反例:
TEST(GameOfLiftTest, should_not_be_alive_when_a_cell_be_created)
{
ASSERT_EQ(cell.status(), DEAD);
}
TEST(GameOfLiftTest, should_a_dead_cell_becomes_to_alive_cell)
{
cell.live();
ASSERT_EQ(cell.status(), ALIVE);
}
TEST(GameOfLiftTest, should_given_cells_equals_expect_cells_given_no_neighbour_alive_cell)
{
int GIVEN_CELLS[] =
{
0, 0, 0,
0, 1, 0,
0, 0, 0,
};
int EXPECT_CELLS[] =
{
0, 0, 0,
0, 0, 0,
0, 0, 0,
};
ASSERT_UNIVERSAL_EQ(GIVEN_CELLS, EXPECT_CELLS);
}
正例:
TEST(CellTest, should_not_be_alive_when_a_cell_be_created)
{
ASSERT_EQ(cell.status(), DEAD);
}
TEST(CellTest, should_a_dead_cell_becomes_to_alive_cell)
{
cell.live();
ASSERT_EQ(cell.status(), ALIVE);
}
TEST(UniversalTest, should_given_cells_equals_expect_cells_given_no_neighbour_alive_cell)
{
int GIVEN_CELLS[] =
{
0, 0, 0,
0, 1, 0,
0, 0, 0,
};
int EXPECT_CELLS[] =
{
0, 0, 0,
0, 0, 0,
0, 0, 0,
};
ASSERT_UNIVERSAL_EQ(GIVEN_CELLS, EXPECT_CELLS);
}
2.2.4 尝试使用DSL表达测试场景
尝试使用DSL描述测试用例,领域专家可以根据测试用例表述,判断业务是否正确。测试DSL可能需要抽取业务特征,设计、开发测试框架。
TEST_AIRCRAFT(aircraft_should_up_a_step_when_receive_instruction_UP)
{
WHEN_AIRCRAFT_EXECUTE_INSTRUCTION(UP);
THE_AIRCRAFT_SHOULD_BE_AT(Position(0,0,1,N));
}
TEST_AIRCRAFT(aircraft_should_down_a_step_when_receive_instruction_DOWN)
{
WHEN_AIRCRAFT_EXECUTE_INSTRUCTION(UP);
THEN_AIRCRAFT_EXECUTE_INSTRUCTION(DOWN);
THE_AIRCRAFT_SHOULD_BE_AT(Position(0,0,0,N));
}
2.3 对象和数据结构
此处不讨论面向对象与面向过程设计范式的优劣,仅区分对象与数据结构使用场景与注意事项。
遵循原则:
- 对象隐藏数据,公开行为
- 数据结构公开数据,无行为
注意事项:
- 数据结构与对象不可混用
- 避免在对象中使用getter/setter方法
- 避免在对象中暴露数据
- 避免在数据结构中添加行为
2.3.1 区分数据结构与对象的使用场景
对象主要关注“做什么”,关心如何对数据进行抽象;数据结构主要表示数据“是什么”,过程式主要关注“怎么做”,关心如何对数据进行操作。二者都可以很好的解决问题,相互之间并不冲突。
在使用场景上:
- 若数据类型频变,可以考虑使用对象
示例:struct Shape { virtual double area() = 0; }; struct Square : Shape { virtual double area(); private: Point topLeft; double side; }; struct Rectangle : Shape { virtual double area(); private: Point topLeft; double height; double width; }; struct Circle : Shape { virtual double area(); private: Point center; double radius; };
- 若类型行为频变,可以考虑使用数据结构
示例:
现实中,我们会结合对象与数据结构使用,而不是二分法将其对立。struct Circle { Point center; double radius; }; double calcArea(const Circle*); double calcPrimeter(const Circle*); double calcVolume(const Circle*);
2.3.2 避免在对象中使用getter & setter
面向对象较面向过程的一个很大的不同是对象行为的抽象,较数据“是什么”,更关注对象“做什么”,所以,在对象中应该关注对象对外提供的行为是什么,而不是通过getter&setter暴露数据,通过其他的服务、函数、方法操作对象。如果数据被用来传送(即DTO,Data Transfer Objects),使用贫血的数据结构即可。
反例:
struct Coordinate
{
void setX(int x);
void setY(int y);
void setZ(int z);
int getX() const;
int getY() const;
int getZ() const;
private:
int x;
int y;
int z;
};
正例:
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;
};
//google code style
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;
};
2.3.3 避免在对象中暴露成员变量
面向对象为外部提供某种服务,内部的数据类型应该被封装,或者说隐藏,不应为了访问便利,暴露成员变量,如果需要频繁被调用,请考虑为DTO,使用数据结构。
反例:
struct Coordinate
{
Coordinate up() const;
Coordinate down() const;
Coordinate forward(const Orientation&) const;
bool operator==(const Coordinate& rhs) const;
int x;
int y;
int z;
};
正例:
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;
};
//Coordinate is DTO
struct Coordinate
{
int x;
int y;
int z;
};
2.3.4 避免在数据结构中添加行为
数据结构表示数据“是什么”,承载着数据的特征、属性。为数据结构增加一些“做什么”的行为,不但让数据结构变的不伦不类,也会让使用者感到迷惑,不知道该调用它的方法还是作为DTO使用。对于特殊的构造函数或者拷贝构造函数、赋值操作符除外。
反例:
struct QosPara
{
BYTE grbIEPresent;
BYTE qci;
ArpIE arp;
GbrIE gbrIE;
bool isGbrIEValid() const;
bool isGbr() const;
};
正例:
typedef struct QosPara
{
BYTE grbIEPresent;
BYTE qci;
ArpIE arp;
GbrIE gbrIE;
}QosPara;
//CPP style
struct QosParaChecker
{
bool isGbrIEValid() const;
bool isGbr() const;
private:
QosPara qos;
};
//C style
BOOLEAN isGbrIEValid(const QosPara*);
BOOLEAN isGbr(const QosPara*);
Clean Code Style 基础篇
Clean Code Style 高阶篇
参考文献: