代码整洁之道-读书笔记

目录

1、整洁代码

2、有意义的命名

3、函数:

 4、注释

5、格式

纵向格式:

横向格式:

团队规则:

6、对象和数据结构

7、错误处理

8、边界

9、单元测试

10、类

11、系统

12、迭进

13、并发编程

14、逐步改进


1、整洁代码

1)要有代码:因为代码呈现需求的细节,而这些细节无法被忽略或者抽象,因此需要规范。

2)糟糕代码:糟糕的代码就像沼泽一样,曾经说过有朝一日再回头清理——稍后等于永不

3)混乱代价:随着时间发展,生产力会随着糟糕的代码趋向于零

4)新设计:当旧系统糟糕到开发团队造反,重新开发新系统替代时候,新系统不久也会走老系统的老路。

5)态度:程序员遵从 不了解混乱风险的经理的意愿是不专业的做法,就像医生做手术按照病人说的来办。

6)谜底:制造混乱无助于赶上期限,赶上期限的唯一方法是:始终尽可能保持代码整洁

7)整洁代码的艺术:像绘画,需要遵循大量小技巧,习得“整洁感”和“代码感”。

8)什么是整洁代码

Bjarne Stroustrup(C++语言发明者):我喜欢优雅高效的代码,代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省的引诱别人做没规矩的优化,搞出一堆混乱来。——整洁的代码只做好一件事

 Grady Booch(《面向对象分析与设计》作者):整洁的代码简单直接。整洁的代码如同优美的散文,整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直接了当的控制语句。

Michael Feathers(《修改代码的艺术》作者):整洁的代码总是看起来像是某位特别在意的人写的,几乎没有改进的余地,代码作者什么都想到了,如果你企图改进它,总会回到原点,赞叹某人留给你的代码——全心投入的某人留下的代码

Ron Jeffries(《极限编程实践》作者): <1>能通过所有测试;<2>没有重复代码;<3>体现系统中的全部设计理念;<4>包括尽量少的实体,比如类、方法、函数等。——有意义的命名;消除重复代码;只做一件事;表达力;小规模抽象

Ward Cunningham(Wiki发明者,极限编程创始人之一):如果每个例程都让你感到深合己意,那就是整洁代码。如果代码让编程语言看起来像是专门解决那个问题而存在,就可以成为漂亮代码。——让编程语言像是专为解决那个问题而存在

本书中设计原则引用:单一职责原则(SRP)开放闭合原则(OCP)依赖倒置原则(DIP)

2、有意义的命名

1)名副其实:变量、函数或类的名称应该回答为什么存在、做什么事情、怎么用等问题。

2)避免误导:应该避免使用与本意相悖的词。hp、aix、sco都不应该作为变量名,因为他们都是UNIX平台或类UNIX平台的专有名称。避免小写字母l和字母o作为变量名因为在某些字体下和1(一)以及0(零)太像。

3)做有意义的区分:以数字系列命名(a1、a2,...aN)没有提供正确信息,没有提供作者意图。废话也是无意义的表达,需要区分名称。

4)使用读的出来的名称:人类擅长记忆和使用单词,命名读起来顺口较好。

5)使用可以搜索的名称:单个字符a或数字常量7不太容易搜索,因此使用单词或MAX_CLASSES_PER_STUDENT更利于搜索。

6)避免使用编码:编码太多了,不应该增加解码负担。

7)避免思维映射:循环计数器常用i、j、k来计数,使用a,b、c不太好。——明确是王道

8)类名和方法名:类名和对象名应该是名词或名词短语;方法名应该为动词或动词短语。

9)别用双关语:代码作者应该写出易于理解的代码。

10)使用有意义的语境:添加前缀addFirstName、addLastName、addState等,不要添加没有用的语境,添加IDE不能自动添加的语境就是浪费生命。

3、函数:

错误函数写法:一个函数中包含多个不同层级的抽象,奇怪的字符串处理和函数调用,混用双重嵌套以及多个if语句等。

推荐函数写法:

