Clean Code Style - 高階篇

目錄

前言

“Clean Code That Works”,來自於Ron Jeffries這句箴言指導我們寫的代碼要整潔有效,Kent Beck把它作爲TDD(Test Driven Development)追求的目標,BoB大叔(Robert C. Martin)甚至寫了一本書來闡述他的理解。
整潔的代碼不一定能帶來更好的性能,更優的架構,但它卻更容易找到性能瓶頸,更容易理解業務需求,驅動出更好的架構。整潔的代碼是寫代碼者對自己技藝的在意,是對讀代碼者的尊重。
本文是對BOB大叔《Clen Code》[1] 一書的一個簡單抽取、分層,目的是整潔代碼可以在團隊中更容易推行,本文不會重複書中內容,僅提供對模型的一個簡單解釋,如果對於模型中的細節有疑問,請參考《代碼整潔之道》[1]


III 高階級

高階部分包括函數、類、系統設計相關的一些設計原則和實現模式。需要特別指出的是,我們編寫的代碼不是靜態的,而是在不斷變化的,當我們發現代碼有明顯的壞味道時,要大膽的對其進行重構,保持整潔。

3.1 函數

遵循原則:

  • 別重複自己(DRY)
  • 單一職責(SRP)
  • 函數內所有語句在同一抽象層次

注意事項:

  • 避免強行規定函數的長度
  • 避免打着性能的幌子拒絕提取函數
  • 避免函數名名不副實,隱藏函數真正意圖
  • 避免一開始就考慮抽取函數,建議先完成業務邏輯,再重構

3.1.1 每個函數只做一件事

每個函數只做一件事,做好這件事,是單一職責在函數設計中的體現。只做一件事最難理解的是要做哪件事,怎麼樣的函數就是隻做一件事的函數呢?
提供如下建議:

  1. 函數名不存在and,or等連接詞,且函數名錶達意思與函數完成行爲一致
  2. 函數內所有語句都在同一抽象層次
  3. 無法再拆分出另外一個函數

反例:

WORD32 GetTotalCharge(T_Customer* tCustomer)
{
    BYTE   byIndex       = 0;
    WORD32 dwTotalAmount = 0;
    WORD32 dwThisAmount  = 0;

    for (byIndex = 0; byIndex < MAX_NUM_RENTALS; byIndex++)
    {
        dwThisAmount = 0;  

        switch (tCustomer->atRentals[byIndex].byPriceCode)
        {
        case REGULAR:
            dwThisAmount += 2;
            if (tCustomer->atRentals[byIndex].byDaysRented > 2)
            {
                dwThisAmount += (tCustomer->atRentals[byIndex].byDaysRented - 2) * 2;
            }
            break;
        case NEW_RELEASE:
            dwThisAmount += tCustomer->atRentals[byIndex].byDaysRented * 3;
            break;
        case CHILDRENS:
            dwThisAmount += 1;
            if (tCustomer->atRentals[byIndex].byDaysRented > 3)
            {
                dwThisAmount += (tCustomer->atRentals[byIndex].byDaysRented - 3) * 3;
            }
            break;
        default:
            break;
        }

        dwTotalAmount += dwThisAmount;
    }

    return dwTotalAmount;
}

正例:

static WORD32 getRegularCharge(BYTE daysRented)
{
    WORD32 price = 2;
    if (daysRented > 2)
    {
        price += (daysRented - 2) * 2;
    }

    return price;
}

static WORD32 getNewReleaseCharge(BYTE daysRented)
{
    return daysRented * 3;
}

static WORD32 getChildrensCharge(BYTE daysRented)
{
    WORD32 price = 1;
    if (daysRented > 3)
    {
        price += (daysRented - 3) * 3;
    }

    return price;
}

static WORD32 getCharge(Rental* rental)
{
    typedef WORD32 (*GetChargeFun)(BYTE daysRented);
    static GetChargeFun getCharge[] =
    {
        getRegularCharge,
        getNewReleaseCharge,
        getChildrensCharge,
    };

    return getCharge[rental->movieType](rental->daysRented);
}

#define _MIN(a,b) ((a) < (b) ? (a) : (b))

WORD32 GetTotalCharge(Customer* tCustomer)
{
    BYTE   index       = 0;
    WORD32 totalAmount = 0;

    BYTE maxNum = _MIN(tCustomer->rentalNum, MAX_NUM_RENTALS);
    for (index = 0; index < maxNum; index++)
    {
        totalAmount += getCharge(&tCustomer->rentals[index]);
    }

    return totalAmount;
}

3.1.2 函數內語句同一抽象層次

