程序员修炼第一课 | 如何通过改善代码风格来消灭隐藏bug

2,331 阅读9分钟

正如食物腐烂之前,可能会发出异味。当代码存在隐藏问题时,代码也会表现出一些异状,我们称之为代码异味(code smell),它存在于整体结构和代码设计阶段,暗示代码块或通用的编程模式中可能存在更深层次的问题。

代码异味通常被认为是暗示代码段需要重构的标志,但这并不是说代码有bug或者是无用的 。通常情况下,存在代码异味的代码块也能够运行的很好,但是一般难以维护和扩展,这就会导致一些技术问题,特别是在大型项目中,这种现象更加明显。

那么常见的代码异味有哪些呢?又要如何解决呢?下面我们将重点介绍10种最常见的代码异味以及如何对它们进行“除臭”。


1.紧耦合

存在的问题

紧耦合是指两个对象相互依赖于彼此的数据或函数,如果修改其中一个对象,那么另一个对象也需要修改。当两个对象过于紧耦合时,修改代码可能会是一场噩梦,同时更有可能在每次修改时引入bug。

例如:

在这种情况下,Worker类和Bike类紧密耦合。如果有一天你想开车去上班而不是骑自行车?你必须进入Worker类,将所有与Bike类相关的代码替换为与Car类相关的代码。这就会变得很混乱,很容易出错。

解决方法

你可以通过添加一个抽象层来降低耦合程度。在这种情况下,Worker类不仅可以骑自行车,还可以开车,还可以开卡车,甚至还可以骑摩托车。这些都属于交通工具,不是吗? 所以可以创建一个交通工具接口,根据你的需求,允许插入和修改不同类型的交通工具。

例如:

2.上帝对象

存在的问题

上帝对象是指包含太多变量和函数的大型类或模块。“知道得太多”和“做得太多”都会造成一些问题,原因有以下两点。首先,其他类或模块会变得过分依赖于数据(紧密耦合)。其次,由于所有代码都挤在同一个地方,使得整体结构杂乱无章。

解决方案

取一个上帝对象,然后根据它存在的问题来分离它的数据和函数,再将这些分组转换成对象。相较于上帝对象,分解为许多小对象可能会更好。

例如,假设你有一个巨大的User类:

您可以将其转换为以下内容的组合:

这样在下次需要修改登录过程时,就不必通过巨大的User类,而是通过易于管理的Credentials类!

3.长函数

存在的问题

顾名思义,长函数是指函数太长了。虽然没有一个特定的数字表示多少行代码对于一个函数来说“太长”,但当你看到这个函数时,你就会知道它是不是太长。这几乎是上帝对象问题的一个更严重的版本,一个长函数包含了太多的功能实现。

解决方案

长函数应该被分解成许多子函数,其中每个子函数被设计为处理单个任务或问题。理想情况下,原始的长函数将变成一个子函数调用列表,从而使代码更清晰,更易于阅读。

4.参数过多

存在的问题

函数或类的构造函数拥有太多的参数会造成一些问题,原因有以下两点。首先,这会使得代码不易阅读,测试也更加困难。其次,更重要的是,这意味着该函数的功能太模糊,承担着太多功能的实现。

解决方案

尽管“过多”对于参数列表而言是主观的,但我们建议对任何超过3个参数的函数保持关注并尽量避免。当然,有时候一个函数有5个甚至6个参数也是允许的,前提是有合理的理由。

大多数情况下,不存在一种方法能更好地将该函数分解为两个或更多不同的函数。与“长函数”不同的是,这个问题不能仅仅通过用子函数替换代码来解决 ,因为是函数本身需要分解为单独的子函数,而每个子函数都需要包含各自的功能。

5.命名模糊的标识符

存在的问题

一个或两个字母的变量名、无明显意义的函数名称、过分修饰的类名、使用变量类型标记的变量名称(例如,b_isCounted表示布尔变量),最糟糕的是,在一个代码中混合使用不同的命名规则,所有这些都将导致代码难以阅读,难以理解和难以维护。

解决方案

