如何编写优雅的代码系列之——高质量的子程序

478 阅读7分钟

前言

你的同事:"这个代码的作者是谁?"

期望:"是我!" 你会自豪地回答,因为该代码像公主一样美丽。

现实:"不,不是我!" 你说谎是因为该代码像野兽一样丑陋。

现在,如果你想让期望成为现实,请继续阅读。

定义

​ 在讨论高质量的子程序的细节之前,先明确下面两个基本术语将会很有帮助。

首先,那什么是“子程序”?

子程序是为实现一个特定的目的而编写的一个可被调用的方法(method)或过程(procedure),例如 C++ 中函数(function),Java 中的方法(method),Javascript 的函数,或者 Microsoft Visual Basic 中的函数过程(function procedure)或者子过程(sub procedure)。

上面说了这么多晦涩难懂的词汇,那到底什么才是高质量的子程序呢? 别急,最简单的办法,让我们先来看看什么东西不是高质量的子程序,这里以 Javascript低质量的子程序,举 🌰 如下:

const profitList = [];
function handleStaff(
  inputRec,
  currentQtr,
  empRec,
  cropExpense,
  estimateRevenue,
  ytdRevenue,
  screenX,
  screenY,
  newColor,
  prevColor,
  status,
  expenseType
) {
  for (let i = 0; i < 100; i++) {
    inputRec.revenue[i] = 0;
    inputRec.expense[i] = cropExpense[currentQtr][i];
  }
  // global methods
  UpdateCorpDataBase(empRec);
  estimateRevenue = (ytdRevenue * 4.0) / currentQtr;
  newColor = prevColor;
  status = "SUCCESS";
  // global variable
  for (let i = 0; i < 12; i++) {
    if (expenseType === 1) {
      profitList[i] = revenue[i] - expense.type1[i];
    } else if (expenseType === 2) {
      profitList[i] = revenue[i] - expense.type2[i];
    } else if (expenseType === 3) {
      profitList[i] = revenue[i] - expense.type3[i];
    }
  }
}

这个子程序有哪些不好的地方呢?

  • 很差劲的名字 handleStaff, 一点也没有告诉我们这个子程序究竟是做什么的
  • 布局不好,看一下 expenseType === 2 这几处的代码风格
  • 输入变量 inputRec 的值被改变了,如果是一个输入变量(参数)就不应该被修改
  • 读写了全局变量profitList,应该更直接的与其他子程序通信,而不是去读写全局变量(副作用),或者可以读,但是不能去修改
  • 没有一个单一目的,初始化了一些变量,又像数据库写入数据,然后又进行了一些计算
  • 使用了大量的神秘数字(魔法数字):100、1、2、3
  • 入参太多了(12 个),合理的参数个数,其上限是 7 个
  • 存在未使用到的入参:screenX、screenY
  • 参数顺序混乱,且没有注释!

抛开计算机本身,子程序也算得上是计算机科学中一项最伟大的发明了,使得程序变得更加易读和理解

一、创建子程序的正当理由

降低复杂度

当内部循环或条件判断的嵌套层次很真实,就意味着需要从子程序中提取出新的子程序。把嵌套的部分提取出来,形成一个独立的子程序,可以降低外围子程序的复杂度。

引入中间、异动的抽象

把一段代码放入一个命名恰当的子程序内,是说明这段代码用意最好的方法之一。

举 🌰

// bad
let statusText = "";
if (status === 1) {
  statusText = "创建中";
} else if (status === 2) {
  statusText = "运行中";
} else if (status === 3) {
  statusText = "暂停中";
} else if (status === 4) {
  statusText = "回收中";
}

// good
const statusText = getStatusText(status);
function getStatusText(status) {
  const statusTextMap = new Map([
    [1, "创建中"],
    [2, "运行中"],
    [3, "暂停中"],
    [4, "回收中"],
  ]);
  return statusTextMap.get(status) || "";
}

避免代码重复

如果在两段子程序那边写相似的代码,就意味着需要代码分解,应该把两段程序中重复代码提取出来,然后将里面相同部分放到一个基类,或者说放到公共函数当中,然后再把两段程序中的差异代码放入一个派生类。

还有一种办法,就是说可以把相同的代码放入新的程序中,然后让其余的代码来调用这个子程序,与重复的代码相比,让相同的代码只出现一次,可以节约空间,代码改动起来也更加方便,因为你只需要在一处改动即可。

这么做的话,可以使改动更加可靠,因为可以避免需要做相同的修改时,却做了一些略有不同的修改。

隐藏顺序

假设你写了下面这样的代码

const stack = [1, 2, 3];
let index = 3;
// 先读取栈顶的数据
const current = stack.pop();
// 然后减1
index--;