抽象層次是業務概念,即函數內業務邏輯在同一層級,不能把抽象與細節進行混雜。可以通過提取函數(Extract Method)或者分解函數(Compose Method)的方法將將函數重構到同一抽象層次。
反例:

static Status verify(const Erab* erab, SuccessErabList* succList, FailedErabList* failList)
{
    if(!isErabIdValid(erabId)) return E_INVALID_ERAB_ID;

    if( containsInSuccList(erabId, succList) ||
        containsInFailList(erabId, failList)) return E_DUP_ERAB_ID;

    ASSERT_SUCC_CALL(verifyQosPara(&erab->qosPara));

    return SUCCESS;
}

Status filterErabs(const Erab* erab, SuccessErabList* succList, FailedErabList* failList)
{
    Status status = verify(erab, succList, failList);

    if(status != SUCCESS)
    {
        ASSERT_TRUE(failList->num < MAX_ERAB_NUM_PER_UE);
        ASSERT_TRUE(!containsInFailList(erab->erabId, failList));

        FailedErab failedErab = {erab->erabId, status};
        failList->erabs[failList->num++] = failedErab;

        return SUCCESS;
    }

    ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
    ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));

    succList->erabs[succList->num++] = *erab;
    return SUCCESS;
}

正例:

...
static Status verifyErabId( BYTE erabId
                          , const SuccessErabList* succList
                          , const FailedErabList* failList)
{
    if(!isErabIdValid(erabId)) return E_INVALID_ERAB_ID;

    if( containsInSuccList(erabId, succList) ||
        containsInFailList(erabId, failList)) return E_DUP_ERAB_ID;

    return SUCCESS;
}

static Status verify( const Erab* erab
                    , const SuccessErabList* succList
                    , const FailedErabList* failList)
{
    ASSERT_SUCC_CALL(verifyErabId(erab->erabId, succList, failList));
    ASSERT_SUCC_CALL(verifyQosPara(&erab->qosPara));

    return SUCCESS;
}

static Status addToSuccessErabList(const Erab* erab, SuccessErabList* succList)
{
    ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
    ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));

    succList->erabs[succList->num++] = *erab;

    return SUCCESS;
}

static Status addToFailedErabList(const Erab* erab, Status status, FailedErabList* failList)
{
    ASSERT_TRUE(failList->num < MAX_ERAB_NUM_PER_UE);

    ASSERT_TRUE(!containsInFailList(erab->erabId, failList));

    FailedErab failedErab = {erab->erabId, status};
    failList->erabs[failList->num++] = failedErab;

    return SUCCESS;
}

Status filterErabs(const Erab* erab, SuccessErabList* succList, FailedErabList* failList)
{
    Status status = verify(erab, succList, failList);

    if(status != SUCCESS)
    {
        return addToFailedErabList(&failedErab, status, failList);
    }

    return addToSuccessErabList(erab, succList);
}

3.1.3 儘量避免三個以上的函數參數

函數最好無參數,然後是一個參數,其次兩個,儘量避免超過三個[1]。太多參數往往預示着函數職責不單一,也很難進行自動化測試覆蓋。遇到參數過多函數,考慮拆分函數或將強相關參數封裝成參數對象來減少參數。

反例:

static Status addToErabList( const Erab* erab
                           , Status status
                           , SuccessErabList* succList
                           , FailedErabList* failList)
{
    if(status != SUCCESS)
    {
        ASSERT_TRUE(failList->num < MAX_ERAB_NUM_PER_UE);
        ASSERT_TRUE(!containsInFailList(erab->erabId, failList));

        FailedErab failedErab = {erab->erabId, status};
        failList->erabs[failList->num++] = failedErab;

        return SUCCESS;
    }

    ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
    ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));

    succList->erabs[succList->num++] = *erab;

    return SUCCESS;
}

正例:

static Status addToSuccessErabList(const Erab* erab, SuccessErabList* succList)
{
    ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
    ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));

    succList->erabs[succList->num++] = *erab;

    return SUCCESS;
}

static Status addToFailedErabList(const Erab* erab, Status status, FailedErabList* failList)
{
    ASSERT_TRUE(failList->num < MAX_ERAB_NUM_PER_UE);
    ASSERT_TRUE(!containsInFailList(erab->erabId, failList));

    FailedErab failedErab = {erab->erabId, status};
    failList->erabs[failList->num++] = failedErab;

    return SUCCESS;
}

3.1.4 區分查詢函數與指令函數

