返回
Featured image of post 代码整洁之道

代码整洁之道

外国技术哥的代码哲学

代码整洁之道(Clean Code)

视频来源:第五场_哔哩哔哩_bilibili

The only way to go fast is to go well. 走得快的唯一方法是走得好。

函数法则

  • 函数应该是小的,或者更小。
  • 你可能永远不需要向函数传入三个参数——因为那三个参数往往可以封装成一个类(结构体)。
  • 大部分情况下你不应该传入一个 bool 参数,因为函数内部就会出现一个 if。为什么不把它拆分成两个函数呢?
  • 最小惊讶定理(Principle of Least Surprise):把函数写清晰,不要给使用者带来惊喜。

避免使用 switch

switch 做了不止一件事。当你已经处理了三种类型,需要增加第四种时,就必须在所有用到该 switch 的地方都加上第四个 case。可以用策略模式代替。

这不只是性能问题——采用正确做法后,你想加什么就加什么,旧代码永远不受影响。

switch 还会导致脚本不得不导入大量脚本,因为它的 case 需要这些依赖;一旦出现改动,所有脚本都需要重新编译,所有内容都必须重新发布。

当你把模块分好时,DLL 的优势才能体现出来。

副作用

标准定义:对系统状态的改变。如果你调用一个函数,而该函数导致系统状态发生了改变,那么就存在副作用。

例如 “打开” 函数有一个副作用:它产生了一个打开的文件;new 函数有一个副作用:它留下了一块分配的内存。

副作用是成对出现的:new 和 delete、open 和 close。那么,我们有什么能力去管理 alloc 和 free 呢?

GC 让我们不再需要过分在意 “结束”,它确实好用,能让人更容易写出安全的代码。但它会让你不再以合理的方式编写代码——你只是把管理的责任丢给了 GC 系统,而 GC 并不能完美解决所有问题。这种编码方式让你没有养成 “有开就有关” 的好习惯。

成对的 “开” 和 “关” 必须按正确顺序调用——你不能还没打开就关闭。那我们应该如何管理这种顺序呢?

Lambda 就是一个 Class,它有一个 execute 函数,你可以自己写。视频以 Java 为例,不确定 C# 是否也是如此。

命令与查询分离(Command Query Separation)

一个返回 void 的函数,一定有副作用;所以一个有返回值的函数一定不能有副作用。这就是 “Command and Query Separation” 原则。

因为返回 void 的函数改变了系统状态,任何有返回值的函数都不该改变系统状态。如果你遵循这个原则,那么任何有返回值的函数都可以放心使用,相信它不会留下诸如未关闭的 FileStream 之类的隐患。

返回异常而不是错误码

当我写一个 try 块的时候,整个函数中唯一包含尝试内容的应该是 try 块内部。如果有一个需要抛出的异常,那么整个函数的第一个可执行语句应该是 try,而这个 try 也只应该包裹一个函数——就是需要抛出异常的那个函数。

这个函数内部除了 try-catch-finally 之外不要再添加任何其他函数,也不要在同一个函数里写两个 try。

DRY 原则:Don’t Repeat Yourself

显而易见,不要重复自己。

结构化编程

别用 Goto。

我们并不确保程序是正确的,我们做一大堆测试,只是为了证明我们写的不是错的。

让代码随时可以上线

在任何时候,你的代码都应该是可以上线的。

好的期望是:你的代码在每个"迭代周期"结束后,都随时处于可发布状态。就像系统目前"能登出但不能登录"也没关系,只要两周后"登出"这个功能是稳定可行的就行。

什么是发布状态?就是所有的测试都已完成、所有细节都已完成的时候。

每隔这个周期你都处于可发布状态——即使功能并不完善,但已经完成的功能都是可发布的。

不要因为觉得"功能看起来没问题、应该稳定"就凭猜测发布代码,这是不负责任的。

最好保持稳定的生产力,而不是随着项目变大,新增功能的耗时越来越长、长到无法估计;也不要因为插件老旧、框架老旧,而在后期维护上越来越耗时。

向系统添加新功能,不应该导致你的速度变慢。

低成本适应(Inexpensive Adaptability)

也就是修改系统非常容易。

更准确地说:修改功能的成本应该与修改功能的大小成正比。改一个小功能就耗费巨大是不正常的,大功能需要更长时间倒是很正常。

怎么做到?

  1. 保持代码干净、单一职责。
  2. 有一套非常好的测试。

持续改进(Continuous Improvement)

随着时间推移,代码应该越来越好。我们应该期望项目有一群 Champion(守护者)在不断地改进代码。

无所畏惧的能力(Fearless Competence)

不要害怕去修改烂代码,不要害怕"因为我尝试修复,所以它爆炸了,就是我的错"。

如果系统必定会烂,那是因为你一定会去改变它,但用的是以最小个人损失为目的的方法,而不是对系统友好的方法——那这永远是错的。

怎么做到不惧怕?需要一套完整的测试。如果测试能告诉你哪些是错的、哪些是好的,就能让你不怕破坏系统。

给自己写测试,让自己对自己写的功能有绝对控制权,让自己了解、掌握自己的代码。

但是,破碎地散落在世界各地的单元测试绝对是没有用的。根源在于设计了脆弱的单元测试——一旦修改了系统的一小块,整个单元测试就全炸了。归根结底是因为没有意识到:测试也是系统的一部分,必须作为系统的一部分来设计。

怎么解决?当你设计测试时,将测试与系统隔离、解耦,构建 API 供测试使用,并使用与正常系统同样的技术来进行测试。

代码覆盖率

代码覆盖率在技术团队之外没有任何意义。

