代码整洁之道 (Robert C. Martin)

在软件开发过程中,代码并不是写完提交之后就结束了。一个项目真正的生命周期,往往伴随着持续的修改、扩展和维护。需求会不断变化,新的功能会不断加入,原本简单的模块可能逐渐变得复杂。如果代码经过良好的设计和整理,随着时间推移,系统会慢慢演变成难以理解、难以修改,甚至无人敢触碰“遗留代码”。

正如软件工程领域常说的一句话:

代码的第一读者是人,第二读者才是机器。

计算机只关心代码是否能够正确运行,而开发人员需要面对代码背后的意图、结构以及未来变化的可能性。一段能够运行的代码,并不一定是一段优秀的代码。

由Robert C. Martin(人称Uncle Bob)编写的《代码整洁之道》正是围绕这一问题展开讨论。本书从变量命名、函数设计、类的职责划分,到错误处理、测试、架构设计等多个方面,系统总结了如何编写更加清晰、可维护的软件代码。

书中提出:

“Clean code always looks like it was written by someone who cares.”

优秀的代码不仅仅是满足需求,更体现了开发者对软件质量的关注。在实际开发中,我们经常会遇到这样的代码:

  • 一个几百行甚至上千行的函数
  • 一个承担几十种职责的巨大类
  • 大量重复逻辑散落在项目各处
  • 修改一个小需求,却需要影响多个模块
  • 新成员接手项目,需要花费大量时间理解代码

这些问题并不是某一天突然出现的,而是在一次次“先实现功能,以后再优化”的过程中逐渐累积形成的。《代码整洁之道》提供了一套思考方式:

  • 如何通过命名让代码表达真实意图
  • 如何设计短小、专注的函数
  • 如何让类保持单一职责
  • 如何降低模块之间的耦合
  • 如何通过测试保护代码质量
  • 如何让软件具备持续演进的能力

学习整洁代码,并不是为了追求代码形式上的“漂亮”,而是为了降低软件长期维护的成本,让系统能够面对未来的不确定变化。在本文中,我将结合书中的核心思想,并结合实际工程开发中的案例,对《代码整洁之道》进行深入学习和总结,希望能够探索:

什么样的代码才是真正适合长期维护的软件代码?

第1章 整洁代码

1.5 我们是作者

读和写花费时间的比例超过10:1。

要想干的快,要想早点做完,要想轻松些代码,先让代码易读吧。

1.6 童子军军规

“让营地比你来时干净。”

第2章 有意义的命名

2.2 名副其实

变量、函数或类的名称应该已经答复了所有的大问题。它该告诉你,它为什么存在,它做什么事,应该怎么用。如果名称需要注释来补充,那就不算是名副其实。看到名字,不看实现,也应该大致知道它的职责

2.3 避免误导

不要用那些会让人产生错误理解的名称。一个好的名字不仅要表达正确的信息,还不能暗示错误的信息。名字传递的信息必须和实际行为一致。

例如:

1
var users = new Dictionary<int, User>();

变量名user看起来像一个用户集合,但实际上是一个字典,更好的:

1
var usersById = new Dictionary<int, User>();

2.4 做有意义的区分

如果两个名字表示不同的东西,那么它们必须体现出真正的区别;不要为了“看起来不同”而添加没有意义的区分。也就是说:名字之间的差异必须传达信息,否则就是噪声。

很多代码为了避免重名,会随便加一些后缀:

1
2
3
Customer customer1;
Customer customer2;
Customer customer3;

这些名字虽然不同,但是没有任何信息,数字只是为了满足编译器。应该:

1
2
3
Customer billingCustomer; // 付款客户
Customer shippingCustomer; // 收货客户
Customer guestCustomer; // 访客客户

2.5 使用读的出来的名称

名字应该能够被正常读出来,这样人们才能在交流、讨论和思考代码时自然使用它。

2.6 使用可搜索的名称

