0%

前言

习艺之要有二:知和行。你应当习得有关原则、模式和实践的知识,穷尽应知之事,并且要对其了如指掌,通过刻苦实践掌握它。

假如只有知,没有行。很多时候只是在阅读的时候感觉良好,但是在实际编程的时候却又写出糟糕的代码。因此,为了真正掌握编程的良好习惯,需要跟着作者的思路,进行完整的拆解。第一部分和第三部分是理论,读完只会感觉良好,而第二部分是拆解,是核心所在,只有好好阅读这部分才能习得精湛技艺。

第1章 整洁代码

代码永存。

好代码很重要。

稍后等于永不。

写整洁代码,需要遵循大量的小技巧。我们称之为代码感。要想拥有良好的代码感,需要阅读代码、写代码,琢磨为什么这么写,该怎么写。

本书前传:《敏捷软件开发》。本书中涉及的一些概念在前传中有提到,假如不懂可以找这本书看看。

第2章 命名

要让命名名副其实、有意义。因为这样可以让代码更容易理解和修改。

避免误导:防止使用某些专有名词,防止使用List来命名map,防止使用相似的名称等。

做有意义的区分:不要随便使用错误的拼写或数字来绕过重复的命名,而应该使用有意义的区分。避免废话,废话就是冗余,应该消除冗余。为了做有意义的区分,可以添加形容词、场景等。

使用读得出来的名称,因为人类擅长记忆和使用单词。不要使用缩写等不易读的、需要解决的命名。

使用可搜索的命名,单字母名称仅用于短方法中的本地变量,名称长短应与其作用域大小相对应。若某个变量或常量使用得较多,应赋予其便于搜索的名称。

尽量避免使用编码,现代的编辑环境已经可以侦测到类型错误,不需要使用这种落后的标记方式。强行使用编码,反而会让代码不易读,成为冗余。所谓编码就是在变量上添加int, string等标志,我一般会在需要类型转化时这么命名。

类名应该是名词或名词短语。不要使用比较泛的命名,比如Data, Info, Core等。

方法名应该是动词或动词短语。属性访问器、修改器和断言应该根据其值命名,并加上get, set, is前缀。重载构造器时,使用描述了参数的静态工厂方法名,比如Complex.FromRealNumber(23.0)而不是new Complex(23.0)。

别扮可爱,即不要使用梗来命名,这样不易被所有人理解。

每个概念对应一个词。举个例子,controller和manager表示的是同个意思,就只用一个就好了,假如混用两者容易让人困惑,以为它们的作用不同。

别用双关语。举个例子,假如Add用来表示两个数相加,那么就不要用来表示将一个数插入到集合中,而应该用Append或Insert来区分它。

添加有意义的语境。常用的方法是将相关的变量一起放到一个类中,赋予这些变量一个语境。

第3章 函数

函数的第一条规则是要短小,第二条规则还是要更短小。

函数应该做一件事。做好这件事。只做这一件事。

只做一件事的函数的一个标志是不可以再被切分为多个函数区段。

每个函数一个抽象层级。向下规则,一系列To起头段落。

switch语句:应该将它埋在较低的抽象层,并且用多态来让它符合SRP、OCP原则。

函数命名:使用描述性的名称,别害怕长名称,别害怕花时间取名字,命名方式要保持一致。

函数参数:

  • 最理想的参数数量是零,其次是一,再次是二,应尽量避免三。
  • 过多的参数会影响可读性、测试的复杂度
  • 入参将布尔变量作为标识参数,是非常丑陋的,因为它说明了这个函数不只做了一件事情,应该尽快将其分成两个函数
  • 当参数过多时,应该将一些参数封装成类,进行抽象
  • 函数名称一般以动词或动宾结构为佳

无副作用:函数中不应该做其他被隐藏起来的事。若有副作用,可能会导致古怪的时序性耦合或出乎预期的结果。

尽量避免使用输出参数,所谓输出参数,是指用入参中的参数用来输出。应该使用面向对象的思想,封装起来会更易读些。在实际工程中,确实有这么写的,确实难读,需要掌握封装对象的方法。

