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 名稱區分問題域與實現域

  1. 現代程序設計期望程序能很好的描述領域知識、業務場景,讓開發者和領域專家可以更好的交流,該部分的命名要更貼近問題域。

    #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;
    }
    
  2. 對於操作實現層面,儘量使用計算機術語、模式名、算法名,畢竟大部分維護工作都是程序員完成。

    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 風格統一的測試場景描述

  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());
}
  1. 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 高階篇

參考文獻:


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

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