QA 的角色

QA 应该什么都找不到。QA 的功能不应该是流程的后端,而是规定系统应该如何表现。

通常而言,程序甚至没有给 QA 足够的时间去测试。QA 应该是流程的开端。

我们期望测试是自动化的——手动测试的成本其实远高于自动化测试。QA 没有足够的时间去测试,还得分辨哪些测试需要测、需要知道测什么才够;由于没有时间,所以也只能猜测。

团队协作

当团队有人倒下时,应该相互支持。你的责任是确保当你倒下的时候,有人能撑住你的位置——也就是说,你要交代好你的东西。

应该相互传递信息、相互审阅代码,不要造成信息孤岛。这比代码审查要好得多。

诚实的估算

如果有人问你一个功能能不能在周二完成,你说可以,你就是在说谎——除非你有绝对把握。而管理员需要这个信息来做管理。

“不知道"或许是一个好答案。可以划出一个大概范围,有一个叫做 PERT 项目评估 的方法可以参考:给出三个数字——顺利情况下的估计、一般情况的估计、最差情况的估计。

当管理员问的时候,你只能这样回答,因为这是你的责任,你无法完全把握。

编程中不变的事情

序列、选择、迭代。

各种各样的编程语言,本质上依然是这样。

你需要懂得说"不”

一个很犀利的观点:你被雇佣不是因为你会写代码,而是因为你知道——知道什么时候答案是 No。

你可以通过说不来防止公司走下坡路,你必须会说不。当有事情无法完成时,你必须会说不。因为程序员不会跟人"沟通对抗",所以这对程序员来说并不容易。

当然,作为新人到处说不也不行;而是当你作为管理层的时候,当真的需要说不的时候,你要能说不,无论压力多大。你需要更好地处理,而不是单纯说"都是他们搞的",这是消极的处理方式。

比如某人突然说"我立刻就要做到这个功能",因为有某些必须的原因,但你也确实没有在这么短时间内完成它的方法,那么你必须能说"不"。

但有个小地方可能被经理用来攻击你:“你要不先试试。"——这句话其实在心理上谴责你"你怎么试都没试过就说不行”。你必须小心,不能说"那我试试"。

因为实际情况是无法做到。你必须说:“我们已经尽力了,没有什么需要额外尽力的了,我们的工作就是尽力。“如果你说"试试”,其实你已经知道就是做不了,没有试的必要,你不会改变你的行为。但你也不能说你不试——这是一个语言陷阱。

测试驱动开发(TDD)

医院的医生是怎么消毒的?是有步骤的,确保一定消毒完全。

三条规则:

  1. 在你写完一个失败的测试之前,不允许写任何上线代码。因为没有代码被验证过可以上线。
  2. 你必须写能通过编译的代码,如果不能编译,你就得停止。
  3. 当你的代码刚好能通过刚刚失败的测试时,你不允许再写任何生产代码。

你写出来的这些代码,别人将可以通过测试来了解你是如何执行的。为什么写测试代码不好玩?因为你已经知道代码是怎样运行的了。

但是你会遇到难以测试的代码,因为它写的时候并没有考虑这一点。当它"可行"的时候,你知道里面有漏洞,但是你没有自信。测试模块应当看作一个降落伞。

如果你先写测试用例,你就不会写出无法测试的代码。

虽然写测试案例很痛苦、很无趣,但出来的结果,你是信任的、有自信的。

为什么 GUI 类型的代码难测?因为你难以知道正确答案。

测试驱动方法其实跟会计的复式记账法类似:代码、测试,两边都合格,下一个。会计就是资产记账、负债记账,两边加一起等于 0 则正确,下一步。

继承是最紧密的耦合,所以得尽可能少继承。所有子类父类都要测,因为每个函数都是可以测的。

上面那三条规则可以这样体现:你必须先为不存在的代码写测试。比如你测试的时候会新建一个 DoSomething 的类,但是这个类不存在,所以你会创建这个类,代码就可以编译了。

有个规则:不能让上线代码比测试代码更加具体。

你每增加一个测试,应该让你的测试越来越具体、越来越难通过;而你写上线代码,所做的一切都应该让代码更加通用。

写代码,别直接跑向目标。

如果你决定要用测试驱动,你大概会踩坑——因为这是需要好几个月去学习的技能,才能让你安全地用在工作中。先学好,再去工作上使用。

作者认为只有探索性测试值得人力去做,因为这是你在做好一个系统之后,让测试人员去打破它。而打破系统最好的测试人员是"客户”。

架构

如果你想写代码的时候做得合理,就必须遵循架构的原则。

因为架构不是完全纯思想层面的那种高层次东西,架构会渗透进你的每行代码里。如果你代码都写不好,你就没办法建立架构。

在之前的会议上,我们都认为不需要在前期投入大量时间来设计软件的每一小部分,但同时我们也认为架构不是凭空出现的。

最终我们得到了答案:任何规模的系统,都需要至少先做好架构之后才能成型。

不是敏捷开发没有框架,敏捷开发就是我们能设计架构的框架之一。

“当你最初决定系统应该是什么形状的时候,系统之后就该是那个形状?"——这是错的。

因为当你开始写代码、开始做模块的时候,你会发现你所做的开始影响架构、改变了它的形状。每当有人新增功能或行为、或者改变功能,都会给架构带来压力,架构必须根据当时的情况进行改变。架构是"活"的,跟着系统每天都在变,我们需要在整个软件生命周期内一直维护它。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
由 Hugo 构建
主题 Stack 由 Jimmy 设计
© Licensed Under CC BY-NC-SA 4.0