《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记

栏目: 编程工具 · 发布时间: 5年前

内容简介:《代码整洁之道》主要讲述了一系列行之有效的整洁代码操作实践。软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相关。这一点,无论是敏捷开发流派还是传统开发流派,都不得不承认。这本书的阅读对象为一切有志于改善代码质量的程序员,书中介绍的规则均来自作者Bob大叔多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。封面图片:

《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记

There are only two hard things in Computer Science: cache invalidation and naming things.
-- Phil Karlton

《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记

《代码整洁之道》主要讲述了一系列行之有效的整洁代码操作实践。软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相关。这一点,无论是敏捷开发流派还是传统开发流派,都不得不承认。这本书的阅读对象为一切有志于改善代码质量的程序员,书中介绍的规则均来自作者Bob大叔多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一“家”之言,然诚有可资借鉴的价值。

封面图片:

《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记

上面这张图是M104:草帽星系,其核心是一个质量超大的黑洞,有100万个太阳那么重,环绕着M104的光环就像一顶墨西哥草帽,仿佛经历了大爆炸之后碎片四溅的产物。联系到我们所经历过的没由整洁代码风格各异不可维护的软件项目,其实当你接手时之前的代码都是一个个的黑洞,存在着某天会定时爆发的风险,而当它真正爆发时,接手这个项目的所有人都会因此遭殃。

其次,再说说副标题:“ 细节之中自有天地,整洁成就卓越代码 ”。本书讲述的就是一个又一个的细节之处,不好的处理和好的处理都一一道来,让我们形成整洁的规范。

