CleanCode

CleanCode——代码整洁之道,从19年开始断断续续在阅读,最近终于看完了。最近写代码、重构其他人代码的过程中有些感触,一些手段与书中不谋而合,总结一下书中整洁代码的技巧。

在读类似的书或者文章的时候,强烈建议先去理解面向对象的概念和一些设计原则,大部分的准则都是围绕代码的可拓展、可维护性、以及低耦合、高内聚展开的,与面向对象的思想是一致的。做到了OO,绝大部分的代码都是整洁的代码,

CleanCode——整洁代码

什么是整洁的代码呢?它们都有一些共性,也是我们在写代码时需要遵行的守则。

优雅高效,逻辑直截了当,讲述事实,不引人猜测;减少依赖关系,便于维护;分层符合逻辑;减少重复代码,提高表达力,提早构建简单的抽象。

童子军军规:让营地比你来时更干净。写好代码的同时,必须保证代码的整洁。

Meaningful Names——有意义的命名

想让别人还有自己读懂代码,有意义的命名是前提。好的命名使阅读理解代码时事半功倍。

名副其实,名称阐述真实所做。做有意义的区分,使用读得出来的名称。

别害怕长名称,长而且具有描述性的名称,要比短而令人费解的名称好。

类名和对象名应该是名词或者是名词短语,避免使用Manager、Processor、Data、Info这样的类名。

方法名应该是动词或者动词短语。

使用领域名称,添加有意义的语境。

Functions——函数

函数应该尽可能短小,且函数应该做一件事,并把这件事做好,编写函数的目的是为了把大一些概念拆分微另一抽象层面上的一系列步骤,及分治、命令的思想,调用方只需要知道函数做了什么而不需要考虑函数的具体实现。

要确保函数只做一件事,函数中的语句都要在同一抽象层级上。

同时函数越短小、功能越集中,就越便于取个好名字。

函数参数越少越好。禁止向函数中传递布尔类型的参数。当参数需要两个、三个甚至三个以上,说明其中一些参数应该封装成类,且并不是简单的值对象,而是有自身概念行为的对象。

函数应该无副作用。

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

使用异常代替返回错误码,使用异常代替错误码,错误处理代码就能从主路径代码中分离出来。新异常可以从异常类中派生。

别重复自己DRY(Don’t Repeat Yourself),重复是邪恶的根源。

最终的函数不是一蹴而就的,一定是经历了分解、修改等一系列打磨而成的。

Comments——注释

注释并不能美化糟糕的代码,用代码来阐述你的意图,唯一真正好的注释是你想办法去不写的注释。

Formatting——格式

用空白行来标识出新的独立的概念。

变量声明尽可能靠近其使用的位置。

函数中的变量应该在函数的顶部出现。实体变量应该在类的顶部声明。

相关函数应该放到一起,调用者尽可能放在被调用者上面。

概念相关的代码应该放到一起。

Objects and Data Structures——对象和数据结构

对象把数据隐藏与抽象之后,暴露操作数据的方法。数据结构暴露其数据,没有提供有意义的函数。

过程式代码便于在不改动既有数据结构的前提下添加新的函数。面向对象的代码便于在不改动既有函数的前提下添加新类。过程式代码难以添加新数据结构,因为必须修改所有函数。面向对象代码难以添加新的函数,因为必须修改所有类。

The Law of Demeter:模块不应了解它所操作对象的内部情形。方法不应该调用任何函数返回的对象的方法。只跟朋友谈话,不与陌生人谈话。对象隐藏数据,暴露操作。

Error Handing——错误处理

错误处理不能搞乱代码逻辑。

可控异常:可控异常的代价是违反开闭原则,对较低层级代码的修改将波及高层级的签名。可控异常通常用于编写支撑形的代码库中。

不可控异常:对于一般的应用开发,使用不可控异常将重心放在主要逻辑上。

定义常规流程:使用特例模式,创建一个类或者配置一个对象,来处理特例,将异常行为封装到特例对象中。

别返回null,别传递null!

Boundaries——边界

依赖抽象,使用Adaptor封装第三方代码API,依靠能控制的东西,好过依靠你控制不了的东西,免得日后受它控制。

UnitTest——单元测试

保持测试的整洁,单元测试让你的代码可扩展、可维护、可复用。保证测试的可读性。

单个测试中断言的数量应该最小化。

每个测试一个概念。

FIRST:Fast、Independent、Repeatable、Self-Validating、Timely。测试应该够快、应该相互独立、应可在任何环境中重复通过、应有布尔值的输出、及时编写。

