01 | 缺乏业务含义的命名:如何精准命名?准命名?

不精确的命名

  • 命名过于宽泛,不能精准描述,这是很多代码在命名上存在的严重问题,也是代码难以理 解的根源所在。
  • 命名要能够描 述出这段代码在做的事情。
  • 一个好的名字应该描述意图,而非细节。

用技术术语命名

  • 编程有一个重要的原则是面向接口编程,因为接口是稳定的,而实现是易变的
  • 在一个技术类的项目中,这些技术 术语其实就是它的业务语言。但对于业务项目,这个说法就必须重新审视了。

用业务语言写代码

命名需要在业务层面上再进行讨论。例如审核人是reviewerUserId而不是userId

02 | 乱用英语:站在中国人的视角来看英文命名

违反语法规则的命名

常见的命名规则是:类名是一个名词,表示一个对象,而方法名则是一个动 词,或者是动宾短语,表示一个动作。

作为函数名,它应该是一个动词

不准确的英语词汇

在这种情况下,最好的解决方案还是建立起一个业务词汇表,千万不要臆想。

用集体智慧,而非个体智慧

英语单词的拼写错误

很多程序员对英语的感觉并没有那么强

像 IntelliJ IDEA 这样的 IDE 甚至可以给你提 示代码里有拼写错误(typo)

03 | 重复代码:简单需求到处修改,怎么办?

不要使用复制粘贴。真正应该做的是,先提取出函数,然后,在需要的地方调用这个函数。

重复的结构

①调用业务函数; ②如果出错,发通知 (可以抽象为方法,入参为Runnable)

一般来说,参数是名词,而函数调用,是动词。我们传统的程序设计教育中,对于名词是 极度重视的,但我们必须认识到一点,动词也扮演着重要的角色,尤其是在函数式编程兴起之后。

做真正的选择

只要你看到 if 语句出现,而且 if 和 else 的代码块长得又比较像,多半就是出现了 这个坏味道

写代码要想做到 DRYf(不要重复自己,简称 DRY),一个关键点是能够发现重复

04 | 长函数:为什么你总是不可避免地写出长函数?

对于函数长度容忍度高,这是导致长函数产生的关键点。

一个好的程序员面对代码库时要有不同尺度的观察能力,看设计时,要能够高屋建瓴,看 代码时,要能细致入微。

长函数的产生

  • 以性能为由:性能优化不应该是写代码的第一考量。

  • 平铺直叙:【两个典型问题:1、把多个业务处理流程放在一个函数里实现; 2、把不同层面的细节放到一个函数里实现。】

    • 关注点越多越好,粒度越小越好。
  • 一次加一点:任何代码都经不起这种无意识的累积,每个人都没做错,但最终的结果很糟糕

05 | 大类:如何避免写出难以理解的大类?

如果一个类里面的内容太多,它就会超过一个人的理解范畴,顾此失彼就在所难免了。

大类的产生

  • 职责不单一:关键就是能够把不同的职责拆分开来。
  • 字段未分组:所谓的将大类拆解成小类,本质上在做的工作是一个设计工作。

06 | 长参数列表:如何处理不同类型的长参数?

  • 聚沙成塔:是将这些参数封装成一个类

    • 一个模型的封装应该是以行为为基础的
  • 动静分离:长参数列表固然可以用一个类进行封装,但能够封装出这个类前提条件是:这些参数属于一个类,有相同的变化原因。

  • 告别标记

07 | 滥用控制语句:出现控制结构,多半是错误的提示

  • 嵌套的代码:可以把循环中的内容提取成一个函数,让这个函数只处理一个元 素
  • 以卫语句取代嵌套的条件表达式(Replace Nested Conditional with Guard Clauses)。
  • 不要使用 else 关键字
  • 重复的 Switch—>以多态取代条件表达式( 接口)

08 | 缺乏封装:如何应对火车代码和基本类型偏执问题?

火车残骸

  • 隐藏委托关系(Hide Delegate),说得更直白一些就是,把 这种调用封装起来
  • 要想摆脱初级程序员的水平,就要先从少暴露细节开始。
  • 迪米特法则:只与自己最直接的朋友交谈。

基本类型偏执

  • 这种采用基本类型的设计缺少了一个模型。

    • 例如价格大于0,double没有这种限制
  • 以对象取代基本类型(Replace Primitive with Object)

  • 封装之所以有难度,主要在于它是一个构建模型的过程

09 | 可变的数据:不要让你的代码“失控”

满天飞的 Setter

  • 相比于读数据,修改是一个更危险的操作。
  • 比可变的数据更可怕的是,不可控的变化,而暴露 setter 就是 这种不可控的变变化。把各种实现细节完全交给对这个类不了解的使用者去修改,没有人会知道他会怎么改,所以,这种修改完全是不可控的

可变的数据

  • 在函数式编程中,数据是建立在不改变的基础上 的,如果需要更新,就产生一份新的数据副本,而旧有的数据保持不变。

  • 解决可变数据,还有一个解决方案是编写不变类。具体如下

    • 所有的字段只在构造函数中初始化;
    • 所有的方法都是纯函数;
    • 如果需要有改变,返回一个新的对象,而不是修改已有字段。
  • 对象分成两种,实体和值对象。实体对象要限制数据变化,而 值对象就要设计成不变类

10 | 变量声明与赋值分离:普通的变量声明,怎么也有坏味道?

变量的初始化

  • 变量初始化最好一次性完成。
  • 这种代码真正的问题就是不清晰,变量初始化与业务处理混在在一起。
  • 在能够使用 final 的地方尽量使用 final
  • 变量声明可以通过try-with-resource的写法更简洁
    • try (InputStream is = new FileInputStream(…))