1)短小20行封顶最佳,不应该超过100行(一屏幕显示长度)。——if语句、else语句、while语句等,代码块应该只有一行,该行为一个函数调用语句

2)只做一件事:做好这件事,只做一件事情;包括:同一抽象层上的步骤。

3)每个函数一个抽象层级:自顶向下读代码(向下规则),一个函数相当于文章中一段,每段描述当前抽象层级,并引出下一抽象层级。

4)短小的switch语句:若遇到switch根据类型,new新的对象时候,可以将switch埋到抽象工厂底下,利用接口多态的new新的对象。

5)使用描述性的名称:长而有描述性的名称比短而令人费解的名称好;选择描述性的名称能帮助理清模块的设计思路,并帮助改进思路。

6)函数参数:最好是参数为0,其次是一个,两个就可能造成顺序问题。三个及以上最好使用结构体或类缩短参数个数;不要在参数列表地方弄输出参数,很容易出错。

7)使用异常代替返回错误码:错误码会带来更深的嵌套结构,异常能简化代码结构。函数应该只做一件事情,而错误处理就是一件事,因此错误处理try语句不应在函数中出现。应该让函数抛出异常。

8)不要重复自己:重复代码应该抽离出来,不然修改时候需要修改多个地方。

9)如何写出这样函数:写代码应该像写文章或写论文一样,先想到什么就写什么,然后再打磨他。

 4、注释

1)能用代码表达意思的用代码表达:注释的问题在于:代码在不断更新,注释很少有人维护

2)好的注释:法律信息、对正则表达式的解释、警告信息、TODO工作列表、放大不合理地方的重要性。

3)坏的注释:不明所以、多余的注释、误导性注释、循规式注释(每个函数,每个参数都有注释是愚蠢可笑的)、日志式注释、废话注释、位置标记、括号后面的注释、归属或署名、注释掉的代码(应该删除,版本控制系统可以帮助找回历史代码)、HTML注释、非本地信息、信息过多、不明显的联系、函数头(利用好的函数名字解决)、过长的注释(读起来浪费时间)。

5、格式

格式的目的代码格式关乎沟通,沟通是开发者的头等大事。代码阅读一般都是从上往下,从左往右,代码行应展示一条完整的思路。

纵向格式

1)垂直方向上的分割:利用空行分割垂直方向上的代码,因为目光总会停留在空白行之后那一行。

2)垂直方向上的靠近:有密切关系的代码行应相互靠近,例如相关变量声明应放在一起。

3)变量声明:应尽可能靠近其使用位置,本地变量应放在函数的顶部。

4)实体变量:C++习惯放在底部,java习惯放在顶部。重点是放在谁都知道的地方声明,尊重习惯。

5)相关函数:应该按照调用的顺序,从上往下的自然顺序。

6)概念相关:概念相关的代码应该放在一起。相关性越强,彼此之间的距离就应该越短。

横向格式:

1)一行长度:循序无需拖动滑动条到右边的原则,一般在100字符以内。

2)水平间隔和靠近:使用空格字符将彼此紧密相关的事物连接在一起,也可以使用空格把相关性较弱的事物分隔开。人第一眼比较容易看到空格后一个字符。在赋值操作符周围加空格以此达到强调作用;不在函数名和左括号之间加空格,因为函数和参数密切相关;参数一 一隔开,使用逗号强调参数是分离的。

3)水平对齐:变量水平对齐没有啥用,有自动格式化工具使用自动格式化工具对齐即可。

4)缩进:花括号缩进有利于查看代码工作范围。没有缩进的代码难以阅读。

5)空范围:while或for语句的语句体为空时候,应该确保分号在下一行缩进位置。或者使用花括号。

团队规则:

1)每个人都有自己喜欢的格式规则,但若在一个团队中工作,应该统一使用一种格式风格

2)好的软件系统应该读起来是一致和顺畅的风格。

3)该书作者风格:见CodeAnalyzer.java

6、对象和数据结构

1)对象:把数据藏于抽象之后,暴露操作数据的函数;数据结构:直接暴露数据,没有提供有意义的函数。

