- 有意义的命名
- 名副其实
- 体现本意的名称
- 用名副其实的函数代替魔术数
- 避免误导
- 避免错误使用专有名词
- 堤防使用不同处较小的名称
- 拼写一致
- 做有意义区分
- 数字系命名区分存在误导
- 废话区分(info、data)等,名称不同,意思无区别
- 使用读得出来的名称
- 使用可搜索的名称
- 名称长短应与其作用域大小相对应
- 避免使用编码
- 匈牙利语标记法 ( 名称 + 类型 )
- 成员前缀 ( m_前缀来标明成员变量)
- 接口和实现,推荐使用ShapeFactoryImp而非 IShapeFactory(存疑)
- 避免思维映射
- 明确是王道
- 类名
- 避免使用 Manager、Processor、Data 或 Info 这样的类名。
- 类名不应当是动词。
- 类名和对象名应该是名词或名词短语。
- 方法名
- 方法名应当是动词或动词短语
- 别扮可爱
- 避免使用俗话或哩语
- 每个概念对应一个词
- 给每个抽象概念选一个词,并且一以贯之
- 函数名称应当独一无二,而且要保持一致
- 别用双关语
- 避免将同一单词用于不同目的
- 使用解决方案领域名称
- 使用源自所涉问题领域的名称
- 添加有意义的语境
- 用有良好命名的类、函数或名称空间来放置名称,给读者提供语境
- 语境的增强也让算法能够通过分解为更小的函数而变得更为干净利落
- 不要添加没用的语境
- 只要短名称足够清楚,就要比长名称好
- 名副其实
- 函数
- 短小
- 函数20 行封顶最佳。
- if 语句、else 语句、while 语句等,其中的代码块应该只有一行
- 函数的缩进层级不该多于一层或两层
- 只做一件事
- 如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事
- 不能再拆出一个函数(该函数不是单纯地重新诠释其实现 )
- 在对函数测试的时候,函数应该剔除所有副作用。
- 函数要么“做什么事”,要么“回答什么事”。即是将指令和询问分隔开来。
- 每个函数一个抽象层级
- 要确保函数只做一件事,函数中的语句都要在同一抽象层级上
- 自顶向下读代码:向下规则,即每个函数后面都跟着下一抽象层级的函数
- 函数的抽象层级以功能来划分
- 短小
- 注释
- 注释规范
- 别给糟糕的代码加注释——重新写吧
- 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败
- 好注释
- 法律信息。在每个源文件开头,写上版权时间等法律信息。
- 提供信息的注释。例如解释某个抽象方法的返回值,规定参数的顺序和个数。
- 对意图的解释。使别人更清楚一段复杂代码是在干什么。
- 阐释把某些晦涩难懂的参数或返回值的意义翻译成某种可读形式,但是更好的办法是尽量让参数或返回值本身更清楚
- 警示。比如说有一个函数,测试它将会花上很多时间,我们将写上警示。
- 放大重要性。就是可以用来放大某处(看起来不合理)的重要性。
- 坏注释
- 注释掉的代码。注释后的代码会严重误导以后别人阅读这段代码,在早已有版本控制系统的时代,系统会将之前版本的代码记录,而无需我们用注释来标记,所以被注释掉的代码应该全部删掉。
- 为了解释复杂语句的注释。当一行语句很复杂的时候,我们通常会写注释。但实际上规范的写法是,一个语句只做一件简单的事,我们应该重构原来的复杂语句,改成几行简单语句,从而代替注释。
- 喃喃自语。任何注释如果要迫使读者查看其它模块的注释和代码,就是没能与读者沟通好,这样的代码不值得去写。就像喃喃自语一样,并没有它的真正。
- 多余的注释。当注释没有证明代码的意义,也没有给出代码的意图和逻辑,它并不比读代码更容易的时候,这种注释,多半是多余的。他不如代码精确,会误导读者接受不精确的信息,而不能正确的理解代码。
- 误导性注释。有的时候注释缺少一些信息,会误导程序员,使得其他人简单的调用某个函数,哦,缺少的信息,很可能导致程序员陷入调试的困境之中。
- todo 注释
- 不要把todo作为懒政的借口
- todo注释需要定期查看,解决并删除不需要的todo
- 注释规范
- 格式
- 格式的规范
- 好的代码格式,意味着代码的整洁和对细节的关注
- 如果是在团队中工作,则团队应该一致同意采用一套简单的格式规则,所有成员都要遵守,并且贯彻。使用能帮助你应用这些格式规则的自动化工具也很有帮助
- 格式的目的
- 格式关乎沟通,而沟通是专业开发者的头等大事
- 修改和维护代码才是开发者花时间花得最多的地方。只有拥有良好的代码格式,代码的可读性才会增加,这对日后修改和维护产生深远影响
- 垂直格式
- 像报纸学习。源文件要像报纸文章一样,名称如同标题一样,简单且一目了然。源文件最顶部应该给出高层次概念和算法,细节应该往下渐次展开
- 在不同的思路之间的代码以空白号为分隔。因为每个空白行都是一条线索,标识出新的独立概念,往下读代码时你的目光,总会停留在空白行之后那一行,而且这样是代码,思路更清晰更易懂
- 靠近的代码行则暗示了他们之间的紧密关系,紧密相关的代码应该相互靠近
- 垂直距离
- 变量声明尽可能靠近其使用位置
- 关系密切的概念,不要放到不同文件中
- 概念相关的代码应该放到一起,相关性越强,彼此之间的距离就该越短
- 循环中的控制变量(for(int i...)),应该总是在循环语句中声明
- 类的属性变量应该全部在类的顶部声明,而不是东一个西一个,使人很难找到
- 若某个函数调用另一个,就应当把他们放在一起
- 垂直顺序
- 最上面的代码应该是最抽象的,底部细节应该在下面实现。这样就能像报纸文章一样,最重要的概念在最前面,底部细节最后才会出来
- 横向格式
- 一行的上限是120个字符。短代码行,利于理解,所以应该尽力保持代码行短小(在30个字符以内)
- 水平方向上的区隔与靠近。在赋值操作符周围加上空格字符达到强调目的
- 水平对齐。不需要无意义的水平对齐,比如在那类里面声明属性的时候,那些无意义的水平对齐会在强调不重要的东西,会把读者的目光从真正的意义上拉开
- 团队规则
- 每个程序员都要自己喜欢的格式规则,但如果在一个团队中工作,就必须是团队说了算。
- 一组开发者应当认同一种格式风格,启动项目之前制定一套编码风格,所花时间很短,却能为为以后阅读他人代码、团队合作提供了巨大的便捷,并且整个软件系统是由一系列读起来的不错的,代码风格统一的代码文件组成。绝对不要用各种不同的风格来编写源代码,这样会增加其复杂度。
- 格式的规范
- 对象和数据结构
- 数据抽象
- 隐藏实现关乎抽象。类并不简单的用取值器和赋值器将其变量推向外检,而是暴露抽象接口,一遍用户无需了解数据的实现就能操作数据本体
- 数据、对象的反对称性
- 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数,面向对象代码便于在不该懂既有函数的前提下添加新类
- 反过来讲:过程式代码难以添加新的数据结构,因为必须修改所有函数,面向对象代码难以添加新的函数,应为必须修改所有类
- 德墨忒耳律
- 德墨忒耳律认为,模块不应该了解它所操作对象的内部情形
- 类C的方法f只应调用以下对象的方法:
- C;
- 由f创建的对象;
- 作为参数传递给f的对象;
- 由C的实体变量持有的对象;
- 方法不应调用由任何函数返回的对象的方法,换句话说,只和朋友说话,不和陌生人说话。以下就是违反该法则的一段代码:final String outputDir=ctxt.getOptions().getScratchDir().getAbsolutePath();
- 避免创造一半是对象一半是数据结构的混杂结构
- 对象的内部结构应该做到隐藏而不暴露
- 数据传送对象(Data Transfer Objects)
- 最为精简的数据结构,只有公共变量、没有函数的类。多用在与数据库通信,解析套接字信息之类的场景中。例如 JavaBean 结构,会有用复制器和取值器操作的私有变量。其实这些封装并无其他好处
- Active Record,特殊 DTO 形式。拥有公共变量的数据结构,通常也会拥有类似 save 和 find 这样可浏览方法。一般是对数据库表或者其他数据源的直接翻译。不要在这类数据结构里面塞进业务规则,应该创建包含业务规则隐藏内部数据的独立对象。
- 数据抽象
- 错误处理
- 错误处理规范
- 错误处理很重要,但是如果它搞乱了代码逻辑,就是错误的做法
- 使用异常而非你返回码
- 代码整洁,不容易被错误处理搞乱
- 先写 try catch finally 语句
- 可以帮你定义代码的用户应该期待什么,无论 try 代码块中执行的代码出什么错都一样
- 使用 try catch 结构定义一个范围,继续用测试驱动(TDD)的方法构建剩余的代码逻辑
- 使用未检异常
- 可控异常的代价就是违反开放/闭合原则。如果你的方案中抛出可控异常,旧的在 catch 语句和抛出异常处之间的每个方法签名中声明该异常
- 可控异常有时也会有用:你必须捕获异常。对于一般应用开发,其依赖成本要高于收益
- 给出异常发生的环境说明
- 异常提供足够的环境说明,一遍判断错误的来源和处所。堆栈踪迹以及充分的错误消息都应该记录下来
- 依调用者需要定义异常类
- 打包类翻译处理调用 API 并处理 API 所抛出的异常。降低对 API 的依赖
- 如果你想要捕获某个异常,并且放过其他异常,就使用不同的异常类
- 定义常规流程
- 使用特例模式(SPECIAL CASE PATTERN)。创建一个类或者配置一个对象,来处理特例。客户代码就不用应付异常行为了,异常行为被封装到特例对象中
- 别返回null,别传递null
- 错误处理规范
- 边界
- 使用第三方代码
- 学习性测试
- 找到最基础的文档(用来给第一次使用的人看的),开始阅读文档。每读完几个的api,便开始整合完成你想要的某一个功能,写一个类的一个函数将其封装起来
- 完成你初步罗列出来的功能便可以开始测试,如果不需要深入理解他人的代码的话,完成所需功能即可
- 测试完成后,便应该只用自己封装起来的函数来写自己旳程序
- 学习性测试的好处
- 减少了学习成本,减少了混乱的调试,比以前的方法更有效
- 可以测试出不兼容的更新
- 当我们需要的代码还未存在的时候,我们可以编写类似于学习测试的代码,将所需要的功能写出(adapter模式)
- 学习性测试
- 单元测试
- TDD三定律
- 在编写不能通过的单元测试前,不可编写生产代码
- 只可编写刚好无法通过的单元测试,不能编译也算不通过
- 只可编写刚好足以通过当前失败测试的生成代码
- 保持测试整洁
- 每个测试一个断言。每个测试中的断言,要尽可能少!不能把不同的测试放在一起
- 我们通过打造一套包装这些api的函数和工具代码,这样就可以更方便的编写测试,写出来的测试,也更便于阅读。我们通过测试那些函数和工具代码,从而测试那些api
- 函数和工具代码也以功能为构建目标,不同的功能用不同的函数
- 这种测试的函数和代码工具并非当初就设计出来,而是在对那些充满令人迷惑细节的测试代码进行后续重构时逐渐演进
- 测试好处
- 单元测试让你的代码可扩展,可维护,可复用。没有测试,每次修改都可能带来缺陷
- 整洁的测试
- 可读性,测试可以清晰拆分为3个环节,Build-Operate-Check
- 双重标准
- 每个测试一个断言
- 每个测试一个概念
- 整洁的测试规则
- 快速。测试不应该过于缓慢,如果测试过于缓慢,你就会不想频繁的测试,如果你不频繁运行测试,就不能尽早发现问题。代码将腐化
- 独立。测试之间应该相互独立,一个功能一个功能的测试,不会相互依赖
- 可重复。测试应当可在任何反应中重复通过
- 自足验证。测试的结果应该明显,最好是bool值,不应通过查看日志这种低效率的方法来判断测试是否通过。应当由程序来判断
- 及时。测试应该及时编写。单元测试,应该恰好在使其通过的生产代码之前编写
- TDD三定律
- 使用第三方代码
- 类
- 类的组织
- 类应该从一组变量列表开始。如果有公共静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。很少会有公共变量
- 公共函数应跟在变量列表之后,最后再是私有函数
- 类应该短小
- 类名应该精确。类的名称应该描述其权责
- 一个类应该只有一个权责
- 内聚。类应该只有少量实体变量。类中的每个方法都应该操作一个或多个这样的变量。高的内聚性,意味着类中的方法和变量互相依赖、互相结合成一个逻辑整体
- 有时候,随着对方法的扩充,实体变量的数量开始上升,往往这意味着至少有一个类要从大类里面挣扎出来。重构代码后,实体变量就分给几个不同的类了
- 为了修改而组织
- 我们知道编写一个类不是一触而就的,而是通过了无数次修进的。而系统的每处修改(添加功能,改变逻辑方法等)都让我们冒着系统会出现问题的风险。这时候我们要对类加以修进(组织和重构),以降低修改所面临的风险
- 当一个类庞杂巨大需要重构的时候,将一个类分隔为几个类,用明确的功能权责来划分
- 当有新特性要添加时,可以写一个新类,如果能达到新类只用了原有类的极少数(一个或两个)方法时,就是低耦合度。原有类没有被干扰,新类也相当简洁(只服务于某个新特性)
- 通过依赖倒置,降低连接度
- 类的组织
- 系统
- 将系统的构造和使用分开
- 分解main
- 将构造与使用分开的方法之一是将全部的构造过程搬迁到main模块中,设计系统的其余部分时,假设所有对象(运行程序前所需的)都已正确构造和设置。
- main函数创建系统所需的对象,再传给application。这时候,应用程序应对对象的构造过程一无所知
- 使用工厂方法自行决定何时创建实例,但是构造细节却在其他地方
- 使用依赖注入实现分离构造与使用
- 分解main
- 系统需要领域特定语言
- 在软件领域,领域特定语言(Domain-Specific Language,DSL)是一种单独的小型脚本语言或以标准语言写就的 API,领域专家可以用它编写读起来像是组织严谨的散文一般的代码。
- 优秀的 DSL 填平了领域概念和实现领域概念的代码之间的“壕沟”,就像敏捷实践优化了开发团队和甲方之间的沟通一样。如果你用与领域专家使用的同一种语言来实现领域逻辑,就会降低不正确地将领域翻译为实现的风险。
- DSL 在有效使用时能提升代码惯用法和设计模式之上的抽象层次。它允许开发者在恰当的抽象层级上直指代码的初衷。
- 领域特定语言允许所有抽象层级和应用程序中的所有领域,从高级策略到底层细节,使用 POJO 来表达。
- 将系统的构造和使用分开
- 迭代
- 运行所有的测试:为能方便测试,我们的生产代码也要足够短小,耦合度低
- 紧耦合的代码难以编写测试。同样,编写测试越多,就越会遵循 DIP 之类规则,使用依赖注入、接口和抽象等工具尽可能减少耦合。如此一来,设计就有长足进步。
- 遵循有关编写测试并持续运行测试的简单、明确的规则,系统就会更贴近 OO 低耦合度、高内聚度的目标。编写测试引致更好的设计。
- 重构:在写代码过程中要及时重构,保持代码的优雅
- 有了测试,就能保持代码和类的整洁,方法就是递增式地重构代码。添加了几行代码后,就要暂停,琢磨一下变化了的设计。设计退步了吗?如果是,就要清理它,并且运行测试,保证没有破坏任何东西。测试消除了对清理代码就会破坏代码的恐惧。
- 在重构过程中,可以应用有关优秀软件设计的一切知识。提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称,如此等等。这也是应用简单设计后三条规则的地方:消除重复,保证表达力,尽可能减少类和方法的数量。
- 不可重复:已有的代码要利用起来,消除重复
- 表达力强:这应该是一个目标或是结果,做好前边的工作自然而然可以达到
- 1. 软件项目的主要成本在于长期维护
- 2. 选用好名称
- 3. 保持函数和类尺寸短小
- 4. 采用标准命名法
- 5. 编写良好的单元测试
- 6. 最重要方式却是尝试
- 尽可能少的类和方法
- 即便是消除重复、代码表达力和 SRP 等最基础的概念也会被过度使用。为了保持类和函数短小,我们可能会造出太多的细小类和方法。
- 我们的目标是在保持函数和类短小的同时,保持整个系统短小精悍
- 运行所有的测试:为能方便测试,我们的生产代码也要足够短小,耦合度低
- 并发编程
- 并发目的
- 并发是一种解耦策略。它帮助我们把做什么(目的)和何时(时机)做分解开
- 并发防御原则
- 单一权责原则,分离线程相关代码和线程无关代码
- 限制数据作用域,数据严格封装;严格限制对可能被共享的数据的访问
- 使用数据副本,避免使用共享数据
- 并发模型
- 生产者消费者模型
- 读者作者模型,共享资源主要为读者提供信息源,偶尔被作者线程更新。
- 宴席哲学家。前两者多少带点协作性质,哲学家模型则纯粹是对资源的分时使用
- 警惕同步方法之间的依赖
- 基于客户端的锁定——客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码
- 基于服务端的锁定——在服务端内创建锁定服务端的方法,调用所有方法,然后解锁。让客户端代码调用新方法
- 适配服务端——创建执行锁定的中间层。这是一种基于服务端的锁定的例子,但不修改原始服务端代码
- 尽可能减少同步区域
- 尽早考虑线程关闭问题,尽早令其工作正常
- 测试线程代码
- 编写有潜力曝露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,跟踪错误。别因为后来测试通过了后来的运行就忽略失败
- 测试建议
- 将伪失败看作可能的线程问题
- 先使非线程代码可工作
- 编写可插拔的线程代码;
- 单线程与多个线程在执行时不同的情况
- 线程代码与实物或测试替身互动
- 用运行快速、缓慢和有变动的测试替身执行
- 将测试配置为能运行一定数量的迭代
- 编写可调整的线程代码
- 运行多于处理器数量的线程;任务交换越频繁,越有可能找到错过临界区或导致死锁的代码
- 在不同平台上运行;应该在所有可能部署的环境中运行测试
- 调整代码并强迫错误发生;硬编码
- 自动化;可以使用 Aspect-Oriented Framework、CGLIB 或 ASM 之类工具通过编程来装置代码
- 并发目的
Clean Code读书笔记
最新推荐文章于 2021-11-20 18:00:47 发布