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-代码整洁之道

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

推荐阅读更多精彩内容

  • 目录 前言 “Clean Code That Works”,来自于Ron Jeffries这句箴言指导我们写的代码...
    李永顺阅读 2,273评论 1 7
  • 如何实施重构 稍微复杂的重构过程,都是由一系列的基本重构手法组成. 《重构》一书中针对各种重构场景,给出了大量的重...
    MagicBowen阅读 3,931评论 0 3
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,107评论 29 470
  • 注:这是第三遍读《C语言深度解剖》,想想好像自从大学开始就没读完过几本书,其中谭浩强的那本《C语言程序设计(第四版...
    HavenXie阅读 1,704评论 1 6
  • 春节假期啃了大前研一的《思考的技术》,书中作者通过自身经历和实例提供了从理论到实践的一整套思维能力体系知识,着实对...
    小晖2017阅读 538评论 0 2