代码整洁之道-读书笔记1

第一章 整洁代码

1.2糟糕的代码

糟糕的代码会毁掉一个公司,但是为什么会出现糟糕的代码?
可能是因为赶时间,如果花时间重构或者清理以前的代码,老板就会大发雷霆。
勒布朗法则:稍后等于用不。

1.3混乱的代价

有些团队在项目初期进展迅速,但是有一两年时间却慢如蜗牛。对代码的每次修改都影响都其他部分的修改,随着混乱的增加,团队生产力持续下降,管理层想提升生产力,因此增加人力,但是新人并不熟悉系统的设计,搞不懂什么样的修改符合设计意图,什么样的修改违背设计意图,因此他们可能会制造更多混乱,驱动生产力向零那端不断下降。
在这里插入图片描述

什么是整洁的代码

只做一件事,表达力强,小规模抽象。

第二章 有意义的命名

名副其实

如果一个函数名或者变量名需要注释来补充说明,那就不算是名副其实。
你应该告诉他做什么事,该怎么用。
之后作者开始用举例的方式介绍糟糕的命名和好的命名的对比。问题不在于代码的简洁度,而是在于代码的模糊度,也即是上下文在代码中未被明确体现的程度。

避免误导

避免留下隐藏代码本意的错误线索,应当避免使用与本意相违背的词。例如用accounts表示用户组比用accountList表示这个数组强,因为如果不是List类型就会引起错误的判断。

做有意义的区分

如果无意义的命名只是为了通过编译,就会制造麻烦。因为如果只是添加数字或者是废话就体现不出正确的信息,例如ProductInfo和Product,他们虽然名称不同,意思确实没有区别。要区分名称就要以读者鉴别不同之处的方式来区分。

使用读得出来的名称

如果读不出来,同事之间交流的时候就会很搞笑。挨个字母念也不懂什么意思。

使用可搜索的名称

长名称胜于短名称,搜得到的名称胜于用自造编码代写的名称。
名称的长短应该与作用域大小相对应。
不必用前缀来标明成员变量,应当把类和函数做的足够小,消除对成员前缀的需要。

接口的实现命名

这里作者宁可选择实现也不选择对接口名称的编码,例如有个AbstractFactory抽象工厂接口,需要用具体类来实现,选择ShapeFactoryImp比ISapeFactory好。

避免思维映射

聪明程序员和专业程序员的区别是:专业程序员了解,明确才是王道,编写容易让人理解的代码。例如对单个循环变量计数时,通常使用i,j,k,不使用l因为L和数字1很像,会有可能出现理解错误。

类名

类名和对象名应当是名词,不应该是动词。

方法名

方法名应当是动词或者是动词短语。属性访问,修改,断言应该加上get,set,is前缀。

每个概念对应一个词

也即是沿袭传统命名,保持一致,就可以不借助多余的浏览找到正确的方法。

别用双关语

避免将同一个单词用于不同的目的,同一术语用于不同概念,基本上就是双关语,例如好多类中都有add方法,只要这些add方法的参数列表和返回值在语义上是等价的,就一切顺利。比如在多个类中都有add方法,该方法通过增加现存值来获得新值。但是这时候有个类中的add方法是增加一个参数(与之前的不一致),这时不应该命名为add方法,应该是append或者insert。

第三章 函数

短小

if,else,while语句等,其中的代码块应该只有一行,该行应该是一个函数调用语句,这样不但能保持函数短小,而且因为块内调用的函数拥有较具说明性的名称从而增加了文档的价值,这样的函数才易于阅读和理解。

只做一件事

函数应该只做一件事并且做好这件事。要判断函数是否只做了一件事,还有一个办法就是看能否再拆出一个函数,该函数不仅只是单纯的重新诠释其实现。

每个函数一个抽象层级

要确保函数中的语句都在同一抽象层级上。
自顶向下读代码:向下规则
想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来在查看函数列表时,就能根据抽象层及向下阅读。称之为向下规则。

Switch语句

即便只有两种条件的switch语句也比函数大得多,switch天生要做N件事,但是我们有时有不可避免的要使用它。
对于普通的switch违反了开闭原则,单一职责原则,每次增加条件都要修改switch中的case,解决方案,可以将switch语句埋到抽象工厂底下,不让任何人看到,该工厂为抽象类创建合适的实体。

public abstract class Employee{
    
	public abstract boolean isPayday(); 
    public abstract Money calculatePay(); 
    public abstract void deliverPay(Money pay);
}
public interface EmployeeFactory {
		public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; 
}
public class EmployeeFactoryImpl implements EmployeeFactory {
		public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType{
			switch (r. type){
				case COMMISSTONED: return new CommissionedEmployee(r); 
                case HOURLY: return new HourlyEmployee(r); 
                case SALARIED: return new SalariedEmploye(r); 
                default: throw new InvalidEmployeerype(r. type);
            }
      }
}

函数参数

最理想的参数个数是0,其次是1,再次是2,尽量避免用三个或者三个以上的参数。
测试角度上看,肯定是参数越好越容易处理,输出参数比输入参数还要难理解。

单参数函数

有一种不那么普遍但有用的单参数函数形式,就是事件,在这种形式中,有输入参数而无输出参数。程序将函数看做是一个事件,例如void passwordAttemptFailedNtimes(int attempts);
例如一个函数的功能是转换一个值,如果函数要对输入的参数进行转换,转换结果就应该体现在返回值上。

标识参数

标识参数向函数传入boolean值简直是sb,如果为true做这一件事,如果是false做另一件事,这与函数的定义只做一件事相违背。举例:render(Boolean isSuite)改为reanderForSuite()和renderForSingleTest()。

参数对象

如果函数看来需要两个,三个或三个以上的参数,就说明应该将需要的参数封装为类了。

参数列表

有时候不确定使用多少参数,可以定义为可变参数,其实可变参数就和类型为List的单个参数没什么两样。

动词关键字

对于一元函数,函数和参数应当形成一种非常良好的动词/名词的形式,writeField(name),比write(name)好。

输出参数

普遍而言应该避免使用输出参数,如果函数必须要修改某种状态,就修改所属对象的状态吧。

抽离try/catch代码块

try/catch会混乱代码结构,使错误处理和正常流程混为一谈,应该把try/catch代码块抽离出来形成函数。

public void delete(Page page){
    try{
        deletePageAndA11References(page); catch (Exception e){
        logError(e);
        }
    }
}
private void deletePageAndA1lReferences(Page page) throws Exception{
		deletePage(page); 
        registry. deleteReference(page, name); 
        configKeys. deleteKey(page. name. makeKey()); 
 }
private void logError(Exception e){
		logger.log(e. getMessage());
 }

错误处理应该就是只做一件事,catch/finally代码块后面也不应该有其他内容。

依赖磁铁Error

返回错误码通常按时某处有一个类或者是枚举,定义了所有错误码。
这样的类就是一块依赖磁铁,许多类都要导入和使用它,当Error枚举修改的时候,所有这些其他的类都需要重新编译和部署,这对Error类造成了负面压力。程序员不愿意增加新的错误代码,因为这样他们就得重新构建和部署所有东西,于是他们就得复用旧的代码,而不添加新的。使用异常代替错误码,新异常类可以从异常父类里面派生出来,无需重新编译或者重新部署。

不要重复

重复是软件中一切邪恶的根源,许多原则与时间规则都是为了控制重复或是消除重复而创建的。数据库范式是为了消灭数据重复而服务,面向对象编程是将代码集中到基类,从而避免冗余。面向切面编程,面向组件编程多少也是消除重复的一种策略。自子程序发明以来,软件开发领域的所有创新都是不断尝试从源代码中消灭重复。

第四章 注释

若编程语言有足够的表达力,或者说我们经常用代码来表达意图就不需要注释。注释的用法是弥补我们在用代码表达意图时遭遇的失败。注释存在的时间越长,就会离当初所描述的代码越远,最后会变得全然错误,因为程序员不能坚持维护注释。
代码在变动,在演化。但是注释并不是一直跟着变动,因此会越来越不准确。不准确的注释要比没注释坏的多,他们满口胡言。只有代码能够忠实的告诉你他做的事。

注释不能美化糟糕的代码

少量注释+有表达力的代码>大量注释+糟糕的代码,作者说预期花时间添加注释,不如清理那堆糟糕的代码。

好注释

  • 法律信息(半圈以及著作权声明)
  • 提供信息的注释
  • 对意图的解释
  • 阐释(将某些晦涩难懂的参数或返回值翻译为某种可读心事也是有用的)

在这里插入图片描述

  • 警示
  • TODO注释(因为某些原因目前还没做的工作,需要定期查看,删除不需要的TODO)
  • Javadoc

坏注释

多余的注释,误导性注释,循规式注释(如:每个变量和函数都要有Javadoc注释),日志式注释,废话注释,HTML注释,能用函数或者变量表达时就不要用注释
在这里插入图片描述

位置标记和括号后面的注释对染在长函数中可能有意义但是我们更愿意编写短小,封装的函数,因此也不推荐。
归属和署名虽然当时可以知道是谁在什么时间写的代码,但是会随着时间推移,越来越和原作者没关系。可以用源代码控制系统将这些信息放在那。
注释掉的代码删掉即可。

第五章 格式

代码格式很重要,今天写的功能可能在下个版本中被修改,但是代码的可读性却会对以后可能发生的修改行为产生深远的影响,原始代码修改之后,其代码风格和可读性仍然会影响到可维护性和扩展性。

垂直格式

概念间垂直方向上的区隔

因为大多数代码是从上往下读,从左往右读。每行展现一个表达式或者一个句子,每组代码展示一条完整的思路,这些思路用空行隔开。

垂直方向上的靠近

如果说空白行隔开了概念,靠近的代码行则按时他们之间的紧密关系。例如变量放一块,再隔一行写方法。

垂直距离

变量声明应尽可能靠近使用位置,因为函数很短,本地变量应该在函数的顶部出现。
实体变量应该放在类的顶部,这不会增加变量的垂直距离。
相关函数,若某个函数调用了另外一个,就应该将他们放在一起,而且调用者应该尽可能放在被调用者上面。这样读者可以确信函数声明总会在调用后很快出现,增强了整个模块的可读性。
相关性越强的代码,彼此之间的距离就该越短。

横向格式

作者遵循无需拖动IDE滚动条到右边的原则,但是随着显示器越来越宽,年轻程序员又能将显示字符缩小到如此程度,一个屏幕上甚至能容纳200个字符。

水平方向上的间隔和靠近

我们通常在复制操作符周围加上空格字符,以此达到强调目的,赋值语句有两个要素:左边和右边,空格字符加强了分隔效果。
另一方面,我不在函数名和左括号之间加上空格,这是因为函数与其参数密切相关,如果隔开,就会显得毫无关系。
乘法因子之间不加空格,因为乘除的优先级比加减要高。

缩进

源文件是一种继承结构而不是大纲结构。继承结构中的每一层及都圈出一个范围,名称可以在其中声明,而声明和执行语句也可以在其中解释。
缩进可以让代码结构更加鲜明。

空范围

有时候if,while中的花括号内容为空,尽量不使用这种结构,如果无法避免就确保空范围的缩进,用括号包围起来。将分号另起一行再加上缩进,否则很难看到空范围。

第六章 对象和数据结构

数据抽象

将变量声明为private私有,隐藏实现并非只是在变量之间放上一个函数层那么简单。隐藏实现关乎抽象,使用户无需了解数据的实现就能够操作数据本体。

数据,对象的反对称性

过程式代码在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不改动既有函数的前提下添加新类。
所以对于面向对象较难的事,对于面向过程的代码却很容易,反之亦然。

得墨忒耳律

模块不应该了解他所操作对象的内部情况。
方法不应该调用由任何函数返回的对象的方法。

数据传输对象

DTO是一个只有公有变量,没有函数的类,这样的数据结构被称为数据传输对象——DTO。DTO在数据库通信,解析套接字传递消息之类的场景中很常用。
Active Record是一种特殊的DTO形式,他们是拥有公共变量的数据结构,但通常也会拥有类似save和find这样的可浏览方法。active record一般是对数据库表或其他数据源的直接翻译。但是也有开发者往这类数据结构中添加业务规则方法,把这类数据结构当成对象来用,这是不理智的行为,它导致对象更加复杂混乱。

第七章 错误处理

使用异常而不是返回码

如果使用返回码进行判断执行结果是否正确,就需要调用者必须在调用之后立即检查错误,但是这个步骤容易遗忘,因此遇到错误直接抛异常,逻辑不会被错误结果搞乱。

使用try-catch-finaly

使用不可控异常

可控异常(checked exception)违背了开放封闭原则,如果你在方法中抛出可控异常,而catch语句在三个层级上,你就得在catch语句和抛出异常处之间的每个方法签名中声明该异常。假设最底层函数throw一个异常,这意味着每个调用他的函数都要捕获或者继续向上抛出,最终得到的就是一个从最低端贯穿到最高端的修改链,封装被打破了,因为在抛出路径中的每个函数都要去了解下一层级的异常细节。

给出异常发生的环境说明

你抛出的每个异常,都应当提供环境说明,用来判断错误的来源和住处,在Java中,你可以从异常里得到堆栈踪迹,但是无法告诉失败操作的最终原因。如果有日志系统,应当抛出详细的错误信息,包括失败操作和失败类型。

调用者需要自定义异常类

在定义异常类的时候最重要的考虑应该是他们如何被捕获。
如果有多个异常可能会捕获,我可以抽象出一个基类,然后通过打包调用API,确保他返回通用异常类型,从而简化代码。

定义常规操作

尽量不要在catch代码块中写业务逻辑,创建一个类或者是配置一个对象,用来处理特例,然后就不用处理异常行为了,因为异常行为被封装到特例对象中。
在这里插入图片描述

不要返回NULL

方法返回null就是自己给自己和调用者添加麻烦,如果调用者忘记校验null值就会在运行的时候出现NullPointException,如果你打算在方法中返回null,不如直接抛出异常,或是返回一个无属性的空对象。如果返回类型是集合,就返回一个空集合而不是null。这样就能减少校验也能尽量避免空指针操作了。

不要将NULL作为参数传递

虽然仍可以通过if判断加抛异常的方式或异常定义处理器处理,但是还是未能解决问题,还会得到运行时错误,因此恰当的做法就是禁止传入null值。

第八章 边界

在购买程序包或使用开放源代码时,我们依靠公司中其他团队打造组件或者子系统,不管什么情况,都需要将外来代码整合到自己的代码中。因此需要一些保持软件边界整洁的手段和技巧。

学习性测试的好处

毫无成本。编写测试是学习API的途径。能够帮助我们增加对AP的理解。
不过是免费,当第三方包发布了新版本,我们可以运行学习性测试,看看程序包的行为有没有改变。
学习性测试确保第三方程序包按照我们想要的方式工作。一旦整合进来,就不能保证总是兼容。

使用尚不存在的代码

另一种边界是将已知和未知分隔开的边界。作者曾经开发的子系统还没有定义,但是又并不想受阻碍,因此就从未知代码很远处开始工作。

整洁的边界

边界上的代码需要清晰的分割和定义了期望的测试,应该避免我们的代码过多的了解第三方代码中的特定信息,依靠你能控制的东西,好过依靠你控制不了的东西。我们通过第三方边界接口的位置来管理第三方边界。可以使用是北汽模式将接口转化为第三方提供的接口。

第九章 单元测试

TDD三定律

  1. 定律一:再编写不能通过的单元测试之前,不可编写生产代码
  2. 定律二:只可编写刚好无法 通过的单元测试,不能编译也算不通过。
  3. 只可编写刚好足以通过当前失败测试的生产代码。

保持测试的整洁

