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-领域驱动设计

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容