2)过程式代码(使用数据结构):便于在不改动数据结构的前提下添加新函数;面向对象代码:便于在不改动既有函数的前提下添加新类。

3)迪米特法则:只和朋友谈话,不与陌生人谈话。类C的方法f只应该调用以下对象:C由f创建的对象作为参数传递给f的对象由C的实体变量持有的对象。不应该调用任何函数返回的对象的方法。

4)数据传送对象(DTO):类似于java bean结构,对bean结构进行半封装,只暴露get接口。

7、错误处理

1)使用异常而非返回值:每调用一个函数之后,就得判断该函数返回值是否有问题的方式,搞乱了代码逻辑,而且这个判断很容易忘记没写。遇到错误时,最好跑出一个异常,这样调用代码就很整洁,逻辑不会被错误处理搞乱。见下图:DeviceController.java

//使用返回值
public class DeviceController {
...
public void sendShutDown() {
    DeviceHandle handle = getHandle(DEV1);
    if(handle != DeviceHandle.INVALID) {
        DeviceRecord record = retrieveDeviceRecord(handle);
        if(record.getStatus() != DEVICE_SUSPENDED) {
            pauseDevice(handle);
            clearDeviceWorkQueue(handle);
            closeDevice(handle);
        } else {
            logger.log("Device suspended. Unable to shut down");
        }
    } else {
        logger.log("Invalid handle for : " + id.toString());
    }
  }
}

//使用异常代替返回值
public class DeviceController {
...
public void sendShutDown() {
    try {
        tryToShutDown();
    } catch(DeviceShutDownError e) {
        logger.log(e);
    }
  }

private void tryToShutDown() throws DeviceShutDownError {
    DeviceHandle handle = getHandle(DEV1);
    DeviceRecord record = retrieveDeviceRecord(handle);

    pauseDevice(handle);
    clearDeviceWorkQueue(handle);
    closeDevice(handle);
  }

private DeviceHandle getHandle(DeviceID id) {
...
    throw new DeviceShutDownError("Invalid handle for : " + id.toString());
...
  }
}

2)使用不可控异常:使用可控异常必须在catch语句和抛出异常处之间的每个方法签名中声明该异常。这意味着较低层级的修改会波及到较高层次的签名,破坏了封装。

3)给出异常发生的环境说明:可以从异常那里得到堆栈踪迹stack trace,从而了解异常的原因。

4)依调用者需要定义异常类:我们应用程序定义异常类时候,最重要考虑是:他们如何被捕获

5)特例模式:创建一个类或配置对象,用来处理特例。你来处理特例,客户代码就不用应付异常行为了,异常行为被封装到特例对象中。

//原代码
try {
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
    m_total += getMealPerDiem();
}

//使用特例模式
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();

public class PerDiemMealExpenses impleMents MealExpenses {
    public int getTotal() {
        //return the per diem default
    }
}

6)别返回null值:每行代码都在检查null值的应用程序糟糕透了,只要有一处没有检查null值,程序就会失控。

//原代码
List employees = getEmployees();
if(employees != null) {
    for(Employee e : employees) {
        totalPay += e.getPay();
    }
}

//修改后
List employees = getEmployees();
for(Employee e : employees) {
    totalPay += e.getPay();
}
//修改getEmployees方法,特殊情况返回空列表
public List getEmployees() {
    if( ... there are no employees ...)
        return Collections.emptyList();
}

7)别传递null值:当有人传递null值参数给一个函数,改如何处理,可以创建一个新异常类型并抛出;使用assert断言;但这两种方法均未解决问题,运行时候仍会错误。恰当的做法是禁止传入null值

8、边界

1)使用第三方代码:第三方库追求普适性,而使用者想集中满足特定需求的接口,这种差异可能导致系统边界上出现问题。例如java.util.Map中提供丰富接口,而我们希望使用者不要删除Map中的任何东西,但是java.util.Map中提供了clear接口。较好的方法是:使用Sensors类封装Map,隐藏细节,不暴露clear接口。——不建议总是用这种方式封装Map使用,而是建议不要将Map在系统中传递,当Map在这种边界时候,使用封装的方式更好