应该把【先读取栈顶的数据】和【减 1】这两个逻辑对应的代码执行的顺序隐藏起来,这种信息隐藏起来,比在系统内到处散布要好很多。

提高可移植性

简化复杂的布尔判断

把布尔判断的逻辑放入单独的函数中,也强调了它的重要性,这样做也会激励人们在函数内部做出更多的努力,提高判断代码的可读性。

改善性能

把代码集中在一处之后,想用更高效的算法,或者说更快速高效的语言来重写,代码也是更容易做了。

确保所有子程序都很小

最好的代码行数是不超过 50 行

二、如何设计子程序?

正所谓程序设计的理念之一是 高内聚,低耦合

内聚性

  1. 功能的内聚性 这是最强也是最好的一种内聚性,也就是一个子程序仅执行一项操作

  2. 顺序上的内聚性

  3. 通信上的内聚性

  4. 临时的内聚性

指含有一些,因为需要同时执行才放到一起的操作的子程序,典型的例子就相当于我们在 Vue3setup 勾子中,初始化操作和获取数据函数 initHandler

  1. 过程上的内聚性

  2. 逻辑上的内聚性

  3. 巧合的内聚性

三、什么才是好的子程序名字?

描述子程序所做的所有事情

避免使用无意义的,模糊或者表述不清的动词

不要仅通过数字来形成不同的子程序名字

比如 outputUser1outputUser2

根据需要确定子程序名字的长度

变量名的最佳长度是9-20个字符,当然了,子程序的长短也要视该名字是否清晰易懂而定

给函数起名要使用动词+宾语的形式

可以准确的使用仗词

add/remove
get/set
old/new
open/close
show/hide
start/stop
create/destroy
insert/delete
...

举 🌰

// 不推荐
function usernameGet(username) {}
// 推荐
function getUsername(username) {}
// 不推荐
function usernameValidation(username) {}
// 推荐
function validateUsername(username) {}
// 对于 boolean 类型的命名需要以 is 作为开始。

let isValidName = validateName(‘amyjandrews’);

四、子程序可以写多长?

最佳的范围是是50~150行,IBM 曾经把子程序的长度限制在50 行之内

五、如何使用子程序参数

按照输入-修改-输出的顺序排列参数

先是输入数据,然后修改数据,最后输出结果

如果几个子程序使用了类似的一些参数,应该让这些参数的排列顺序保持一致

必须使用到所有的参数

不要把子程序的参数用作工作变量

把传入的子程序的参数用作工作变量是很危险的,应该在内部使用局部变量,拷贝传入的参数作为初始化值

把子程序的参数限制在7 个以内

如果参数实在是很多(至少 7 个),在 Javascript 中,可以考虑将参数放入到一个对象当中,然后在子程序中解构提取。

确保实参和形参相匹配

这点其实可以使用对象解构的方式来规避

六、鉴定需要提取成子程序吗?来看一份核对表

1、大局事项

  • 创建子程序的理由充分吗?
  • 一个子程序中所有适用于单独提取的部分是不是已经被提出到单独的子程序中了?
  • 函数的名字中是否用了动词加宾语的词语,是否描述了其返回值?
  • 子程序是否具有强烈的功能上的内聚性。也就是说,他是否只做了一件事并且把它做好?
  • 子程序之间是否有较松的耦合?子程序与其他程序之间的连接,是否是小的明确的可见的以及灵活的?
  • 子程序的长度是否由其功能和逻辑自然确定,而非遵循任何人为的编码标准。

2、参数传递

  • 子程序的参数长度是否没超过 7 个?
  • 子程序是否永达了每一个输入参数?
  • 子程序是否避免了把输入参数用作工作变量?
  • 如果子程序是一个函数,那么他是否在所有可能的情况下都能返回一个合法的值?

总结

  1. 创建子程序最主要的目的是提高程序的可管理性,当然也有一些其他的理由,其中节省代码空间只是一个次要原因,提高可读性可靠性和可修改性等原因都要更重要一些
  2. 有时候把一些简单的操作写成独立的子程序也是非常有价值的
  3. 子程序可以按照其内聚性分为很多类,而你应该让大多数子程序具有功能上的内聚性,这是一种最佳的内聚性。
  4. 子程序的名字是表示它质量的一个指示器,如果名字很糟糕,但恰如其分,那就说明这个子程序设计的很差劲,如果名字糟糕而且又不准,那么说明反应不出程序是干什么呢,不管怎么样,糟糕的名字就意味着程序是需要修改的。

最后

欢迎关注 james,欢迎关注 gayhub,不定期分享前端知识,程序员前端热点文章应有尽有,2021 陪你一起度过。