Robert C. Martin,(Bob大叔)自1970年进入软件行业,从1990年起成为国际软件咨询师。是软件工程领域的大师级人物,是《敏捷软件开发:原则、模式与实践》、《敏捷软件开发:原则、模式与实践(C#版)》(邮电)、《极限编程实践》(邮电)等国内引进的畅销书的作者,其中第一本原著荣获美国《软件开发》第13届震撼(Jolt)大奖,Martin的敏捷系列书是软件工程界的权威书籍。

Clean Code:

代码逻辑应该直接了当,叫缺陷难以隐藏;

尽量减少依赖关系,使之便于维护;

依据某种分层战略完善错误处理代码;

性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来;

整洁的代码只做好一件事;

有单元测试和验收测试;

有意义的命名;

尽量“少”;

两条重要原则:

尽量让代码易读,开发时减少读的时间。

童子军军规:“让营地比你来时更干净”。

1. 一切代码与注释都是有实际意义的,没有冗余,整洁干净

2. 代码能通过所有测试,运行高效

3. 命名名副其实,区分清晰,意义明了,尽量做到看名字就能知道你的意图

4. 代码逻辑尽量直白,简单

5. 每个方法只做一件事,功能明确且单一,方法间层次分明

6. 每个类职责尽量单一,高内聚,类与类之间低耦合

7. 测试覆盖面广,每个测试用例基本只测一个点

8. 测试代码的要求与业务代码一样高.

第一章 为什么要整洁代码

1、代码永不消失

代码就是衔接 人脑理解需求的含糊性机器指令的精确性 的桥梁。哪怕未来会有对现在高级编程语言的再一次抽象——但这个抽象规范自身仍旧是代码。

所以既然代码会一直存在下去,且自己都干了 程序员 这一行了,就好好的对待它吧。

2、读远比写多

当你录下你平时的编码的过程,回放时,你会发现读代码所花的时间远比写的多,甚至超过 10:1。所以整洁的代码会增加 可读性

3、对抗拖延症—— 稍后等于永不

糟糕的代码会导致项目的 难以维护 。而当你正在写糟糕的代码的时候,心里却圣洁的想:“有朝一日再回来整理”。但现实是残酷的,正如 勒布朗(LeBlanc)法则 :稍后等于永不(Later equals never)

4、精益求精

写代码就跟写文章一样,先自由发挥,再细节打磨。 追求完美

大师级程序员把系统当作故事来讲,而不是当作程序来写。

第二章 命名

1、名副其实

(×)图表:mapTable

(√)图表:chart

推荐一个根据中文意思帮你起英文变量名的网址: https://unbug.github.io/codelf/

《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记

2、避免误导

(1)不要用专有名词

const var = 0;

(2)避免细微之处有不同

XYZControllerForEfficientKeepingOfStrings

XYZControllerForEfficientHoldingOfStrings

《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记

(3)避免废话

如果是一个数组类型,就没必要叫 ProductArray

如果返回值就是数据,就没必要叫 ProductData

(4)便于搜索

(×)

if ( team.length === 3 )

(√)

const MAX_NUM_OF_TEAM = 3 ;

……if ( team.length === MAX_NUM_OF_TEAM )

MAX_NUM_OF_TEAM 比 3 好检索.

早期流行一种 匈牙利语标记法

如 arru8NumberList 的前缀 "arru8" 表示变量是一个无符号8位整型数组;

3、避免思维映射

类名、参数名用名词:

member

leader

方法名用动词:

getTeam

根据 Javabean 的标准,方法名可以加上 get set is 前缀

例如循环中的  i、j、k  等单字母名称不是个好选择;读者必须在脑中将它映射为真实概念。最好用  filter、map  等方法代替  for循环.

4、每个概念用一个词

get fetch 选一个,不要混着用.

5、添加有意义的语境

如果你要记录 member 的详细住址,会设置了如下几个变量:

(×)

addrStreet

addrHouseNumber

addrCity

addrState

(√)

new Address {

street,

houseNumber,

city,

state

}

第三章 函数

  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. 函数修改的策略

    对于冗长复杂的函数,先加 单元测试

    覆盖每行丑陋的代码,然后

    分解函数、修改名称、消除重复

    ,同时保持单元测试通过。

小结

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

(ref:https://www.jianshu.com/p/822549096a82)

第四章 注释

尽量用代码表达,而不是用注释

就像上文提到的用详尽的函数名代替注释,或者:

//代码过于追求简洁而导致这里要加详细的注释

if( smodule.getDependSubsystems().contains(subSysMod.getSubSytem()) )

还不如这里做拆分,取易懂的变量名方便理解,就可以不用加注释或者少加

ArrayList moduleDependees = smodule.getDependSubsystems();

String ourSubSystem = subSysMod.getSubSystem();

if( moduleDependees.contains(ourSubSystem) )

原因是:注释存在的越久,随着代码的不断迭代,会离代码的距离越来越远,这个时候好的做法是同时维护代码 + 注释,而注释越多越复杂, 维护的成本 自然就上升了。

注释不能美化糟糕的代码,尽量去优化代码

好注释

(1)法律信息、许可证、版权、著作权、外链文档的 url

(2)对意图的解释

(3)警示

(4)TODO 注释

坏注释

(1)只有自己看得懂

(2)多余的注释

a、不能提供比代码更多的信息

b、读的时间比直接看代码还久

(3)歧义性

(4)循规蹈矩的注释

例如用第三方 工具 生成的注释,很多都是累赘的废话

(5)日志式、署名式注释

(×)

//  write by colin//  fix #201 bug

(√)

交给 git 记录

(6)慎用位置标记

// **********************

及时清理不需要的注释

(1)越堆越多

(2)导致以后因看不懂而不敢删

第五章 格式

向报纸学习

(1)从上往下读,从左往右读

(2)源文件在最顶端给出高层次的概念和算法,细节往下逐次展开,直到最底层的函数和细节。

垂直格式

(1)善用空白行:人会更容易将目光聚焦到空白行之后的那一行

(2)函数:调用者放在被调用者上面

横向格式

(1)缩进

(2)IDE 中不会出现横向滚动条

第六章 对象和数据结构

  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. 数据、对象的反对称性

  • 对象把数据隐藏于抽象之后,暴露操作数据的函数。

    数据结构暴露其数据,没有提供有意义的函数。

  • 对象与数据结构之间的二分原理:

    过程式代码 (使用数据结构)便于在不改动数据结构的前提下添加新函数,

    面向对象代码 便于在不改动既有函数的前提下添加新类。

    反过来说,

    过程式代码 难以添加新数据结构,因为必须修改所有函数,

    面向对象代码 难以添加新函数,因为必须修改所有类。

    《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记

    过程式代码

    《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记

    面向对象多态代码

  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当成数据结构,另外创建包含业务规则、隐藏内部数据的独立对象。

小结

  • 数据结构暴露数据,没有明显行为。

    过程式代码操作数据结构,添加新的函数无需修改数据结构,但添加新的数据结构需要修改所有函数。

  • 对象暴露行为,隐藏数据。

    面向对象式代码操作对象,添加新的类无需修改既有函数,但添加新的函数需要修改所有类。

    不应对任何一种抱有成见,根据具体情况使用。

(ref: https://www.jianshu.com/p/822549096a82 )

第七章 错误处理

抽离Try/Catch代码块

函数应该只做一件事,错误处理就是一件事。

// bad
public void delete(Page page) {
    try{
        deletePage(page);
        registery.deleteReference(page.name);
        configKeys.deleteKey(page.name.makeKey();
    }catch(Expection e){
        logError(e);
    }
}
// good
public void delete(Page page) {
    try{
        // 将上例的操作,封装到一个方法
        deletePageAndAllReferences(page);
    }catch(Expection e){
        logError(e);
    }
}

使用非受控异常

受控异常: Checked Exception(FileException、SQLException等) ,这类异常必须写  try/catch ,或者  throw抛出 ,否则编译通不过。

非受控异常: Unchecked Exception ,这类异常也叫做运行时异常(与非受控异常 字数相等),这类异常不需要  try/catch ,也不需要  throw抛出 。即使 throw 了,上层调用函数也非必须捕获,编译能通过。

受控异常的代价就是违反开放/闭合原则。如果你在方法中抛出受控异常,这意味着每个调用该函数的函数都要修改,捕获新异常,或在其签名中添加合适的throw语句。对于一般的应用开发,受控异常依赖成本要高于收益成本,尽量 try/catch 处理,不要抛出。

给出异常发生的环境说明

应创建信息充分的错误信息,并和异常一起传递出去。在消息中,包括 失败的操作和失败类型 。如果你的应用程序有日志系统,传递足够的信息给catch块,并记录下来。

依调用者需要定义异常类

// bad
ACMEPort port = new ACMEPort(12);
try {
    port.open();
} catch(DeviceResponseException e) {
    reportPortError(e);
    logger.log("Device response exception",e);
} catch(ATM1212UnlockedException e) {
    reportPortError(e);
    logger.log("Unlock exception",e);
} catch(GMXError e) {
    reportPortError(e);
    logger.log("Device response exception",e);
} finally {
    // .....
}

通过打包调用API,确保它返回通过用异常类型,从而简化代码

// good
LocalPort port = new LocalPort(12);
try {
    port.open();
} catch(PortDeviceFailure e) {
    reportError(e);
    logger.log(e.getMessage(),e);
} finally {
    // .....
}
public class LocalPort{
 private ACMEPort innerPort;
 
 public LocalPort(int portNumber){
     innerPort = new ACMEPort(portNumber);
 }
 
 public open() {
  try {
       innerPort.open();
   } catch(DeviceResponseException e) {
           // 自定义的异常类
       throw new PortDeviceFailure(e);
   } catch(ATM1212UnlockedException e) {
       throw new PortDeviceFailure(e);
   } catch(GMXError e) {
       throw new PortDeviceFailure(e);
   }
 }
}

将第三方API打包是个良好的实践手段。当你打包一个第三方API,你就降低了对它的依赖。

(ref: https://segmentfault.com/a/1190000015995913 )

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

不要返回或传递null值

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

小结

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

《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记


以上所述就是小编给大家介绍的《《代码整洁之道》细节之中自有天地,整洁成就卓越代码 读书笔记》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

区块链核心算法解析

区块链核心算法解析

【瑞士】Roger Wattenhofer(罗格.瓦唐霍费尔) / 陈晋川、薛云志、林强、祝庆 / 电子工业出版社 / 2017-8 / 59.00

《区块链核心算法解析》介绍了构建容错的分布式系统所需的基础技术,以及一系列允许容错的协议和算法,并且讨论一些实现了这些技术的实际系统。 《区块链核心算法解析》中的主要概念将独立成章。每一章都以一个小故事开始,从而引出该章节的内容。算法、协议和定义都将以形式化的方式描述,以便于读者理解如何实现。部分结论会在定理中予以证明,这样读者就可以明白为什么这些概念或算法是正确的,并且理解它们可以确保实现......一起来看看 《区块链核心算法解析》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具