Clean Code Style - 基礎篇

目錄

前言

“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 好的註釋

  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>
#
  1. 陷阱、警示
#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.
//
  1. 意圖解釋
// 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
    }
  1. 性能優化代碼
// Fast version of "hash = (65599 * hash) + c"
hash = (hash << 6) + (hash << 16) - hash + c;
  1. 不易理解代碼
// kk::mm::ss, MM dd, yyyy
std::string timePattern = "\\d{2}:\\d{2}:\\d{2}, \\d{2} \\d{2}, \\d{4}";

1.2.2 不好的註釋

  1. 日誌型註釋 -> 刪除,使用源碼管理工具記錄
    反例:
    /**
     *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"
  1. 歸屬、簽名 -> 刪除,源碼管理工具自動記錄
    反例:

    /**
     * @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
    
  2. 註釋掉的代碼 -> 刪除,使用源碼管理工具保存
    反例:

    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();
    }
  1. 函數頭 -> 嘗試使用更好的函數名,更好參數名,更少參數替換註釋
    反例:

    /***********************************************************************
    * 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)
    {
        ...
    }
    
  2. 位置標記 -> 刪除,簡化邏輯
    反例:

 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();
    }
  1. 過時、誤導性註釋 -> 刪除
    反例:
// 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"); 
    }
}
  1. 多餘、廢話註釋 -> 刪除
    反例:
    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 僅包含需要的文件

  1. 文件設計時,應遵循最小依賴原則,僅包含必須的文件即可。
    反例:
    #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
    
  2. 特別的,對於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 僅公開用戶需要的接口

  1. 文件設計時,應遵循信息隱藏原則,僅公開用戶需要的接口,對於其他信息儘量隱藏,以減少不必要的依賴。 特別的,對於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;
    };
    
  2. 特別的,對於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 高階篇

參考文獻:


  1. Robert C.Martin-代碼整潔之道

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章