從數據的狀態是否被修改,可以將函數分爲兩大類:查詢函數和指令函數。查詢函數不會改變數據的狀態;指令函數會修改數據的狀態。區分二者,需要注意如下:

  1. 查詢函數使用is,should,need等詞增強其查詢語義
  2. 指令函數使用set,update,add等詞增強其指令語義
  3. 查詢函數往往無參數或僅有入參,考慮使用const關鍵詞明確查詢語義
  4. 忌在查詢函數體內修改數據,造成極大迷惑
  5. 指令函數忌用查詢語義詞彙

反例:

static Status processErabs( Erab* erab
                    , SuccessErabList* succList
                    , FailedErabList* failList)
{
    ASSERT_SUCC_CALL(verifyErabId(erab->erabId, succList, failList));
    ASSERT_SUCC_CALL(verifyQosPara(&erab->qosPara));

    ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
    ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));
    succList->erabs[succList->num++] = *erab;

    return SUCCESS;
}

正例:

static Status verify( const Erab* erab
                    , const SuccessErabList* succList
                    , const FailedErabList* failList)
{
    ASSERT_SUCC_CALL(verifyErabId(erab->erabId, succList, failList));
    ASSERT_SUCC_CALL(verifyQosPara(&erab->qosPara));

    return SUCCESS;
}

static Status addToSuccessErabList(const Erab* erab, SuccessErabList* succList)
{
    ASSERT_TRUE(succList->num < MAX_ERAB_NUM_PER_UE);
    ASSERT_TRUE(!containsInSuccList(erab->erabId, succList));

    succList->erabs[succList->num++] = *erab;

    return SUCCESS;
}

3.1.5 消除重複的函數

“函數的第一規則是短小,第二規則是更短小[1]”,但短小是結果,不是目的,也沒有必要刻意追求短小的函數。消除代碼中的重複,自然會得到長度可觀的函數。重複可謂一切軟件腐化的萬惡之源,能識別重複並消除重複是我們軟件設計的一項基本功。

3.2 類

本節不會涉及太多關於擴展性建議,主要關注類設計的整潔、可理解性。

遵循原則:

  • 別重複自己(DRY)
  • S.O.L.I.D原則[2]

注意事項:

  • 避免公開成員變量
  • 避免類過多的方法(上帝類)
  • 避免過深的繼承層次
  • 避免將父類強轉爲子類
  • 區分接口實現與泛化

3.2.1 設計職責單一的類

單一職責是類設計中最基本、最簡單的原則,也是最難正確使用的原則。職責單一的類必然是一些內聚的小類,內聚的小類進一步簡化了類與類之間的依賴關係,從而簡化了設計。軟件設計在一定程度上就是分離對象職責,管理對象間依賴關係。
那麼什麼是類的職責呢?Bob大叔把它定義爲“變化的原因”,職責單一的類即僅有一個引起它變化的原因的類。如何判斷一個類是否職責單一呢?給出一些建議:

  • 類中數據具有相同生命週期
  • 類中數據相互依賴、相互結合成一個整體概念
  • 類中方法總是在操作類中數據

反例:

enum orientation {N, E, S, W};
struct Position
{
    Position(int x, int y, int z, const Orientation& d);
    bool operator==(const Position& rhs) const;

    Position up() const;
    Position down() const;
    Position forward(const Orientation&) const;

    Position turnLeft() const;
    Position moveOn(int x, int y, int z) const;

private:
    int x;
    int y;
    int z;
    Orientation o;
};

正例:

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 Orientation
{
    Orientation turnLeft() const;
    Coordinate moveOn(int x, int y, int z) const;
    bool operator==(const Orientation&) const;

    static const Orientation north;
    static const Orientation east;
    static const Orientation south;
    static const Orientation west;

private:
        Orientation(int order, int xFactor, int yFactor);
private:
    int order;
    int xFactor;
    int yFactor;
};

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);
};

3.2.3 避免方法過多的接口

接口隔離原則(ISP)[2]就是避免接口中綁定一些用戶不需要的方法,避免用戶代碼與該接口之間產生不必要的耦合。接口中雖然沒有數據,可以根據用戶依賴或者接口職責對其拆分。清晰的接口定義不但可以減少不必要的編譯依賴,還可以改善程序的可理解性。

反例:

struct Modem
{
    virtual void dial(std::string& pno) = 0;
    virtual void hangup() = 0;
    virtual void send(char c) = 0;
    virtual void receive() = 0;
};

struct ModemImpl : Modem
{
    virtual void dial(std::string& pno);
    virtual void hangup();

    virtual void send(char c);
    virtual void receive();
};

正例:

struct DataChannel
{
    virtual void send(char c) = 0;
    virtual void receive() = 0;
};