Classes——类

类的组织:公共静态变量->私用静态变量->私有实体变量->公共函数 私有函数跟在第一次调用它的函数后面。

类应该短小,一个类只有一个职责,如果无法为一个类命名,大概这个类就太长了。

类的设计遵循单一职责原则(SRP),类或者模块有且只有一条加以修改的理由。其中蕴含分治的思想。

每一个到达一定规模的系统都会包括大量的逻辑和复杂性,系统应该由许多短小的类而不是少量巨大的类组成。

内聚:类应该只有少量的变量,类中的每个方法都应该操作一个或者多个这种变量。类的内聚性高,意味着类中的变量和函数相互依赖,互相组合成一个逻辑整体。

Systems——系统

软件系统应该将启动过程和启始过程之后的运行时逻辑分离开。

领域特定语言(Domain-Specific Language,DSL),一种单独的小型脚本语言或者标准语言写就的API。

优秀的DSL填平了领域概念和实现领域概念的代码之间的“壕沟”。DSL在有效使用时能提升代码惯用法和设计模式之上的抽象层次。它允许开发者在恰当的抽象层级上直指代码的初衷。

Emergence——迭进

运行所有的测试:测试驱使系统更贴近OO低耦合度、高内聚度的目标。测试消除类对清理代码就会破坏代码的恐惧。

重构:提升内聚性,降低耦合度,切分关注面,模块化系统关注面,缩小函数和类的尺寸,选用更好的名称。

消除重复、保证表达力、尽可能减少类和方法的数量。

Concurrency——并发编程

并发是一种解耦策略,把做什么(目的)和何时(时机)分解开。

解耦目的和时机能明显改进应用程序的吞吐量和结构。

并发防御原则:

  • 单一职责原则:分离并发相关代码与其他代码
  • 限制数据作用域:谨记数据封装,严格限制对可能被共享的数据的访问
  • 使用数据副本:使用对象副本避免代码同步执行,避免锁定
  • 线程应尽可能独立:尝试将数据分解到可被独立线程操作的独立子集

了解执行模型:

  • 生产者——消费者模型
  • 读者——作者模型
  • 宴席哲学家

Successive Refinement——逐步改进

创建何时的空间放置不同种类的代码。对关注面的分隔让代码更易于理解和维护。

保持代码持续整洁和简单,用不让腐坏有机会开始。

JUnit Internals——JUnit内幕

重构是一种不停试错的迭代过程,不可避免地集中于我们认为是专业人员该做的事。

Smells and Heuristcs——味道与启发

注释

  • 不恰当的信息:注释只应该描述有关代码和设计的技术性信息。
  • 废弃的注释:过时、无关、不正确的注释就是废弃注释,应当删除。
  • 冗余的注释:注释描述的是充分自我描述的东西,那么注释就是冗余的,应该删除。
  • 糟糕的注释:花时间写出最好的注释。
  • 注释掉的代码:注释掉的代码纯属厌物,删除注释掉的代码。

环境

  • 需要多步才能实现的创建:构建系统应该是单步的小操作。
  • 需要多步才能做到的测试:发出单个指令就可以运行全部单元测试,是基础、重要的,应该快速、轻易和直截了当的做到。

函数

  • 过多的参数:函数的参数应该少。三个以上的参数应该坚决避免。
  • 输出参数:输出参数违反直觉,如果函数要改变什么东西的状态那么就改便其所在对象的状态。
  • 标识参数:不要使用布尔值作为入参
  • 死函数:用不被调用的方法应该丢弃。

