第一章 整洁代码(2024.6.4)
- 态度:对项目负责,对经理提出自己的专业看法
- 时间和混乱的代码:节省时间的唯一方法就是始终尽可能的保持代码整洁
- 能分辨优劣的代码不意味着能写整洁代码,需要遵循大量的小技巧,刻苦练习
整洁的代码只做好一件事
第二章 有意义的命名(2024.6.5)
- 名副其实:int d;//消逝的时间,以日计 => int elapsedTimeInDays
- 避免误导:XYZControllerForEfficientHandlingOfStrings 与XYZControllerForEfficientStorageOfStrings
- 做有意义的区分:accountData 与 account
- 使用读的出来的名称:genymdhms => generationTimestamp
- 使用可搜素的名称:名称长短应与其作用域大小相对应,单字母命名 => 单词组合
- 避免使用编码
- 避免思维映射:明确是王道
- 类名和对象名应该是名词或名词短语
- 方法名应当是动词或动词短语
- 别扮可爱:HolyHandGrenade => DeleteItems
- 每个概念对应一个词:给每个抽象概念选一个词,并且一以贯之。例如,使用fetch、retriieve和get来给在多个类中的同种方法命名
- 别用双关语:避免将同一单词用于不同目的。
- 使用解决方案领域名称:使用计算机相关术语命名
- 使用源自所涉问题领域的名称:如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称
- 添加有意义的语境:firstName => addrFirstName
- 不要添加没用的语境:只要短名称足够清楚,就要比长名称好。别给名称添加不必要的语境。
取好名字最难的地方在于需要良好的描述技巧和共有文化背景。
第三章 函数(2024.6.6)
- 短小
- 只做一件事:如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事
- 每个函数一个抽象层级:自顶向下读代码:向下规则
- switch语句:用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到
- 使用描述性的名称:函数越短小 、功能越集中,越便于取个好名字
- 函数参数:最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)
- 一元函数的普遍形式:转换:有输出值 InputStream fileOpen("MyFile")
- 标识参数:不应该传入布尔值,应该把函数一分为二
- 二元函数:尽量转换成一元函数,writeField(outputStream,name) => outputStream.write Field(name) 或者将outputStream定义为成员变量
- Circle makeCircle(double x,doubleradius) => Circle makeCircle(Point center, double radius)
- 关键词和动词:assertEqual => assertExpectedEqualsActual(expected, actual)
- 无副作用
- 不在函数中做命名没有描述的事,不然可能会导致时序性耦合以及顺序依赖
- 应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象象的状态吧。
- public void appendFooter(StringBuffer report) => report.appendFooter();
- 分割指令与询问:函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态或是返回该对象的有关信息。两样都干常会导致混乱。
- 使用异常代替返回错误码
- 别重复
- 如何写出这样的函数:先写出代码,然后细细打磨,按照规则组装函数
编程艺术是且一直就是语言设计的艺术,大师级程序员把系统当成故事来讲
第四章 注释 (2024.6.7)
- 注释不能美化糟糕的代码
- 用代码来阐述
- 好注释
- 法律信息
- 提供信息的注释
- 对意图的解释
- 阐释
- 警示
- TODO
- 放大:放大重要性
- 公共API
- 坏注释
- 喃喃自语
- 多余的注释
- 误导性注释
- 循规式注释
- 日志式注释
- 废话注释
- 注释掉的代码
- HTML注释
- 非本地信息
- 信息过多
- 不明显的联系
程序员应当负责将注释保持在可维护、有关联、精确的高度。我同意这种说法。我更
主张把力气用在写清楚代码上,直接保证无须编写注释。
第五章 格式(2024.6.11)
- 垂直格式:用大多数为200行、最长500行的单个文件构建出色的系统
- 源文件也要像报纸文章那样。名称应当简单且一目了然。名称本身应该足够告诉我们是
否在正确的模块中。源文件最顶部应该给出高层次概念和算法。细节应该往下渐次展开,直至找到源文件中最底层的函数和细节。 - 几乎所有的代码都是从上往下读,从左往右读。每行展现一个表达式或一个子句,每组代码行展示一条完整的思路。这些思路用空白行区隔开来。
- 如果说空白行隔开了概念,靠近的代码行则暗示了它们之间的紧密老关系。
- 对于那些关系密切、放置于同一源文件中的概念,它们之间的区隔应该成为双材相互的易
懂度有多重要的衡量标准。应避免迫使读者在源文件和类中跳来跳去。- 变量声明:尽可能的靠近其使用位置
- 实体变量:在类的顶部声明
- 相关函数:若某个函数调用了另外一个,就应该把它们放到一起,而且调用者应该尽可
能放在被调用者上面 - 概念相关:概念相关的代码应该放到一起。相关性越强,彼此之间的距离就该越短
- 一般而言,我们想自上向下展示函数调用依赖顺序。也就是说,被调用的函数应该放在
执行调用的函数下面。这样就建立了一种自顶向下贯穿源代码模块的的良好信息流
- 源文件也要像报纸文章那样。名称应当简单且一目了然。名称本身应该足够告诉我们是
- 横向格式:尽力保持代码短小,不超过120个字符
- 我们使用空格字符将彼此紧密相关的事物连接到一起,也用空格字将把相关性较弱的事
物分隔开。 - 缩进
- 空范围:尽量不使用
- 我们使用空格字符将彼此紧密相关的事物连接到一起,也用空格字将把相关性较弱的事
- 一组开发者应当认同一种格式风格
记住,好的软件系统是由一系列读起来不错的代码文件组成的。它们需要拥有一致和顺畅的风格。读者要能确信,他们在一个源文件中看到的格式风格在其他文件中也是同样的用法。绝对不要用各种不同的风格来编写源代码,这样会增加其复杂度。
第六章 对象和数据结构(2024.6.12)
-
数据抽象
-
数据、对象的反对称性:在任何一个复杂系统中,都会有需要添加新数据类型而不是新函数的时候。这时,对象和面向对象就比较适合。另一方面,也会有想要添加新函数而不是数据类型的时候。在这种情况下,过程式代码和数据结构更合适。
-
- 得墨忒耳律:模块不应了解它所操作对象的内部情形
- 类C的方法f只应该调用以下对象的方法:
- 由f创建的对象;
- 作为参数传递给f的对象;
- 由C的实体变量持有的对象;
- 方法不应调用由任何函数返回的对象的方法。换言之,只跟朋友谈话,不与陌生人谈话
- 反面例子:final String outputDir=ctxt.getoptions().getScrattchDir().getAbsolutePath()
- 避免一连串的调用
- 数据结构避免使用访问器和改值器,以至于与对象混杂
- 隐藏结构:BufferedoutputStream bos = ctxt.createScratchinfileStream (classFileName)
- 数据传送对象(DTO):
- Active Record:是一种特殊的DTO形式。它们是拥有公共(或可豆式(bean)访问的)变量的数据结构,但通常也会拥有类似save和find这样的可测览方法
- 我们不幸经常发现开发者往这类数据结构中塞进业务规则方法,把这类数据结构当成对象来用。这是不智的行为,因为它导致了数据结构和对象的混杂体。
- 解决方法:把Active Record当做数据结构,并创建包含业务规则、隐藏内部数据(可能就是ActiveRecord的实体)的独立对象。
小结:
对象暴露行为,隐藏数据。便于添加新对象类型而无需修改既有行为,同时也难以在既有对象中添加新行为。数据结构暴露数据,没有明显的行为。便于向即有数据结构添加新行为,同时也难以向既有函数添加新数据结构。
在任何系统中,我们有时会希望能够灵活地添加新数据类型,所以更喜欢在这部分使用
对象。另外一些时候,我们希望能灵活地添加新行为,这时我们更喜欢使用数据类型和过程。优秀的软件开发者不带成见地了解这种情形,并依据手边工作的性质选择其中一种手段。
第七章 错误处理(2024.6.13)
- 使用异常而非返回码
- 先写Try-Catch-Finally语句
- 使用不可控异常:c#、c++、python、ruby不支持可控异常
- 可控异常的代价就是违反开放/闭合原则
- 如果你在编写一套关键代码库,则可控异常有时也会有用:你必须捕获异常。但对于一般的应用开发,其依赖成本要高于收益。
- 给出异常发生的环境说明
- 根据调用者需要定义异常类
- 定义常规流程
- 这种手法叫做特例模式(SPECIALCASE PATTERN, [Fowler])。创建一个类或配置一个对象,用来处理特例。你来处理特例,客户代码就不用应付异常行为了。异常行为被封装到特例对象中。
- 别返回null值:返回null值,基本上是在给自己增加工作量,也就是在给调用者添乱。只要有一处没检查null值,应用程序就会失控。
- 别传递null值
- 抛出异常
- 使用断言
- 禁止传入null
整洁代码是可读的,但也要强固。可读与强固并不冲突。如果将错误处理隔离看待,独立于主要逻辑之外,就能写出强固而整洁的代码。做到这一步,我们就能单独处理它,也极大地提升了代码的可维护性。
第八章 边界 (2024.6.17)
- 使用第三方代码
=>
- 边界上的接口(Map)是隐藏的。它能随来自应用程序其他部分的极小小的影响而变动。对泛型的使用不再是个大问题,因为转换和类型管理是在Senssors类内部处理的。
- 我们并不建议总是以这种方式封装Map的使用。我们建议不要将Map(或在边界上的其他接口)在系统中传递。
- 如果你使用类似Map这样的边界接口,就把它保留在类或近亲类中。避免从公共API中返回边界接口,或将边界接口作为参数传递给公共API。
- 总结:将边界代码封装起来,或者避免在公共API中传递。
- 浏览和学习边界
- 不要在生产代码中试验新东西,而是编写测试来遍览和理解第三方代码。 Jim Newkirk 把这叫做学习性测试(learning tests)。
- 在学习性测试中,我们如在应用中那样调用第三方代码。我们基本上是在通过核对试验来检测自己对那个 API 的理解程度。测试聚焦我们想从 API 得到的东西。
- 学习log4j:使用学习性测试方法,在应用程序中如何使用 log4j 包的步骤。
- 学习性测试的好处不只是免费
- 学习性测试是一种精确试验,帮助我们增进对API的理解。
- 学习性测试确保第三方程序包按照我们想要的方式工作。
- 不使用这些边界测试来减轻迁移的劳力,我们可能会超出应有时限,长久地绑在旧版本上面。
- 使用尚不存在的代码
- 还有另一种边界,那种将已知和未知分隔开的边界。在代码中总有许多地方是我们的知识未及之处。有时,边界那边就是未知的(至少目前未知)。有时,我们并不往边界那边看过去。
- 可以先定义自己使用的接口,接口中包括需要使用的方法,这是我们希望得到的接口。然后编写类实现接口,等到真实的 API 被定义出来,只需要修改实现类而已,并且当 API 发生变动时,类时唯一需要改动的地方。
-
整洁的边界
-
在使用我们控制不了的代码时,必须加倍小心保护投资,确保未来的修改不至于代价太大。
-
边界上的代码需要清晰的分割和定义了期望的测试。应该避免我们的代码过多地了屏第三
方代码中的特定信息。 -
我们通过代码中少数几处引用第三方边界接口的位置来管理第三方边界。可以像我们对
待Map那样包装它们,也可以使用ADAPTER模式将我们的接口转换为第三方提供的接口。
-
对于边界代码,尽量与主代码隔离开。使用单独的块封装,主代码尽量只与封装的块交互。