struct Connection
{
    virtual void dial(std::string& pno) = 0;
    virtual void hangup() = 0;
};

struct ModemImpl : DataChannel, Connection
{
    virtual void dial(std::string& pno);
    virtual void hangup();

    virtual void send(char c);
    virtual void receive();
};

3.2.3 避免方法過多的類(上帝類)

方法過多的類,預示該類包含過多職責,行爲存在着大量的重複,不便於設計的組合,需要對該其進行抽象,拆分成更多職責單一,功能內聚的小類。

//java
public class SuperDashboard extends JFrame implements MetaDataUser
{
    public String getCustomizerLanguagePath();
    public void setSystemConfigPath(String systemConfigPath);
    public String getSystemConfigDocument();
    public void setSystemConfigDocument(String systemConfigDocument);
    public boolean getGuruState();
    public boolean getNoviceState();
    public boolean getOpenSourceState();
    public void showObject(MetaObject object);
    public void showProgress(String s);
    public boolean isMetadataDirty();
    public void setIsMetadataDirty(boolean isMetadataDirty);
    public Component getLastFocusedComponent();
    public void setLastFocused(Component lastFocused);
    public void setMouseSelectState(boolean isMouseSelected);
    public boolean isMouseSelected();
    public LanguageManager getLanguageManager();
    public Project getProject();
    public Project getFirstProject();
    public Project getLastProject();
    public String getNewProjectName();
    public void setComponentSizes(Dimension dim);
    public String getCurrentDir();
    public void setCurrentDir(String newDir);
    public void updateStatus(int dotPos, int markPos);
...
};

3.2.4 避免過深的繼承層次

繼承關係包括接口繼承(實現)、類繼承(泛化)兩種,接口繼承即依賴於抽象,方便程序的擴展;類繼承便於複用代碼,消除重複,但是設計中過多的繼承層次,往往導致設計邏輯的不清晰,建議繼承層次不要太深,另外,可以考慮使用組合方案替代繼承方案(比如使用策略模式替代模版方法[3])。

//Template Method
struct Removable
{
    virtual ~Removable() {}
    virtual Position move(Position)  = 0;

private:
    virtual Position doMove(Position p) const
    {
        return p;
    }
};

struct Up : Removable
{
private:
    virtual Position move(Position p) const
    {
        Position up = p.up();
        return doMove(up);
    }
};

struct UpLeft : Up
{
private:
    virtual Position doMove(Position p) const
    {
        return p.left();
    }
};
//Strategy
struct Removable
{
    virtual ~Removable() {}
    virtual Position move(Position)  = 0;
};

struct Up : Removable
{
private:
    virtual Position move(Position p) const
    {
        return p.up();
    }
};

struct Left : Removable
{
private:
    virtual Position move(Position p) const
    {
        return p.left();
    }
};

struct JoinMovable : Removable
{
    JoinMovable(const Removable&, const Removable&);
    virtual Position move(Position) const;

private:
    const Removable& left;
    const Removable& right;
};

#define UpLeft JoinMovable(Up, Left)

3.3 系統

本節主要是系統設計中CleanCode的應用,暫不用代碼呈現。
遵循原則:

  • 分而治之
  • 層次清晰

注意事項:

  • 避免認爲Demo就是真實的系統,二者差異很大
  • 避免盲目套用流行架構,根據需求選用合適架構
  • 考慮系統彈性,避免過度設計,用迭代完善架構
  • 設計時考慮系統性能

3.3.1 合理的對系統進行分層

一個設計良好的系統,必然是一個層次清晰的系統。分層方法可以參考業界常用方法,比如領域驅動設計[4](DDD)將其分爲:表示層、應用層、領域層、基礎設施層。


3.3.2 定義清晰的模塊邊界及職責

分層結構還依賴於層與層之間邊界、接口、職責清晰。

3.3.3 分離構造與使用

分離構造與使用,即將對象的創建與對象的使用分離,是降低軟件複雜度的常用方法,對應領域驅動設計(DDD)中使用工廠(Factory)創建對象,使用倉庫(Repository)存儲對象。

3.3.4 考慮系統性能

系統性能是不同與功能的另一個維度,在軟件設計過程中,把性能作爲一個重要指標考慮。編碼過程中不易過早的考慮性能優化,但也不要進行明顯的性能劣化。

Clean Code Style 基礎篇
Clean Code Style 進階篇

參考文獻:


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

  2. Robert C.Martin-敏捷軟件開發-原則、模式與實踐

  3. Erich Gamma...-設計模式

  4. Eric Evans-領域驅動設計

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