目录
前言
“Clean Code That Works”,来自于Ron Jeffries这句箴言指导我们写的代码要整洁有效,Kent Beck把它作为TDD(Test Driven Development)追求的目标,BoB大叔(Robert C. Martin)甚至写了一本书来阐述他的理解。
整洁的代码不一定能带来更好的性能,更优的架构,但它却更容易找到性能瓶颈,更容易理解业务需求,驱动出更好的架构。整洁的代码是写代码者对自己技艺的在意,是对读代码者的尊重。
本文是对BOB大叔《Clen Code》[1] 一书部分章节的一个简单抽取、分层,目的是整洁代码可以在团队中更容易推行,本文不会重复书中内容,仅提供对模型的一个简单解释,如果对于模型中的细节有疑问,请参考《代码整洁之道》[1] 。
致谢
本文由李永顺编写,并由丁辉、范璟玮、王博、金华、张超、曾亮亮、尉刚强等参与评审,并提出了非常好的修改意见,在此表示诚挚的感谢。
但由于时间的仓促及作者水平有限,如果您发现了本文的错误,或者有其他更好的意见,请第一时间告诉我们,我们将非常感激.
I 基础级
基础级主要包括代码格式、注释、物理设计三部分,这三部分比较容易做到,甚至可以制定为团队的编码规范,保证团队代码保持统一风格。
1.1 格式
遵循原则:
- 关系密切内容聚合
- 关系松散内容分隔
注意事项:
- 编码时使用等宽字体
- 替换Tab为4个空格
- 使用统一的编码格式:UTF-8, GB2312, GBK
- 使用统一的代码格式化风格。例如经典风格 K&R, BSD/Allman, GNU, Whitesmiths
- 控制行宽,不需要拖动水平滚动条查看代码
- 使用卫语句取代嵌套表达式
1.1.1 横向格式
使用空格对内容进行分隔,使用锯齿缩紧对代码段进分隔
反例:
public String toString() {
Point point=new Point();
StringBuilder sb=new StringBuilder();
sb.append("A B C D E F G H");
for(point.x=0;point.x<BOARD_LENGTH;point.x++){
sb.append('\n').append(point.x + 1);
for(point.y=0;point.y<BOARD_WIDTH;point.y++){
sb.append(' ').append(board.get(point).symbol());
}
}
sb.append('\n');
return sb.toString();
}
正例:
public String toString() {
Point point = new Point();
StringBuilder sb = new StringBuilder();
sb.append(" A B C D E F G H");
for (point.x = 0; point.x < BOARD_LENGTH; point.x++) {
sb.append('\n').append(point.x + 1);
for (point.y = 0; point.y < BOARD_WIDTH; point.y++) {
sb.append(' ').append(board.get(point).symbol());
}
}
sb.append('\n');
return sb.toString();
}
1.1.2 纵向格式
使用空行对内容进行分隔,函数或类的方法不要太长,尽量能在视野范围内一览无余
反例:
public class ComparisonCompactor {
private static final String ELLIPSIS = "...";
private static final String DELTA_END = "]";
private static final String DELTA_START = "[";
private int fContextLength;
private String fExpected;
private String fActual;
private int fPrefix;
private int fSuffix;
@SuppressWarnings("deprecation")
public String compact(String message) {
if (fExpected == null || fActual == null || areStringsEqual()) {
return Assert.format(message, fExpected, fActual);
}
findCommonPrefix();
findCommonSuffix();
String expected = compactString(fExpected);
String actual = compactString(fActual);
return Assert.format(message, expected, actual);
}
private boolean areStringsEqual() {
return fExpected.equals(fActual);
}
}
正例:
public class ComparisonCompactor {
private static final String ELLIPSIS = "...";
private static final String DELTA_END = "]";
private static final String DELTA_START = "[";
private int fContextLength;
private String fExpected;
private String fActual;
private int fPrefix;
private int fSuffix;
@SuppressWarnings("deprecation")
public String compact(String message) {
if (fExpected == null || fActual == null || areStringsEqual()) {
return Assert.format(message, fExpected, fActual);
}
findCommonPrefix();
findCommonSuffix();
String expected = compactString(fExpected);
String actual = compactString(fActual);
return Assert.format(message, expected, actual);
}
private boolean areStringsEqual() {
return fExpected.equals(fActual);
}
}
1.2 注释
遵循原则:
- 尽量不写注释,尝试用代码自阐述
- 必要时增加注释
注意事项:
- 擅用源码管理工具
- 提交代码时,日志要详细
- 避免使用中文注释(易引起字符集问题)
- 确认编译器支持
//
-
/*
之后有空格,*/
之前有空格
1.2.1 好的注释
- 法律、版权信息
# /* **************************************************************************
# * *
# * (C) Copyright Paul Mensonides 2002.
# * Distributed under the Boost Software License, Version 1.0. (See
# * accompanying file LICENSE_1_0.txt or copy at
# * http://www.boost.org/LICENSE_1_0.txt)
# * *
# ************************************************************************** */
#
# /* See http://www.boost.org for most recent version. */
#
# ifndef BOOST_PREPROCESSOR_SEQ_FOR_EACH_HPP
# define BOOST_PREPROCESSOR_SEQ_FOR_EACH_HPP
#
# include <boost/preprocessor/arithmetic/dec.hpp>
# include <boost/preprocessor/config/config.hpp>
# include <boost/preprocessor/repetition/for.hpp>
# include <boost/preprocessor/seq/seq.hpp>
# include <boost/preprocessor/seq/size.hpp>
# include <boost/preprocessor/tuple/elem.hpp>
# include <boost/preprocessor/tuple/rem.hpp>
#
- 陷阱、警示
#if (defined(BOOST_MSVC) || (defined(BOOST_INTEL) && defined(_MSC_VER))) && _MSC_VER >= 1300
//
// MSVC supports types which have alignments greater than the normal
// maximum: these are used for example in the types __m64 and __m128
// to provide types with alignment requirements which match the SSE
// registers. Therefore we extend type_with_alignment<> to support
// such types, however, we have to be careful to use a builtin type
// whenever possible otherwise we break previously working code:
// see http://article.gmane.org/gmane.comp.lib.boost.devel/173011
// for an example and test case. Thus types like a8 below will
// be used *only* if the existing implementation can't provide a type
// with suitable alignment. This does mean however, that type_with_alignment<>
// may return a type which cannot be passed through a function call
// by value (and neither can any type containing such a type like
// Boost.Optional). However, this only happens when we have no choice
// in the matter because no other "ordinary" type is available.
//
- 意图解释
// Borland specific version, we have this for two reasons:
// 1) The version above doesn't always compile (with the new test cases for example)
// 2) Because of Borlands #pragma option we can create types with alignments that are
// greater that the largest aligned builtin type.
namespace align
{
#pragma option push -a16
struct a2{ short s; };
struct a4{ int s; };
struct a8{ double s; };
struct a16{ long double s; };
#pragma option pop
}
- 性能优化代码
// Fast version of "hash = (65599 * hash) + c"
hash = (hash << 6) + (hash << 16) - hash + c;
- 不易理解代码
// kk::mm::ss, MM dd, yyyy
std::string timePattern = "\\d{2}:\\d{2}:\\d{2}, \\d{2} \\d{2}, \\d{4}";
1.2.2 不好的注释
- 日志型注释 -> 删除,使用源码管理工具记录
反例:
正例:/** *c00kiemon5ter 2015-9-20 add SquareState * c00kiemon5ter 2015-10-1 change the symbol */ public enum SquareState { BLACK('●'), WHITE('○'), // BLACK('x'), // WHITE('o'), PSSBL('.'), EMPTY(' '); private final char symbol; SquareState(char symbol) { this.symbol = symbol; } public char symbol() { return this.symbol; } }
public enum SquareState { BLACK('●'), WHITE('○'), PSSBL('.'), EMPTY(' '); private final char symbol; SquareState(char symbol) { this.symbol = symbol; } public char symbol() { return this.symbol; } }
$git commit -m "change BLACK symbol from x to ●, WHITE from ○ to O"
-
归属、签名 -> 删除,源码管理工具自动记录
反例:/** * @author c00kiemon5ter */ public enum Player { BLACK(SquareState.BLACK), WHITE(SquareState.WHITE); ... }
正例:
public enum Player { BLACK(SquareState.BLACK), WHITE(SquareState.WHITE); ... }
$git config --global user.name c00kiemon5ter
注释掉的代码 -> 删除,使用源码管理工具保存
反例:
public Point evalMove() {
AbstractSearcher searcher;
Evaluation evalfunc;
searcher = new NegaMax();
//evalfunc = new ScoreEval();
evalfunc = new ScoreDiffEval();
//evalfunc = new ScoreCornerWeightEval();
return searcher.simpleSearch(board, player, depth, evalfunc).getPoint();
}
正例:
public Point evalMove() {
AbstractSearcher searcher;
Evaluation evalfunc;
searcher = new NegaMax();
evalfunc = new ScoreDiffEval();
return searcher.simpleSearch(board, player, depth, evalfunc).getPoint();
}
-
函数头 -> 尝试使用更好的函数名,更好参数名,更少参数替换注释
反例:/*********************************************************************** * function Name: GetCharge * function description:get total Rental charge * return value:WORD32 * other * date version author contents * ----------------------------------------------- * 2014/11/28 V1.0 XXXX XXXX *************************************************************************/ WORD32 GetCharge(T_Customer* tCustomer) { ... }
正例:
WORD32 GetTotalRentalCharge(Customer* customer) { ... }
位置标记 -> 删除,简化逻辑
反例:
double getPayAmount(){
double result;
if(isDead){
result = deadAmount();
}
else{//!isDead
if(isSeparated){
result = separatedAmount();
}
else{ //!isSeparated && !//!isDead
if(isRetired){
result = retiredAmount();
}
else{ //!isSeparated && !//!isDead && !isRetired
result = normalPayAmount();
}
}
}
return result;
}
正例:
double getPayAmount(){
if(isDead) return deadAmount();
if(isSeparated) return separatedAmount();
if(isRetired) return retiredAmount();
return normalPayAmount();
}
- 过时、误导性注释 -> 删除
反例:
// Utility method that returns when this.closed is true. Throws an exception
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis) throws Exception
{
if(!closed)
{
wait(timeoutMillis);
if(!closed)
throw new Exception("MockResponseSender could not be closed");
}
}
正例:
public synchronized void waitForClose(final long timeoutMillis) throws Exception
{
if(!closed)
{
wait(timeoutMillis);
if(!closed)
throw new Exception("MockResponseSender could not be closed");
}
}
- 多余、废话注释 -> 删除
反例:
class GTEST_API_ AssertionResult
{
public:
// Copy constructor.
// Used in EXPECT_TRUE/FALSE(assertion_result).
AssertionResult(const AssertionResult& other);
// Used in the EXPECT_TRUE/FALSE(bool_expression).
explicit AssertionResult(bool success) : success_(success) {}
// Returns true iff the assertion succeeded.
operator bool() const { return success_; } // NOLINT
private:
// Stores result of the assertion predicate.
bool success_;
};
正例:
class GTEST_API_ AssertionResult
{
public:
AssertionResult(const AssertionResult& other);
explicit AssertionResult(bool success) : success_(success) {}
operator bool() const { return success_; }
private:
bool success_;
};
1.3 物理设计
遵循原则[2]:
- 头文件编译自满足(C/C++)
- 文件职责单一
- 文件最小依赖
- 文件信息隐藏
注意事项:
- 包含文件时,确保路径名、文件名大小写敏感
- 文件路径分隔符使用
/
,不使用\
- 路径名一律使用小写、下划线(
_
)或中划线风格(-
) - 文件名与程序实体名称一致
1.3.1 头文件编译自满足(C/C++)
对于C/C++语言头文件编译自满足,即头文件可以单独编译成功。
反例:
#ifndef _INCL_POSITION_H_
#define _INCL_POSITION_H_
#include "base/Role.h"
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);
};
#endif
正例:
#ifndef _INCL_POSITION_H_
#define _INCL_POSITION_H_
#include "Coordinate.h"
#include "Orientation.h"
#include "base/Role.h"
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);
};
#endif
1.3.2 文件设计职责单一
文件设计职责单一,是指文件中对于对于用户公开的信息,应该是一个概念,避免把不相关的概念糅合在一个文件中,文件间增加不必要的依赖
反例:
UnmannedAircraft.h
#ifndef _INCL_UNMANNED_AIRCRAFT_H_
#define _INCL_UNMANNED_AIRCRAFT_H_
#include "Coordinate.h"
#include "Orientation.h"
#include "base/Role.h"
struct Instruction;
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);
};
struct UnmannedAircraft
{
UnmannedAircraft();
void on(const Instruction&);
const Position& getPosition() const;
private:
Position position;
};
#endif
正例:
Position.h
#ifndef _INCL_POSITION_H_
#define _INCL_POSITION_H_
#include "Coordinate.h"
#include "Orientation.h"
#include "base/Role.h"
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);
};
#endif
UnmannedAircraft.h
#ifndef _INCL_UNMANNED_AIRCRAFT_H_
#define _INCL_UNMANNED_AIRCRAFT_H_
#include "Position.h"
struct Instruction;
struct UnmannedAircraft
{
UnmannedAircraft();
void on(const Instruction&);
const Position& getPosition() const;
private:
Position position;
};
#endif
1.3.3 仅包含需要的文件
- 文件设计时,应遵循最小依赖原则,仅包含必须的文件即可。
反例:
正例:#ifndef _INCL_UNMANNED_AIRCRAFT_H_ #define _INCL_UNMANNED_AIRCRAFT_H_ #include "Position.h" #include "Orientation.h" #include "Coordinate.h" #include "Instruction.h" struct UnmannedAircraft { UnmannedAircraft(); void on(const Instruction&); const Position& getPosition() const; private: Position position; }; #endif
#ifndef _INCL_UNMANNED_AIRCRAFT_H_ #define _INCL_UNMANNED_AIRCRAFT_H_ #include "Position.h" struct Instruction; struct UnmannedAircraft { UnmannedAircraft(); void on(const Instruction&); const Position& getPosition() const; private: Position position; }; #endif
- 特别的,对于C++而言,可以使用类或者结构体前置声明,而不包含头文件,降低编译依赖。该类依赖被称为弱依赖,编译时不需要知道实体的真实大小,仅提供一个符号即可,主要有:
- 指针
- 引用
- 返回值
- 函数参数
反例:
#ifndef _INCL_INSTRUCTION_H_
#define _INCL_INSTRUCTION_H_
#include "Coordinate.h"
#include "Orientation.h"
struct Instruction
{
virtual void exec(Coordinate&, Orientation&) const = 0;
virtual ~Instruction() {}
};
#endif
正例:
#ifndef _INCL_INSTRUCTION_H_
#define _INCL_INSTRUCTION_H_
struct Coordinate;
struct Orientation;
struct Instruction
{
virtual void exec(Coordinate&, Orientation&) const = 0;
virtual ~Instruction() {}
};
#endif
1.3.4 仅公开用户需要的接口
- 文件设计时,应遵循信息隐藏原则,仅公开用户需要的接口,对于其他信息尽量隐藏,以减少不必要的依赖。 特别的,对于C语言头文件中仅公开用户需要接口,其他函数隐藏在源文件中。
反例:
struct RepeatableInstruction : Instruction
{
RepeatableInstruction(const Instruction&, int n);
virtual void exec(Coordinate&, Orientation&) const;
bool isOutOfBound() const;
private:
const Instruction& ins;
const int n;
};
正例:
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;
};
-
特别的,对于C可以使用static对编译单元中全局变量、函数等进行隐藏,对于支持面向对象语言则使用其封装特性即可。
反例:
BOOLEAN isGbr(BYTE qci) { return qci >= 1 && qci <= 4; } BOOLEAN isGbrBitRateValid(const GbrIE* gbrIE) { ASSERT_VALID_PTR_BOOL(gbrIE); return gbrIE->dlGbr <= gbrIE->dlMbr && gbrIE->ulGbr <= gbrIE->ulMbr; } BOOLEAN isGbrIEValid(const QosPara* qosPara) { if(!isGbr(qosPara->qci)) return TRUE; if(qosPara->grbIEPresent == 0) return TRUE; return isGbrBitRateValid(&qosPara->gbrIE); }
正例:
static BOOLEAN isGbr(BYTE qci) { return qci >= 1 && qci <= 4; } static BOOLEAN isGbrBitRateValid(const GbrIE* gbrIE) { ASSERT_VALID_PTR_BOOL(gbrIE); return gbrIE->dlGbr <= gbrIE->dlMbr && gbrIE->ulGbr <= gbrIE->ulMbr; } BOOLEAN isGbrIEValid(const QosPara* qosPara) { if(!isGbr(qosPara->qci)) return TRUE; if(qosPara->grbIEPresent == 0) return TRUE; return isGbrBitRateValid(&qosPara->gbrIE); }
Clean Code Style 进阶篇
Clean Code Style 高阶篇
参考文献: