《代码整洁之道》读书笔记

序,前言

  1. 重视代码
    facebookcode review作为重点KPI考核,并采用连坐制。
    code wins argument
    当两人为一个问题争执不下时,不妨以最快的速度用代码把想法写出来,事实胜于雄辩。

  2. 代码是负债不是资产
    代码越多,维护所要付出的成本就越高。
    如果代码结构越好,做了越多单元测试,代码质量越好、越小、耦合越松,那么添加新代码付出的成本就越少。

  3. 本书观点:代码质量与整洁度成正比

一,整洁代码

  • 糟糕混乱的代码给项目带来的负面影响
    略过。

  • 什么是整洁代码
    关键字: 只做好一件事,简单直接,意图明确,单元测试,尽量少的依赖关系,看起来像是专门解决那个问题而存在。

  • 破窗理论和童子军军规
    第一扇窗被打破,但是没有人管,接下来会有更多的窗被打破,烂代码不去管它只会越来越烂。
    当你离开一个地方时,要让它比你来之前更干净。

  • 代码要易读
    我们编写代码时,读和写花费的时间比例超过10:1,要想代码易写,首先做到易读。

  • 面向对象设计的原则(SOLID)
    单一职责原则
    开闭原则
    里氏代换原则
    接口隔离原则
    依赖倒置原则

二,有意义的命名

  1. 名副其实:为变量或函数起一个能反映其含义的名字并不容易,但起名字花的时间是值得的,好的命名能让读者快速理解代码,减少维护成本。这和TDD四原则里的“揭示意图”是一致的。

  2. 作者详细介绍了命名需要注意的问题,

    naming.PNG

    结合我们的项目,印象比较深的是“每个概念对应一个词”
    项目代码中通常会有一些特定职责的类,xxxHandler, xxxManager, xxxProcessor, xxxBuilder, xxxHelper... 这样的类名含义模糊,令人费解,而这些类往往在做类似的事情。

三,函数的原则

  1. 短小
    if、else、while语句内的代码块应该只有一行,该行大抵是一个函数调用语句。
    这样的函数不仅能保持短小,而且调用的函数具有说明性的名称,从而增加了文档上的价值。
    所以函数的缩进不能多于两层。

  2. 只做一件事
    写函数是为了把大一些的概念(换言之,函数名称)拆分为另一抽象层上的一系列步骤。
    判断函数是否不止做了一件事,还有一个办法就是看是否还能再拆出一个函数。

  3. 每个函数一个抽象层级
    要让代码有自顶向下的阅读顺序,向下规则:每个函数后面跟着下一抽象层级的函数。

  4. Switch语句
    问题:太长,做了不止一件事,违反单一职责原则,违反开闭原则,到处存在类似结构的函数。
    解决方案:把switch语句放在工厂类,使用接口多态的接受派遣。

  5. 使用描述性的名称
    函数越短小,功能越集中,越便于取个好名字。

  6. 函数参数

  • 参数越少越好,参数超过三个时,排序、琢磨、忽略的问题都会加倍体现。
  • 一元函数的普遍形式:A.操作参数,转换,返回。B.传入event事件。
  • 参数过多,最好先封装成类再传入。
  • 避免使用标识参数,传入true/false,明显违反“只做一件事”的原则。
  • 避免使用输出参数,如果要修改对象的状态,要调用对象自己的函数:
    appendFooter(report);
    应该改成
    report.appendFooter();
  1. 分隔指令与询问
public boolean set(String attribute, String value);

该函数设置某个属性的值,如果设置成功返回true,如果不存在这个属性返回false。
就会导致以下语句出现:

if (set("userName", "Leo")) {...}

应该改成

if (attributeExists("userName")) {
  set("userName", "Leo")
} 
  1. 使用异常替代返回错误码
    从指令式函数返回Error Code,轻微违反了指令与询问分隔的原则,而且导致更深层次的嵌套结构。使用异常,错误处理代码就能从主路径代码中分离出来,得到简化。
if (deletePage(page) == E_OK) {
  if (registry.deleteReference(page.name) == E_OK) {
    if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
      logger.log("page deleted");
    } else {
      logger.log("configKey not deleted"); 
    }
  } else {
    logger.log("deleteReference from registry failed"); 
  }
} else {
  logger.log("delete failed");
  return E_ERROR;
}

改成

try {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
  logger.log(e.getMessage());
}

try/catch代码块违反了只做一件事的原则,应该把try和catch代码块主体部分抽出来,另外形成函数

public void delete(Page page) {
  try {
    deletePageAndAllReferrence(page);
  } catch (Exception e) {
    logError(e);
  }
}

private void deletePageAndAllReferrence(Page page) {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
  logger.log(e.getMessage());
}
  1. 别重复自己

  2. 结构化编程
    只要函数保持短小,循环偶尔出现return, break, continue没有问题,避免使用goto

  3. 函数修改的策略
    对于冗长复杂的函数,先加单元测试覆盖每行丑陋的代码,然后分解函数、修改名称、消除重复,同时保持单元测试通过。

小结
函数是动词,类是名称,编程艺术是语言设计的艺术。大师级程序员把系统当成故事来讲,而不是当作程序来写。

四、注释

当我们无法用代码表达意图时才使用注释。尽量避免使用注释,用代码表达意图。

五、格式

  1. 格式的目的
  • 代码格式关乎沟通。
  • 代码风格和可读性仍会影响到可维护性和扩展性。
  1. 垂直格式
  • 短文件通常比长文件易于理解,尽量短小而精悍。
  • 概念间垂直方向上的区隔,不同的思路段落之间用空白行隔开,例如单元测试中的Given、When、Then用空白行隔开。
  • 垂直方向上,关系密切的概念应该相互靠近。
  • 垂直顺序,被调用的函数应该放在执行调用的函数下面。
  1. 横向格式
  • 水平方向的区隔和靠近,赋值语句=号左右加空格,函数参数之间加空格,运算符优先级高的× / ÷左右不加空格,优先级低的+/-左右加空格。
  • 水平对齐。
  • 缩进表示层次。
  • 空范围。
  • 格式范例:


    code format sample.PNG

六、对象和数据结构

  1. 数据抽象
    隐藏实现关乎抽象!类并不简单的用getter、setter将其变量推向外间,
    而是暴露抽象接口,以便用户无需了解数据的实现就能操作数据本体。
    以抽象形态表述数据。
//具象点
public class Point {
  public double x;
  public double y;  
}

//抽象点
public interface Point {
  double getX();
  double getY();
  void setCartesian(double x, double y);
  double getR();
  double getTheta();
  void setPolar(double r, double theta);
}
//具象机动车
public interface Vehicle {
  double getFuelTankCapacityInGallons();
  double getGallonsOfGasoline();
}

//抽象机动车
public interface Vehicle {
  double getPercentFuelRemaining();
}
  1. 数据、对象的反对称性
  • 对象把数据隐藏于抽象之后,暴露操作数据的函数。
    数据结构暴露其数据,没有提供有意义的函数。

  • 对象与数据结构之间的二分原理:
    过程式代码(使用数据结构)便于在不改动数据结构的前提下添加新函数,
    面向对象代码便于在不改动既有函数的前提下添加新类。
    反过来说,
    过程式代码难以添加新数据结构,因为必须修改所有函数,
    面向对象代码难以添加新函数,因为必须修改所有类。

    过程式代码.PNG

    面向对象多态代码.PNG

  1. 德墨忒尔定律(迪米特法则,Law Of Demeter)
    也叫做“最少了解原理”,模块不应该了解它所操作对象的内部情形。
    C类的函数f()只能调用以下对象的方法:
  • C类的对象
  • f()创建的对象
  • 通过参数传入的对象
  • C类的实体变量对象

另一种解释:只暴露应该暴露的接口方法,只依赖需要依赖的对象

law of demeter sample.PNG

System应该只暴露close()的接口方法,而不该暴露close()内部的细节,
Person应该只依赖Container(硬件设备容器)的接口,而不该直接依赖System(操作系统)。
这样做也符合依赖倒置原则,也就是面向接口编程

火车失事
下列代码应该切分成三行。

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

是否违反德墨忒尔定律,取决于ctxt、options、scratchDir、absolutePath是对象还是数据结构。
如果是对象,应该隐藏内部结构。
如果是数据结构,则需要暴露内部结构,不算违反德墨忒尔定律。

混杂
尽量避免混合结构,一半是对象,一半是数据结构,既有执行操作的函数,又有getter/setter。同时增加了添加函数和添加数据结构的难度。

隐藏结构
经查,发现上述代码获取outputDir是为了根据路径得到BufferedOutputStream,创建文件,

String outFile = outputDir + "/" +className.replace('.', '/') + ".class";
FileOutputStream fout = new FileOutputStream(outFile);
BufferedOutputStream bos = new BufferedOutputStream(fout);

ctxt应该仅仅暴露获取BufferedOutputStream的接口方法,隐藏具体实现。

BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName);
  1. 数据传送对象
    DTO是只有公共变量(包括私有变量+公共getter/setter)、没有函数的类,是最精炼的数据结构。
    Active Record是一种特殊的DTO形式,同时也会拥有save、find方法,通常是对数据库或其他数据源的之间翻译(就是我们项目中的Domain Object,一个类对应数据库一张表)。Active Record往往被塞进业务规则方法,导致数据结构和对象的混杂体。
    应该把Active Record当成数据结构,另外创建包含业务规则、隐藏内部数据的独立对象。