分隔指令与询问:在具有歧义性的多个入参的函数时,解决方案是把指令与询问分隔开来,防止混淆的发生。(指令和询问混合的例子是什么?)

别重复自己:整个模块的可读性会因为重复的消除而得到了提升。

结构化编程:每个函数、函数中的每个代码块都应该只有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永远不能有任何goto语句。对于小函数,这些规则助益不大,只有在大函数中才会有明显的好处。

第4章 注释

作者认为无需写注释是最完美的。这一点对于业务来说可能是合适的,但是对于开源库来说,还是需要注释来简明扼要地说明函数的行为。

只有代码不会骗人。注释会骗人,文档也会骗人,因为随着时间的推移会过时。

注释不能美化糟糕的代码。假如想要写注释,那么该问问自己代码是不是写得太烂了,需要重构。

用代码来阐述,能用函数或变量时就别用注释。比如一个很长的if表达式,与其用注释来说明,不如将它提取成一个函数,并起个合适的函数名。

好注释:

  • 法律信息
  • 提供有用信息的注释
  • 对意图的解释
  • 阐释,和解释的区别在于它是描述性的,便于阅读
  • 警示
  • Todo注释

坏注释:

  • 喃喃自语
  • 循规式、多余的注释
  • 误导性、过时的注释
  • 日志式注释
  • 注释掉的代码,现在的代码工具能很快找回来,简洁更重要
  • 不明显的联系。假如注释本身还不够清晰,还需要注释来说明注释,显然这是个坏注释

第5章 格式

好的格式能提高代码的可读性。这里的格式不仅是括号换行之类的问题,还包括文件的代码行数等问题。Go中有gofmt,可以帮助干一些格式的工作,但并没有囊括所有。

垂直格式:

  • 短文件通常比长文件易于理解。
  • 源文件名称应当简单且一目了然。
  • 源文件最顶部应该给出最高层次的概念和算法,细节应该往下逐次展开。
  • 源文件中的不同概念,应该用空白行隔开。比如封包声明、导入声明和每个函数之间要有空白行。
  • 源文件中的有联系的概念,应该相互靠近。比如有关系的几个变量。
  • 关系密切相关的概念,应该放在同一个文件中。避免在源文件之间跳转。
  • 注重垂直距离。
    • 变量声明:变量声明应该尽可能地靠近其使用位置。类或结构体的变量应该统一放在类的顶部或底部,关键是位置要统一。
    • 相关函数:如果某个函数调用到了另外一个,就应该把他们放在一起,而且调用者应该尽可能放在调用者上面。

横向格式:

  • 一行的字符数不要超出120字符。

团队规则:

  • 一个团队应该约定一套编码规范。好的代码需要拥有一致和顺畅的风格,这样能减少阅读的复杂度。

第6章 对象和数据结构

对象和数据结构的区别:对象把数据隐藏于抽象之后,暴露操作数据的函数。数据结构暴露其数据,没有提供有意义的函数。

书中提到的对数据结构的这种说法我是不赞同的,因为数据结构通常也提供有意义的函数,提供抽象。比如很多学过的数据结构,队列、栈、二叉树、红黑树,实际上都是暴露操作数据的函数。作者这里想表达的内容是,应该进行有意义的、更高一层的抽象,将接口定义和底层实现分开。

但是,书中也提到了,暴露操作和暴露数据之间,也各有适用的场景。我们称暴露数据的代码称为过程式代码,暴露操作的代码称为面向对象代码。

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

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

其实,我对这里的难点没有特别体会到,感觉两种方式都OK。借助于现在智能的IDE,跳转起来都还挺方便的,修改的工作量似乎也差不多。但是这里有一个点是,假如自己发现用某种做法比较难时,可以想想有没有其他更容易的做法。

什么是德墨忒尔定律,解决了什么问题?