2)整洁的边界:边界上的代码需要清晰的分割和定义了期望的测试。应该避免我们的代码过多的了解第三方代码中的特定信息。依靠你能控制的东西,好过依靠你控制不了的东西,免得日后受它控制。

9、单元测试

1)TDD(测试驱动开发)三定律

        定律一:测试先行(在写功能代码之前,先写测试用例代码);

        定律二:只写刚好无法通过的单元测试,不能编译也算不通过。

        定律三:只写刚好通过当前失败测试用例的生产代码。

这三条定律将限制你在大概30秒的一个循环中。测试与生产代码一起编写,测试只比生产代码早些几秒钟

2)保持测试整洁

        测试代码应该与生产代码一样重要,需执行同样的质量标准,脏测试等同于没有测试。测试代码必须随着生产代码的演进而修改。测试代码需要被思考、被设计、被照料,应该像生产代码一样保持整洁。

3)整洁的测试三要素可读性、可读性、可读性。单元测试用例中需要呈现:构造——操作——检验(Build——Operate——Check)模式。避免用细节误导或吓到使用者。

4)双重标准:测试代码在不考虑性能和内存的情况下,可以和生成代码不同,但是测试代码应当简单、精悍、足具表达力。

5)每个测试一个概念:每个测试一个断言是个好准则,但是每个测试只测试一个概念更好。

6)FIRST:整洁测试5条规则:快速(Fast)、独立(Independent)、可重复(Repeatable)、自足验证(Self-Validating)、及时(Timely)。

10、类

1)类应该短小类的名称应该描述其职责,如果无法为某个类命以精确的名称,则这个类大概率太长了。类名中包括模糊的词:如Processor或Manager或Super等往往说明职责不清。

2)单一职责原则(SRP):类或模块应有且只有一条需要修改的理由。这句话说明了类的长度限制。

3)内聚:类应该只有少量实体变量,类中的每个方法都应该操作一个或多个这种变量。方法操作的变量越多,就越黏聚到类上。因此尽量保持方法和实体变量的粘性,若无法保证,则应该将这些变量和方法拆分到两个或多个类中,让新的类更为内聚。

4)为了修改而组织:对于多数系统,修改将一直持续。每一处的修改都冒着其他部分不能如期工作的风险。整洁系统中应该避免"打开"类进行修改,应该拓展系统而不是修改原有代码

5)隔离修改:需求会改变,因此代码也会改变。具体类包含实现细节,而抽象类只呈现概念。需求改变往往会带来细节的改变,因此我们应该借助接口和抽象类隔离这些细节

6)依赖倒置原则(DIP)应该依赖于抽象而不是依赖于具体的细节。

11、系统

1)将系统的构造与使用分开构造使用是非常不一样的过程。例如如下典型场景:就是所谓的“延迟初始化”,有一点好处是启动时间会更短,还能保证永远不会返回null值。

public Service getService() {
    if(service == null)
        service = new MyServiceImpl(...);
    return service;
}

然而,我们也得到了 MyserviceImpl 及其构造器所需的一切的硬编码依赖。不分解这些依赖关系就无法编译。如果 MyserviceImpl 是个重型对象,则测试也会是个问题

当然,仅出现一次的延迟初始化不算什么严重问题。但应用程序中往往许多类似的情况出现,我们不应该让这种小技巧影响整个系统设计。

2)分解main:将构造和使用分开的方法之一就是:将全部构造过程迁移到main或称为main的模块中。设计系统其余部分时候,假设所有对象都已经正确构造和设置。

3)依赖注入:对象不应负责实体化对自身的依赖,应将这个职责交给其他有权利的机制,从而实现控制的反转。

4)扩容:我们应该只去实现今天的用户故事,然后重构,明天在扩展系统实现新的用户故事。这是迭代增量敏捷的精髓所在。