小结

  • 数据结构暴露数据,没有明显行为。
    过程式代码操作数据结构,添加新的函数无需修改数据结构,但添加新的数据结构需要修改所有函数。
  • 对象暴露行为,隐藏数据。
    面向对象式代码操作对象,添加新的类无需修改既有函数,但添加新的函数需要修改所有类。
    不应对任何一种抱有成见,根据具体情况使用。

七、错误处理

对错误的处理很有必要,也很重要,要保证出现错误时程序仍能正常运行。
但不能因此让代码逻辑变得混乱。

  1. 使用异常而非返回码
    使用异常进行错误处理,能把实现部分和错误处理分离,以免错误处理影响实现部分的逻辑。

  2. 先写Try-Catch-Finally

  3. 使用不可控异常
    我的理解:可控异常违反开闭原则,修改内层方法抛出一个可控异常,外层方法都必须修改捕获这个异常,导致从内到外的修改链。
    但是使用java编写文件处理、反射的程序时,不可避免的需要捕获可控异常。
    自定义的异常都应该继承RuntimeException(不可控异常)。

  4. 给出异常发生的环境说明

  5. 依调用者需要定义异常类
    定义异常,要考虑它们如何被捕获。
    对于第三方API抛出各种不同异常的情况,可以打包API抛出通用的异常类型,简化调用时的代码。
    直接在调用API的地方捕获异常:

    api exception1.PNG

    打包调用API,简化调用代码:
    api exception2.PNG

  6. 定义常规流程
    采取特例模式(SPECIAL CASE PATTERN),创建一个类或配置一个对象,用来处理特例。客户代码就不用应付异常行为了。

  7. 不要返回或传递null值
    方法返回null,不如抛出异常或返回特例对象,否则会有NullPointerException的隐患。

小结
将错误处理和主要逻辑隔离,就能写出整洁而强壮的代码。

八、边界

我们不可避免的需要使用第三方或者其他团队开发的组件,整合到我们自己的代码中,这章主要讲如何保持软件边界整洁。

  1. 使用第三方代码
  • Map的接口功能非常丰富,接收者不要删除其中的映射
  • Map的接口一旦修改,许多地方代码需要修改
  • 不要把Map(或其他在边界上的接口)在系统中传递,否则也要保留在类中,避免从公共API返回边界接口,或把边界接口作为参数传递给公共API。
  1. 通过编写学习型测试来理解第三方代码

  2. 使用尚不存在的代码

    使用尚不存在的代码.PNG

    通信控制器依赖于Transmitter API,但API尚未定义且不受我们控制。
    先定义适合通信控制器使用的接口Transmitter,一旦提供了Transmitter API,就编写Transmitter Adapter来跨接。适配器封装了与API的互动,并且如果API发生变动,只需要修改适配器。

小结
边界上会发生我们不可控的改动,要避免我们的代码过多依赖第三方代码的细节。
使用Sensor类封装第三方接口的返回结果,或使用Adapter模式将第三方接口转换为我们需要的接口。
使用Adapter模式不仅能将不兼容的接口改写成兼容的接口,还能对第三方接口重新封装来避免边界变化对系统的影响。

九、单元测试

  1. TDD三定律
  • 在编写不能通过的单元测试前,不能编写生产代码
  • 只能编写刚好不能通过的单元测试,不能编译也算不通过
  • 只能编写刚好足以通过当前失败测试的生产代码
    我对上述定律的理解,首先必须先写单元测试再写实现,同时在写单元测试和实现时必须保持小步前进。由于真实项目的业务逻辑往往很复杂,一个story如何拆分tasking,需要在小步和项目进度之间做权衡,也取决于对TDD和重构掌握的熟练程度。
  1. 整洁的测试
    测试代码最重要的是可读性,明确、简洁、有足够的表达力。

  2. 一个测试一个断言
    作者认为单个断言是个好的准则,一个测试方法的断言数量要尽量少。
    但太过强调单个断言,会导致given和when部分有很多重复代码。
    我自己TDD的体会,一个测试方法对应一个test-case,如果测试用例拆得足够小,测试方法中的断言自然就会少,这和作者提到的每个测试一个概念应该是一致的。

  3. F.I.R.S.T
    测试还应遵守以下5条规则。

  • 快速(fast) 测试应该能快速运行,太慢了你就不会频繁的运行,就不会尽早发现问题。
  • 独立(independent) 测试应该相互独立,某个测试不应该为下个测试设定条件。当测试相互依赖,一个没通过导致一连串的测试失败,使问题诊断变的困难。
  • 可重复(repeatable) 测试应该可以在任何环境中重复通过。
  • 自足验证(self-validating) 测试应该有布尔值输出,无论通过或失败,不应该是查看日志文件去确认
  • 及时(timely) 单元测试应该恰好在使其通过的生产代码之前编写。