一般性问题

  • 一个源文件中存在多种语言:理想的源文件包括且只包括一种语言。
  • 明显的行为未被实现:函数或者类应该实现其他程序员有理由期待的行为。
  • 不正确的边界行为:别依赖直觉,探索每种边界条件,并编写测试。
  • 忽视安全:不要忽视安全,解决导致测试失败的问题
  • 重复:重复的代码意味着遗漏了抽象。重复的形态最明显的是看到明显一样的代码,隐蔽的形态则是在不同模块中不断重复出现,更隐蔽的形态是采用类似算法但具体代码行不同的模块。尽可能找到并消除重复。
  • 在错误的抽象层级上的代码:创建分离较高层级一般性概念与较低层级细节概念的抽象模型。所有较低层级概念放在派生类中,所有较高层级的概念放在基类中。
  • 基类依赖于派生类:基类对于派生类应该一无所知。
  • 信息过多:限制类或者模块中暴露的接口数量,类中的方法越少越好,函数知道的变量越少越好,类拥有的实体变量越少越好。
  • 死代码:死代码就是不执行的代码。从系统中删除死代码。
  • 垂直分隔:变量和函数应该在靠近被使用的地方定义。
  • 前后不一致:前后一致,坚决贯彻,能让代码更加阅读和修改。
  • 混淆视听:保持源文件整洁,良好的组织,不被搞乱。
  • 人为耦合:不相互依赖的东西不该耦合。花点时间研究在什么地方声明函数、变量和常量。
  • 特性依赖:类的方法只应对其所属类中的变量和函数感兴趣,不应该垂青其他类中的变量和函数。
  • 选择算子参数:使用多个函数,优于向单个函数传递某些代码来选择函数行为。
  • 晦涩的意图:代码要尽可能具有表达力。
  • 位置错误的权责:代码应该放在读者自然而然期待它所在的地方。
  • 不恰当的静态方法:通常倾向于选择非静态函数,如果的确需要静态函数,确保没机会打算让它有多态行为。
  • 使用解释性变量:把计算过程打散成一系列良好命名的中间值,不透明的模块就会变得透明。
  • 函数名称应该表达其行为:从函数名中要能知道它是做什么的。
  • 理解算法:你必须知道解决方案是正确的,得到某种整洁而足具表达力,清楚呈示如何工作的东西。
  • 把逻辑依赖改为物理依赖:如果某个模块依赖于另一模块,依赖就应该是物理上的而不是逻辑上的。
  • 用多态代替If/Else或Switch/Case:在使用swich之前,先考虑使用多态。对于给定的选择类型,不应有多于一个swich语句,在那个swich语句中的多个case,必须创建多态对象,取代系统中的其他类似swich语句。
  • 遵循标准约定:每个团队都应该遵循基于通用行业规范的一套编码标准。
  • 用命名常量代替魔术数:魔术数泛指任何不能自我描述的符号。
  • 准确:在代码中做决定时,确认自己足够准确。
  • 结构甚于约定:坚守结构甚于约定的设计策略。
  • 封装条件:把解释了条件意图的函数抽离出来。
  • 避免否定性条件:尽可能将条件表示为肯定形式。
  • 函数只该做一件事
  • 掩蔽时序耦合:创建顺序队列暴露时序耦合。
  • 别随意:构建代码需要理由,且理由应与代码结构相契合。
  • 封装边界条件:边界条件难以追踪,把处理边界条件的代码集中到一处,不要散落于代码中。
  • 函数应该只在一个抽象层级上:函数中的语句都应该在同一抽象层级中,该层级应该是函数名所示操作的下一层。
  • 在较高层级放置可配置数据:位于较高层级的配置性常量易于修改。它们向下贯穿应用程序。应用程序的较低层级并不拥有这些常量的值。
  • 避免传递浏览:让直接协作者提供所需的全部服务。

Java

  • 通过使用通配符避免过长的倒入清单
  • 不要继承常量
  • 用枚举代替静态常量

名称

  • 采用描述性名称:确认名称具有描述性。
  • 名称应与抽象层级相符:不要取沟通实现的名称,取反应类或者函数抽象层级的名称。这是一个随着对抽象理解而不断演进的过程。
  • 尽可能使用标准命名法:基于约定、用法的名称易于理解。
  • 无歧义的名称:选用不会混淆函数或者变量意义的名称。
  • 为较大作用范围选用较长名称:名称的作用范围越大,名称就应该越长、越准确。
  • 避免编码:不应该在名称中包括类型或者作用范围信息。
  • 名称应该说明副作用:名称应该说明函数、变量或者类的一切信息。不要用名称掩蔽副作用。

测试

  • 测试不足:一套测试应该测到所有可能失败的东西。
  • 使用覆盖率工具:覆盖率工具能汇报你测试策略中的缺口。
  • 别略过小测试:小测试易于编写,其文档上的价值高于编写成本。
  • 被忽略的测试就是对不正确事物的疑问
  • 测试边界条件:特别注意测试边界条件。
  • 全面测试相近的缺陷:缺陷倾向于扎堆,全面测试出现缺陷的函数。
  • 测试失败的模式有启发性:通过找到测试用例失败的模式来诊断新问题所在。
  • 测试覆盖率的模式有启发性:查看被或者未被已通过的测试执行的代码,往往能发现失败的测试为何失败的线索。
  • 测试应该快速:竭尽所能让测试够快。
Author: nopainanymore
Link: http://nopainanymore.me/CleanCode/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
wechat