用于解决多个模块过于耦合的问题,是一个为了让模块之间松耦合的有效方法。该定律内容简单来说,是一个模块不应该了解其他模块的底层细节。举个例子,有A, B, C三个类,对应三个实例是objA, objB, objC,假如objA依赖了objB, objB依赖了objC,那么objA应该只关心objB的方法和属性,不应该关心objC的细节,objB在和objC交互后应该将结果封装给objA。其实,在微服务中,也存在这种思想,假设有A, B, C三个服务,A依赖了B,B依赖了C,现在A需要处理下游服务的状态码,它应该只关心B服务返回的状态码,而不需要关心服务C返回的状态码。假如不这么做,假设B有很多个底层服务,那么A就需要关心很多个底层服务了,这会很耦合。

假如是一个结构体有多个层级,访问底层的成员时会用类似a.b.c.d这种方式,这并不被认为违反德墨忒尔定律,因为它很简单清晰。

第7章 错误处理

主要介绍了Java使用Exception处理错误的方法。在Go中,处理错误一般是逐级传递error。Exception相比于error传递的好处在于,可以不需要逐级传递,在遇到错误时throw exception,在最外层进行try catch处理exception。但是这样做也有坏处,throw的地方会散布于各处。

不管是抛出错误还是异常,多应该给出明确的异常发生的环境说明,方便定位问题。

对错误进行分类、抽象、封装,可以让错误更加清晰。

该章节的一些其他建议和Java比较耦合,比如不返回null。作为一个Gopher,对其缺少共鸣,在Go中是经常会返回nil的,对nil的检查也很常见。

总的来说,错误处理的方式是多样的,不同语言有不同的流派。写过Java的人可能更喜欢Exception的处理方式,之前在一个课程上遇见过在Go中使用Exception来处理异常的,挺hack的。

错误处理的目标应该是尽可能清晰、统一,我认为Go的error处理是符合这一个目标的。虽然逐级处理error有点麻烦,但是这样做可以保证可读性,足够清晰。

第8章 边界

边界是指自身代码和第三方程序、开放源代码的界限。本章节主要介绍保持软件边界整洁的实践手段和技巧。

  • 封装第三方代码。当第三方代码的接口是比较通用的时候,当接入自身系统时就需要一定的转换。为了避免在每一处代码都进行转换,可以对其进行一层封装,屏蔽转换的细节。泛型是避免转换的一种方式,自己写个数据结构封一层也是一种方式。
  • 使用adapter模式使用尚不存在的代码。假如第三方的API的具体定义还不确定,可以先定义一个interface,写上我们预期的行为,此后的开放基于该interface进行开发。当第三方API确定后,定义一个adapter,使用它们的API接口实现我们定义的interface。这样,我们就可以在不被它们API阻塞的情况下完成开发。

第9章 单元测试

保持测试的整洁,测试代码也应该像正式代码一样可维护。

FIRST原则:

  • Fast: 可以快速执行完
  • Independent: 可以并发执行,无相互依赖
  • Repeatable: 可以重复执行,无副作用
  • Self-Validating: 可以自我验证,有意义的校验可以发现问题
  • Timely: 及时编写,在写正式代码的时候就产生测试代码

第10章 类

单一职责原则:类应该只有一个修改的原则。

为了保证上面这个原则,类应该尽可能小。

内聚:类中的变量尽可能都被每个方法用到。

可以将类拆小,将可能发生联系的方法和变量放到一个类,从而让这个类更内聚。

问题:10.3中举的SQL拆分例子,拆分后的类方法generate(),是怎么work的啊?

第11章 系统

要系统层级上面的整洁。使用抽象。

将系统的构造与使用分开。假如不分开,测试会有点难写,因为缺少抽象,难以mock。

分开方法:

  • 集中在main函数进行构造
  • 工厂:可以在运行的时候调用,但是构造还是由工厂方法自己负责
  • 依赖注入

感觉这一章较多知识和Java相关,比如所谓的POJO, Java代理, EJB, Java AOP, AspectJ等。

其中,Java AOP将特定领域的东西横切成面,统一处理的思想。是有点感触的,但是不深。