为变量,函数和类命名是一个难学的技能。如果你正在参与一个已有的项目,请仔细观察现有的标识符命名方式。如果存在命名风格指南,那么请记住它并时刻遵守它。如果是新项目,就可以考虑形成自己的命名风格并且坚持下去。

一般而言,变量名称应该简短但具有描述性。函数名通常应该至少有一个动词,并且函数名称应该表现出该函数的功能,但是不要使用太多的单词,类名也是如此。

6.幻数

存在的问题

当你正在浏览一些其他人写的代码,这时你发现了一些硬编码的数字。它们也许是if语句的一部分,或者是一些难以理解的计算的一部分,看起来没什么意义,而你需要修改该模块,但却无法理解这些数字的含义,这会使你非常苦恼。

解决方案

在编程时,应该不惜一切代价避免这些所谓的“幻数”。硬编码数字在写的时侯是有意义的,但是它们很快就会失去所有含义 ,特别是当其他人试图维护你的代码时。

其中一种解决方法是留下数字的注释,但更好的选择是将幻数转换为常量变量(用于计算)或枚举(用于if语句和switch语句)。通过给幻数起一个名字,代码可读性一目了然,同时也不太容易出现错误。

7.深度嵌套

存在的问题

有两种主要的语句可能造成深度嵌套代码:循环和条件语句。深度嵌套的代码并不总是很糟糕,但可能会产生问题,因为它很难理解(特别是变量没有被很好地命名的情况下),甚至更加难以修改。

解决方案

如果你发现自己正在编写一个双重,三重甚至四重for循环,那么代码将可能试图在超出自身的范围外查找数据。所以你应该提供一种方法,使之可以通过包含该数据的对象或模块函数调用来请求数据。

另一方面,深层嵌套的if语句通常表明你试图在单个函数或类中处理过多的逻辑代码块。事实上,深层嵌套和长函数往往是同时出现的。如果你的代码有大量的switch语句或嵌套的if-then-else语句,你可能需要实现一个状态机或策略模式。

8.未处理异常

存在的问题

异常的功能是非常强大的,但却容易被滥用。不正确地使用throw-catch语句可能会导致调试难度大幅增长。例如,忽略或掩盖捕获的异常。

解决方案

不要忽略或掩盖捕获的异常,而是要打印出异常的及其调用信息,这样调试人员才可以发现错误。如果你的程序悄无声息地运行失败,那么将来你可能就要头痛不已了!此外,我更倾向于输出特殊的异常信息而非所有异常。

9.重复的代码

存在的问题

你在程序多个无关部分执行相同的逻辑代码块,然后发现需要修改该逻辑代码块,但是却不记得所有执行该代码块的地方,假设最终你只修改了5个位置,而实际上有8个位置的代码块需要进行更改,这就会导致结果出现错误。

解决方案

解决重复代码问题的首要选择是转化为函数。假设你正在开发一个聊天应用程序,你是这样编写的:

在代码中的其他地方,你发现你需要执行一个相同的“这个用户在线吗?”检查。这时不要复制粘贴代码块,而是把它放到一个函数中:

这样在代码的任何地方,你都可以使用isUserOnline()函数进行检查。如果你需要修改此逻辑代码块,就只需要修改该方法,再将其应用于所有调用该方法的地方就可以了。

10.缺乏注释

存在的问题

代码在任何地方都没有注释。没有函数的功能注释,没有类的使用概述,没有对算法的解释等等。有人可能会说,写得好的代码不需要注释,但事实上,即使是写的最好的代码也不如注释更容易被理解。

解决方案

易于维护的代码块应该是代码写得足够好以至于不需要注释,但它仍然有注释。在写注释的时候,要记住你的目的是为解释代码块为什么存在,而不是解释代码块在做什么。注释能帮助你更好的理解自己和他人的代码,减少工作量,所以不要忽视他们。


如何编写风格良好的代码

显而易见,大多数不规范的代码都是由于对良好的编程原则和代码风格的忽视。假如你能够严格遵守DRY原则(Don't repeat yourself)就能消除大部分的重复代码,而掌握单一职责原则就可以避免创造巨大的上帝对象。

千万不要轻视这个问题,如果连你都无法一目了然地看懂自己的代码,更何况其他人呢?