小结
关于单元测试的内容还有很多,这一章主要还是强调保持整洁的测试

十、类

  1. 类的组织
    公共静态常量 - 私有静态变量 - 私有实体变量 - 公共方法 - 私有方法,保证自顶向下的阅读顺序。

  2. 类应该短小
    如何判断一个类是否太长,主要看类是否承担了多个职责
    单一职责原则是OO最容易理解和遵循的原则,通常也是被违反得最多的原则。类或模块应有且只有一条加以修改的理由。系统应该有许多短小的类而不是巨大的类组成,每个小类封装一个职责。

  3. 内聚
    如果一个类中的每个变量都被每个方法所使用,则该类具有最大的内聚性。内聚性高,意味着类中的方法和变量相互依赖,相互结合成一个逻辑整体。
    保持内聚性就能得到短小的类,一旦发现类失去内聚性,就拆分它!当某些实体变量只被少数方法使用,就应该拆分出一个类。

  4. 为了修改而组织
    通过多态将一个大类中的细节隔离,等同于把修改隔离,符合开闭原则
    让调用方依赖接口而不依赖细节,符合依赖倒置原则
    隔离细节,更利于单元测试。

十一、系统

将系统的构造和使用分开:构造和使用是不一样的过程。

  1. 工厂
    使用抽象工厂模式,将构造的细节隔离于应用程序之外。

  2. 依赖注入(DI/IOC)
    在依赖管理情景中,对象不应该负责实例化对自身的依赖,反之,它应该将这份权责移交给其他有权利的机制,从而实现控制的反转。

  3. 扩容
    “一开始就做对的系统”纯属神话。
    反之,我们应该只实现今天的用户的需求。
    然后重构,明天再扩容系统,实现新用户的需求。

  4. 面向切面编程(AOP)
    AOP中,被称为方面(aspect)的模块构造指明了系统中哪些点的行为会以某种一致的方式被修改,从而支持某种特定的场景。这种说明是用某种简洁的声明(Attribute)或编程机制来实现的。

小结
这一章的概念和描述比较多,例子不多,看完并没有很深的体会。
但是工厂模式、依赖注入、AOP这些在项目中都有应用,关于Spring AOP可以参考《Spring实战》和这篇文章Spring之AOP由浅入深

十二、迭进

  1. 简单设计规则1 运行所有测试
    紧耦合的代码难以编写测试。同样编写测试越多,就会越遵循DIP之类的原则,使用依赖注入,接口和抽象等工具尽可能减少耦合。如此一来设计就会有长足进步。遵循有关编写测试并持续运行测试的、明确的规则,系统就会更贴近OO低耦合度、高内聚的目标。

  2. 简单设计规则2 重构
    在重构过程中,可以应用有关优秀软件设计的一切知识,提升内聚性,降低耦合度。换句话说:消除重复,保证表达力,尽可能的减少类和方法的数量。

  3. 不可重复
    重复代表着额外的工作、额外的风险和额外不必要的复杂度。重复有多种表现。雷同的代码行是一种。不但是从代码行的角度,也要从功能上消除重复。

  4. 揭示程序员意图

十三、并发编程

  1. 为什么要并发
    并发是一种解耦策略,它帮助我们把做什么(目的)和何时(时机)做分解开。
    在单线程应用中,目的与时机紧密耦合。
    而解耦目的与时机能明显地改进应用程序的吞吐量和结构。
    从结构的角度看,应用程序看起来更像是许多台协同工作的计算机,而不是一个大循环。
    单线程程序许多时间花在等待Web套接字I/O结束上面。

  2. 迷思与误解

  • 并发总能改进性能:并发有时能改进性能,但只在多个线程或处理器之间能分享大量等待时间的时候管用。

  • 并发编程无需修改设计:并发算法的设计可能与单线程系统的设计极不相同,目的与时机的解耦往往对系统结构产生巨大影响。

  • 在采用Web和EJB容器时,理解并发问题不重要:最好了解容器在做什么,如何应付并发更新、死锁等问题。

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

  • 正确的并发是复杂的,即使对于简单的问题也是如此。

  • 并发缺陷并非总能重现,所以常被看做偶发事件而忽略,而未被当做真的缺陷看待。

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

平时工作中并发编程涉及得很少,读起来体会不深,转载一篇java并发编程相关的文章,以后继续学习
关于Java并发编程的总结和思考

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

推荐阅读更多精彩内容