单字母名称和数字常量有个问题,那就是很难在一大篇文字中找出来。

若变量或常量可能在代码中多处使用,则应赋其以便于搜索的名称。

2.7 避免使用编码

把类型信息或者作用域信息编码到名字中,只会额外增加理解这些名字的成本。

经过编码的名字通常难以发音,也容易输入错误。

匈牙利命名法、成员前缀(例如m_前缀标明成员变量)、接口和实现(例如以I开头标明是接口)不必盲目遵守。

注:这里的编码不是指字符编码(UTF-8、ASCII),而是指把额外的信息塞进名字里面的一种命名约定。例如:

1
2
3
string strName;
int iCount;
bool bEnabled;

这里str表示stringi表示intb表示bool,作者认为:

1
2
3
string name;
int count;
bool enabled;

更好!

2.8 避免思维映射

名称应当尽可能直接表达真实含义,而不是让读者在脑海中进行额外转换。

例如:

1
int d;

如果看到:

1
d = 5;

读者大脑可能需要寻找上下文d是表示days?还是distance

2.9 类名

类名和对象应该名词或名词短语,不应当是动词。例如CustomerWikiPageAccountAddressParser。避免使用ManagerProcessorDataInfo这样的类名。

2.10 方法名

方法名应当是动词或动词短语,如postPaymentdeletePagesave

2.11 别扮可爱

不要为了追求有趣、炫技或者个性化,而使用难以理解的命名。

代码不是展示幽默感的地方,命名应该清晰、准确、专业,而不是俏皮。

2.12 每个概念对应一个词

给每个抽象概念选一个词,并且一以贯之。对于那些会用到你代码的程序员,一以贯之的命名法简直是天降福音。

2.13 别用双关语

避免将同一个单词用于不同目的。同一术语用于不同概念,基本上就是双关语了。

代码作者应当尽力写出易于理解的代码。我们想把代码写得让别人能一目尽览,而不必殚精竭虑地研究。

1
2
3
4
public class List
{
    public boolean add(Object element);
}

这里add表示添加一个元素到集合。但是:

1
2
3
4
5
6
public class Product
{
    public void add(Product product)
    {
    }
}

如果这里add表示两个产品合并,那么就属于是“双关”了。使用combine就清晰多了。

2.14 使用解决方案领域名称

优先使用计算机科学、软件工程、算法、设计模式等领域中的专业术语来命名,而不是使用业务之外的模糊名称。简单来说,程序员应该用程序员能理解的词命名

2.15 使用源自所涉问题领域的名称

当没有合适的计算机科学术语可以表达时,应该使用业务领域中的专业术语来命名。

解决方案领域(Solution Domain)名称:软件工程中的术语。

问题领域(Problem Domain)名称:业务、行业中的术语。

2.16 添加有意义的语境

有时候,一个变量、函数或类的名字本身并不能表达完整含义,需要通过增加有意义的上下文来帮助读者理解。换句话说:不要让读者依靠猜测上下文来理解名字,而应该通过命名主动提供必要的信息。

例如:

1
id = 123;

单独看id没有意义,到底是用户ID,产品ID,还是订单ID?需要上下文!如果是userId = 123productOrderId = 123就更加明确。

2.17 不要添加没用的语境

只要短名称足够清楚,就要比长名称好,别给名称添加不必要的语境。

例如:userId已经足够明确,如果写customerUserIdentificationNumber就有点过度了。

第3章 函数

第4章 注释

第5章 格式

第6章 对象和数据结构

第7章 错误处理

第8章 边界

第9章 单元测试

第10章 类

第11章 系统

第12章 迭代

第13章 并发编程

第14章 逐步改进

第15章 JUnit内幕

第16章 重构SerialDate

第17章 味道与启发

附录A 并发编程II


相关内容

请作者喝杯咖啡!
AndyFree96 支付宝支付宝
AndyFree96 微信微信