集合初始化

  • 从一个变量的声明到初始化成一个可用的状态—>【改进】ImmutableList.of( obj1, obj2 )
  • 用声明式的标准来看代码,是一个发现代码坏味道的重要参考。
  • 我们学习编程不仅仅是要学习实现功能,编程的风格也要与时俱进。

11 | 依赖混乱:你可能还没发现问题,代码就已经无法挽救了

如此一个简单的参数,放到哪个层里都有问题。

在 resource 中,我们将 NewBookRequest 这个请求类的对象转换成了 NewBookParameter 对象,然后传到 service 层。

业务代码里的具体实现

  • 业务代码中任何与业务无关的东西都是潜在的坏味道。
  • 识别一个东西是业务的一部分,还是一个可以替换的实现,我们不妨问问自己,如果不用它,是否还有其它的选择?

编程原则

  • 高层模块不应依赖于低层模块,二者应依赖于抽象。
  • 抽象不应依赖于细节,细节应依赖于抽象。

记住一句话:代码应该向着稳定的方向依赖。

12 | 不一致的代码:为什么你的代码总被吐槽难懂?

命名中的不一致

类似含义的代码应该有一致的名字

而一旦出现了不一致的名字,通常都表示不同的含义

方案中的不一致

例如获取时间的代码库不一致。

当一个操作有多个库可以完成时,可以约定类似的操作都以 Guava 为准。

代码中的不一致

能够分清楚代码处于不同的层次,基本功还是分离关注点

关于测试:不要测私有方法。

很多程序员纠结的技术问题,其实是一个软件设计问题,不要通过奇技淫巧去解决一个本来不应该被解决的问题。

13 | 落后的代码风格:使用“新”的语言特性和程序库升级你的代码

Optional

  • 所有可能为 null 的返回值,都要返回 Optional,以此减少犯错的几率。
    • Optional author = book.getAuthor();
    • String name = author.orElse(null);

函数式编程

  • 不是我们不需要遍历集合,而是我们有了更好的遍历集合的方式。
  • 循环语句是在描述实现细节,而列表转换的写法是在描述做什 么,二者的抽象层次不同。
  • 最好的 lambda 应该只有一行代码。
  • 列表转换的本身就完全变成了一个声明,这样的写法才是能发挥出列表转换价值的写 法。

记住一件事:不断学习“新”的代码风格,不断改善自 己的代码。

14 | 多久进行一次代码评审最合适?

代码评审是一个沟通反馈的过程

它的本质,就是沟通反馈的过程。有人给出代码实现的知识,有人 贡献出对技术的理解。

尽可能多暴露问题,尽可 能多做代码评审。

暴露问题

  • 实现方案的正确性;
  • 算法的正确性;
  • 代码的坏味道。

经常出现的问题:正常情况一切顺利,异常情况却考虑不足。

及时评审

  • 极限编程的理念,就是把好的实现推向极致,而代码评审的极致实践就是结对编程。

记住一句话:代码评审暴露的问题越多越好,频率越高 越好。

15 | 新需求破坏了代码,怎么办?

写代码时需要时时刻刻保持嗅觉。

一次驳回的实现

  • 我们必须对新增接口保持谨慎。

    • 接口,是系统暴露出的能力,一旦一个接口提供出去,你就不知道什么人会以什么样的方式使用这个接口。
    • 当我们想对外提供一个接口时,我们必须问一下,真的要提供一个新接口吗?
  • 有时后端不需改,只要把一些类似操作统一,让前端在某些场景下增加一个对原接口的调用

一次定时提交的实现

  • 对于一个业务系统而 言,实体是其中最核心的部分,对它的改动必须有谨慎的思考。

记住一句话谨慎地对待接口和实体的变动。

16 | 熊节:什么代码应该被重构?

可以学习《重构》这本书

什么代码应该被重构?

  • 对于“霰弹式修改”,解决的办法是使用“搬移函数”和“搬移字段”,把所有需要修 改的代码放进同一个模块;
  • 对于“发散式变化”,解决的办法是首先用“提炼函数”将不同用途的逻辑分开,然后用“搬移函数”将它们分别搬移到合适的模块;
  • 对于“过长的消息链”,你应该使用“隐藏委托关系”;对于“中间人”,对症的疗法法则是“移除中间人”,甚至直接“内联函数”。

培养对“坏味道”的判断力

  • “必须培养出自己的判断力,学会判断一个类内有多少实例变量算是太大、一个函数内有多少行代码才算太长”。
  • 缺乏在受控环境下的刻 意练习,很难通过工作中的自然积累提升判断力
  • “正确的代码构造”并非无穷无尽,实际上在单线程编程 中,几十个常见的模式已经几乎能够完全覆盖所有场景。
  • 从一开始就以合理的方式编程,从而使坏味道不要出现,我想这才是负责任的程序员应该 采取的工作方式。
  • 一位“知行合一”的程序员最终会发现,极限编程是唯一合理且有效的软件开发方法。

17 | 课前作业点评:发现“你”代码里的坏味道

  • 除了写程序库,日常开发尽可能不用 static 函数。
  • public UserAccounts(…, final UserContext context) { … this.context = context; }
  • 只有这样 一点一点调整,代码足够安全,每一步都是能够停下来的。
  • 关于README:好的程序员要学会表达,不仅仅会用代码表达,也要会用文字 表达。

記住一件事:尽量不使用 static。

关于写不写注释:先竭力把代码写到不需要用注释,而把注释当作最后的选择。