5)测试驱动系统架构:软件设计师,不需要像建筑设计师一样 "先做大设计Big Design Up Front--BDUF)"。实际上对于软件设计师,BFUF是有害的,它会阻碍改进,因为架构上的方案会影响后续的设计思路。

6)优化决策:在巨大的系统中,不管是一座城市或一个软件项目,无人能做所有决策。最好是授权给最优资格的人决策,但是延迟决策至最后一刻也是好手段。如果决策太早,会缺少客户反馈、关于项目的思考和实施经验。

7)系统需要领域特定语言:在软件领域,领域特定语言(DSL)是一种段杜的小型脚本语言或以标准语言写的API,领域专家可以用它编写读起来像组织严谨的散文一般的代码。

12、迭进

1)“简单设计”的四条规则,一下规则按重要程度排列:

        运行所有测试:只要系统可测试,就会导向保持类短小且目的单一的设计方案。测试消除了对清理代码就会破坏代码的恐惧。

        不可重复:重复是拥有良好设计系统的大敌,“小规模复用”可大量降低系统复杂性,想要实现大规模复用,必须理解如何实现小规模复用。

        表达了程序员的意图(表达力);软件项目的主要成本在于长期维护。代码应该清晰地表达作者的意图,作者把代码写的越清晰,其他人花在理解代码上的时间就越少,从而减少缺陷,缩减维护成本。

        尽可能减少类和方法的数量;类和方法数量太多,有时是由毫无意义的教条主义导致。我们的目标是在保持函数和类短小的同时,保持整个系统的短小精悍。

13、并发编程

1)为什么要并发:并发是一种解耦策略,它帮助我们把做什么(目的)何时做(时机)分解开。解耦目的和时机明显的改进应用程序的吞吐量结构

2)迷思与误解

对并发的误解:

        <1>并发总能改进性能

        <2>编写并发程序无需修改设计

         <3>在采用Web或EJB容器时候,理解并发问题并不重要。

对并发中肯的说法

        <1>并发会在性能和编写额外代码上增加一些开销

        <2>正确的并发是复杂的,即便对于简单问题也是如此

        <3>并发缺陷并非总能复现,所以常被看做偶发事件而忽略,未被当作真的缺陷看待

        <4>并发常常需要对设计策略的根本性修改

3)并发防御原则

        单一职责原则(SRP):方法、类、组件应当只有一个修改的理由。并发设计自身足够复杂到成为修改的理由。

        限制数据作用域:应该谨记数据封装,严格限制对可能被共享的数据的访问。

        线程应尽可能地独立:让每个线程在自己的世界中存在,不与其他线程共享数据。

4)了解执行模型

        生产者—消费者模型:一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源

        读者——作者模型:当存在一个主要为读者线程提供信息源,但只偶尔被作者线程更新的共享资源,吞吐量就会是个问题。挑战之处在于平衡读者线程和作者线程的需求,实现正确操作,提供合理的吞吐量,避免线程饥饿。

        宴席哲学家:一群哲学家坐在圆桌旁,每个哲学家的左手边放了一把叉子。桌面中央摆着一大碗面。哲学家们思索良久,直至肚子饿了。每个人都要拿起叉子吃饭。但除非手上有两把叉子,否则就无法进食。

5)警惕同步方法之间的依赖:建议使用一个共享对象的多个方法。

6)保持同步区域微小:锁是昂贵的,应该尽可能少涉及临界区,尽可能减少同步区域。

7)很难编写正确的关闭代码:尽早考虑关闭问题,尽早令其工作正常,平静关闭很难做到。

8)测试线程代码:将伪失败(偶发事件)看做可能的线程问题;先使非线程代码可工作;编写可插拔的线程代码;编写可调整的线程代码;运行多于处理器数量的线程;在不同平台上运行;调整代码并强迫错误发生。

14、逐步改进

1)草稿:要编写整洁代码,必须先写肮脏代码,然后再清理它。

2)详细例子见原书籍《代码整洁之道》。

你可能感兴趣的:(计算机读书笔记,开发语言,单一职责原则,设计模式)