没有了测试就会失去保证生产代码可扩展的一切要素。正式单元测试让你的代码可扩展,可维护,可复用。因为没有测试就会担心改动会不会引入不可预知的缺陷。
测试覆盖率越高,就不用顾忌这些,如果测试不干净,你就会有锁牵制,开始失去改进代码结构的能力,测试越脏,代码就会越脏。最终会失去测试,代码开始腐烂。

整洁的测试

每个测试都清晰的分为三个环节,第一个是构造测试数据,第二个是操作测试数据,第三个是检验操作是否得到期望的结果。BUILD-OPERATE-CHECK。

面向特定领域的测试

测试过程中并没有直接使用程序员用来对系统进行操作的API,而是打造了一套包装这些API的函数和工具代码,这样就能更方便的编写测试,写出来的测试也更便于阅读。
这些API并非是一开始就设计出来的,而是后续重构时进行演进的。

双重标准

也即是生产环境和测试环境两种不同的标准,有些是是禁止在生产环境中做,但是它可以在测试环境中做(例如内存和CPU的效率问题)。测试代码最重要的还是可读性。

每个测试一个断言

作者认为单个测试中只有一个断言是好准则,不过作者也不否认会出现一个以上的断言,最好的说法是单个测试中的断言数量应该最小化。
更好的测试规则是每个测试函数中只测试一个概念,我们不想要超长的测试函数。因为太长的测试函数,我们不确定那段代码到底要测什么。

FIRST

  • 快速:测试应该够快。测试运行缓慢就不会频繁使用它,如果不多测试,就不能尽早发现问题,也无法轻易修正,从而不能轻而易举的清理代码。
  • 独立:测试应该相互独立。某个测试不应该为下一个测试设定条件。你可以单独运行每个测试,也可以以任意顺序运行,当测试互相依赖的时候,头一个没通过的就会导致一连串的测试失败。
  • 可重复:测试应该可以在任一条件下重复通过。
  • 自足验证:测试应该有布尔值输出,无论是通过或失败,你不应该查看日志文件来确认测试是否通过,你不应该手工对比两个不同文本文件来确认测试是否通过。
  • 及时:测试应该及时编写。单元测试应该恰好使通过的生产代码之前编写。如果在编写生产代码之后编写测试,你就会发现生产代码难以测试,你可能会认为某些生产代码本身难以测试,你可能不会去设计可测试的代码。

第10章 类

类的组织

如果有公共静态变量应该先出现,然后是私有静态变量,以及私有实体变量。很少会有公共变量。公共函数应该在变量列表之后,

类应该短小

对于函数是计算代码行数,对于类是计算权责。公有方法的数量。
类的名称应该明确他的职责。

单一职责原则

单一职责原则认为,类和模块应该有且只有一条加以修改的理由。该原则给出了权责的定义,又是关于类的长度的指导方针。
鉴别修改类的理由可以帮助我们更好的创建抽象。
系统应该由许多短小的类组成而不是少量巨大的类。每个类中封装一个权责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。

内聚

类应该只有少量实体变量,类中的每个方法都应该操作一个或多个这种变量,通常而言,方法操作的变量越多,就越黏聚到类上。如果一个类中的每个变量都被每个方法所使用,则该类具有最大内聚性。
内聚性越高,意味着类中的方法和变量互相依赖,互相结合成一个逻辑整体。

将一个大的函数切割成小函数,就会导致更多的类出现。当类失去了内聚性,就拆分他。所以将大函数才分为许多小函数,往往也是将类分为多个小类的时机。程序会更加有组织,也会拥有更为透明的结构。

为了修改而组织

没处修改都让我们冒着系统其他部分不能如期工作的风险,在整洁的系统中,我们对类加以组织,以降低修改的风险。

隔离修改

需求会改变,因此代码也会改变。具体类包含时间细节,而抽象类则是呈现概念。依赖于具体细节的客户类,当细节改变时,就会有风险。我们可以借助接口和抽象类来隔离这些细节带来的影响。
依赖倒置原则:类应该依赖于抽象,而不应该依赖于细节。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值