DSL:领域特定语言,用于描述系统层级的各个领域。感觉和银平大佬推的什么宣言有点类似。

打造一个系统,也可以用保守主义的思想,先打造出一个大致可行的简单方案,不要一上来就进行恢弘的设计。根据后面的需求,有更多输入的情况下,再对系统进行演进。不要过度设计,不要提前恢弘设计。

第12章 迭进

这一章提到了改进系统设计的实用技巧:

  1. 运行所有测试
  2. 不可重复
  3. 表达力
  4. 减少重复

第一条是改进的基础,第二到第四条是重构中要注意的点。

实际工作中,测试都没有跑起来,后面的几点也无从谈起啊!

第13章 并发编程

servlet标准模式,是Java的一种web框架,类似于Go的gin框架。提供并发处理http请求的解决方法,让工程师集中集中精力写业务逻辑,不需要关心底层的TCP连接、HTTP协议解析、复用线程、IO异常处理等。

许多Java相关的部分。总的来看,有以下写好并发编程代码的方法:

  • 学习并发问题的可能原因:data race;使用了公共资源池,比如线程池。
  • 学习并发技术及类库:自旋锁;mutex;atomic
  • 掌握并发的基本常识
    • 缩小临界区,减少锁的影响范围
    • 减少共享对象,使用通信的方式(为什么?可能是让读和写更明显了)

第14章 逐步改进

作者说了,案例才是这本书最精华的部分,所以要好好研读,琢磨消化。

派生类:基于基类的类,拥有基类的方法。解决代码重用的问题,DRY。

看了两三次,对于一开始介绍的最终版本没有看懂,有点受打击。先跳过吧,因为这部分内容和Java也关系有点多。

第15章 JUnit内部

看标题就是和Java相关比较大,可以先抱着粗略的态度看看。

实际看下来,和Java关系不大,能看懂,有收获感。重点看看作者做出某种重构的考量。

但是感觉实际工作中,假如抱着这样的态度去改态度,会累死= =、除非有契机去推动这样的事情发生。

第16章 重构SerialDate

作者通过单元测试,发现这个库存在的许多问题。这说明单元测试是挺有用的,可以发现算法问题。对于业务代码,当然也是有用的,但是业务中的测试,会面临许多外部调用的mock。有什么方便的方法去mock业务中的外部调用吗,让测试更容易。

重点看看作者做出某种重构的考量,比如命名、函数拆分、细小改变等。感觉作者这种组织模式很不适合阅读,看起来费劲,所以我只粗略看看。

第17章 味道与启发

作者在读了一些源代码后,在实践中提出了他积累的一个CheckList。不妨从中看看对自己有感触的部分。

服务编译应该追求一个脚本就可以构建。假如有仓库依赖,建议在README做好相关说明,或者在编译脚本中给出指南。

同理,单元测试也应该追求一个脚本就可以跑完所有单元测试。可以将必要的环境变量等信息封装到脚本中。

DRY原则我们都懂,大佬们也认为这是最重要的一个原则之一。努力践行这个原则,可以提升自己的设计抽象能力。

问题:如何践行DRY原则,有哪些具体的编程方法?

有OO、结构化编程、数据库范式等。

我发现测试不可读的一个原因,是其中存在许多隐式的约定、魔数等坏味道的代码。假如我们对待测试也像业务代码那样认真,就可以提高可读性。

在较高层级放置默认值,这一点比较好。我看了ares有些库,会将默认值放在底层,就不太可读,常常需要跳转好几次才能找到默认值。或者注释也是一种方法。

名称应该说明副作用。这一条比较有感触,自己在工作看到getXXX()的函数,并不一定会想到他会有写操作。用createOrReturnXXX()就表明可能有写操作。

第一遍读完回顾

时间:2023年2月19日晚上

断断续续读了挺久的这本书,今晚终于读完了。

问题:如何发挥这本书的价值?

  • 最重要的是保持匠心,追求代码的整洁
  • 其次,建立这本书的思维导图,心中有个全景图,做一个checkLIst
  • 最后,常常回顾一下这本书,常读常新