首页投稿(暂停使用,暂停投稿)程序员TDD(测试驱动开发)

Clean Code Style - 进阶篇

2016-07-25  本文已影响311人  李永顺

目录

前言

“Clean Code That Works”,来自于Ron Jeffries这句箴言指导我们写的代码要整洁有效,Kent Beck把它作为TDD(Test Driven Development)追求的目标,BoB大叔(Robert C. Martin)甚至写了一本书来阐述他的理解。
整洁的代码不一定能带来更好的性能,更优的架构,但它却更容易找到性能瓶颈,更容易理解业务需求,驱动出更好的架构。整洁的代码是写代码者对自己技艺的在意,是对读代码者的尊重。
本文是对BOB大叔《Clen Code》[1] 一书的一个简单抽取、分层,目的是整洁代码可以在团队中更容易推行,本文不会重复书中内容,仅提供对模型的一个简单解释,如果对于模型中的细节有疑问,请参考《代码整洁之道》[1]


II 进阶级

进阶级主要包括命名、测试设计、数据结构及对象设计,该部分要求编码时关注到更多细节,从语义层次提升代码的可理解性。

2.1 命名

命名是提高代码表达力最有效的方式之一。我们都应该抱着谨慎的态度,像给自己孩子取名字一样,为其命名。好的名字,总能令人眼前一亮,令阅读者拍案叫绝,但好的名字往往意味着更多的思考,更多次尝试,体现着我们对代码的一种态度。随着我们对业务的进一步了解,发现名字不合适时,要大胆的重构他。

遵循原则:

注意事项:

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 避免在命名中使用编码

在程序设计的历史中,在命名中使用编码曾风靡一时,最为出名的为匈牙利命名法,把类型编码到名字中,使用变量时默认携带了它的类型,使程序员对变量的类型和属性有更直观的了解。

基于如下原因,现代编码习惯,不建议命名中使用编码:

由于历史原因,很多遗留代码仍然使用匈牙利命名法,修改代码建议风格一致,新增代码建议摒弃

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 测试

整洁的测试是开发过程中比较难做到的,很多团队把测试代码视为二等公民,对待测试代码不想工程代码那样严格要求,于是出现大量重复代码、名称名不副实、测试函数冗长繁杂、测试用例执行效率低下,某一天发现需要花费大量精力维护测试代码,开始抱怨测试代码。

遵循原则:

注意事项:

2.2.1 风格统一的测试场景描述

  1. Given-When-Then风格
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 对象和数据结构

此处不讨论面向对象与面向过程设计范式的优劣,仅区分对象与数据结构使用场景与注意事项。
遵循原则:

注意事项:

2.3.1 区分数据结构与对象的使用场景

对象主要关注“做什么”,关心如何对数据进行抽象;数据结构主要表示数据“是什么”,过程式主要关注“怎么做”,关心如何对数据进行操作。二者都可以很好的解决问题,相互之间并不冲突。
在使用场景上:

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

上一篇下一篇

猜你喜欢

热点阅读