目錄
前言
“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_;
};
```
正例:
```cpp
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 物理設計
遵循原則[^4]:
+ 頭文件編譯自滿足(C/C++)
+ 文件職責單一
+ 文件最小依賴
+ 文件信息隱藏
注意事項:
+ 包含文件時,確保路徑名、文件名大小寫敏感
+ 文件路徑分隔符使用`/`,不使用`\ `
+ 路徑名一律使用小寫、下劃線(`_`)或中劃線風格(`-`)
+ 文件名與程序實體名稱一致
#### 1.3.1 頭文件編譯自滿足(C/C++)
對於C/C++語言頭文件編譯自滿足,即頭文件可以單獨編譯成功。
反例:
```cpp
#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++而言,可以使用類或者結構體前置聲明,而不包含頭文件,降低編譯依賴。該類依賴被稱爲弱依賴,編譯時不需要知道實體的真實大小,僅提供一個符號即可,主要有:
- 指針
- 引用
- 返回值
- 函數參數
反例:
```cpp
#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
```
正例:
```cpp
#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 高階篇
參考文獻: