如何写出少bug的代码,你需要这4点建议

译者按: 作者给出 4 点建议,帮助开发团队写出少 bug 的代码。

本文采用意译,版权归原作者所有

小编推荐:Fundebug专注于 JavaScript、微信小程序、微信小游戏,Node.js 和 Java 线上 bug 实时监控。真的是一个很好用的 bug 监控服务,众多大佬公司都在使用。

我们曾经是  这张图右边的情况,现在已经向左侧移动。

几年前,我们在 VideoBlocks 的时候遇到过一个很大的关于代码质量的问题:像意大利面“spaghetti”一样混乱的逻辑,大量的重复代码,没有测试等等。开发一个新的功能、甚至修复一个小小的 bug 都会一肚子气,需要吃胃药来平复。每分钟都在  无数次谩骂”WTF“。

今天,我们整个 codebase 的代码质量有了明显的提升。这要归功于我们可以去花时间花力气来维护代码的质量。之前当我们遇到代码质量问题的时候,我们整个团队都认真阅读了 Robert Martin 的一本书Clean Code,然后尽力尝试用他建议的方式。我们甚至为整个开发团队树立了 clean code 的文化。如果你的公司已经开始规模化,我强烈建议你两项都做。在团队中实现”clean code“的实践,长期来看是可以双倍提高生产率的, 并且整个团队不再会满口脏话。

我从 Clean Code 和其它资源中梳理出来的几点建议:

  1. 如果没有经过测试,代码不合格
    请编写大量的测试,特别是单元测试,否则你会后悔的。
  2. 使用有意义的名字
    使用短小且精确的名字来命名变量、类和函数。
  3. 类和函数要足够小,遵循单  一职责原则(SRP)
    函数不要超过 4 行,类不要超过 100 行。对的,你没有看错。它们应该足够小,只做一件事情。
  4. 函数不应该有副作用
    什么叫副作用呢?修改传入参数的值。这个做法很危险。因此,你要确保没有这么做。

接下来我们一个一个详细过一遍。

1. 如果没有经过测试,代码不合格

在工程师们遇到 bug 并且它们本应该被测试发现的时候,我经常性地重复这句话。你需要反复去  灌输这个理念,在公司树立测试的文化。编写大量的测试,特别是单元测试。尽量去尝试各种可能的集成测试来确保在核心业务方面有足够的测试。

请反复灌输”如果没有经过测试,代码不合格”,直到他们完全实践。要将布道真正实践,不管他是一个新手还是有经验的老手。

2. 使用有意义的名字

在计算机科学中有两大难题:缓存验证和命名。

你也许之前听过这个说法。作为一名软件工程师,肯定感受颇深。如果你和你的团队不擅长合理地命名,最终代码会变得无法维护。工程师会无法继续忍受,甚至业务无法继续。

很认真地告诉你,朋友之间肯定不会随意用难听的名字来命名,就像 data,foobar,或则 myNumber。绝对不允许给类取SomethingManager这样的名字。除非有命名冲突,请确保你的命名简短又精确。在代码 review 的时候,跨文件查找名字可以很容易找到对应的函数。

3. 类和函数要足够小,遵循单  一职责原则(SRP)

小和 SRP 就像鸡和鸡蛋。我们先来解释为何要小。

对于函数来说,什么叫做小?不超过 4 行代码。是的, 你没看错,4 行。你也许认为是瞎扯淡,打算立马关掉该窗口。我建议你看完这段再做决定。你也许没想到或则意识到可以写这么小的函数。不过呢,4-行函数可以促使你努力去思考,如何选择子函数的名字来确保整个代码可读性强。另外,也就是说你不能使用嵌套 IF 语句。

我们来看一个例子。Node 有一个叫”build-url”的 npm 模块。该模块的用途已经在名字中充分表达。下面是代码:

function buildUrl(url, options) {
var queryString = [];
var key;
var builtUrl;

if (url === null) {
builtUrl = "";
} else if (typeof url === "object") {
builtUrl = "";
options = url;
} else {
builtUrl = url;
}

if (options) {
if (options.path) {
builtUrl += "/" + options.path;
}

if (options.queryParams) {
for (key in options.queryParams) {
if (options.queryParams.hasOwnProperty(key)) {
queryString.push(key + "=" + options.queryParams[key]);
}
}
builtUrl += "?" + queryString.join("&");
}

if (options.hash) {
builtUrl += "#" + options.hash;
}
}

return builtUrl;
}

该函数总共有 35 行。要理解这个函数并不是很难,不过如果使用我们的”small“法则,会更加易读。

function buildUrl(url, options) {
const baseUrl = _getBaseUrl(url);
const opts = _getOptions(url, options);

if (!opts) {
return baseUrl;
}

urlWithPath = _appendPath(baseUrl, opts.path);
urlWithPathAndQueryParams = _appendQueryParams(
urlWithPath,
opts.queryParams
);
urlWithPathQueryParamsAndHash = _appendHash(
urlWithPathAndQueryParams,
opts.hash
);

return urlWithPathQueryParamsAndHash;
}

function _getBaseUrl(url) {
if (url === null || typeof url === "object") {
return "";
}
return url;
}

function _getOptions(url, options) {
if (typeof url === "object") {
return url;
}
return options;
}

function _appendPath(baseUrl, path) {
if (!path) {
return baseUrl;
}
return (baseUrl += "/" + path);
}

function _appendQueryParams(urlWithPath, queryParams) {
if (!queryParams) {
return urlWithPath;
}

const keyValueStrings = Object.keys(queryParams).map(key => {
return `${key}=${queryParams[key]}`;
});
const joinedKeyValueStrings = keyValueStrings.join("&");

return `${urlWithPath}?${joinedKeyValueStrings}`;
}

function _appendHash(urlWithPathAndQueryParams, hash) {
if (!hash) {
return urlWithPathAndQueryParams;
}
return `${urlWithPathAndQueryParams}#${hash}`;
}

你会发现,我们并没有严格遵守 4 行函数的原则,我们确实构建的函数都相对比较小。每一个只负责一项工作,而且根据函数名就很容易理解其含义。你甚至可以对每一个函数进行独立的单元测试。按照原来的写法,你需要去测试那个拥有 35 行代码的大函数。你也许注意到了,通过这种方式写出来的代码量略大,55 行,超过 35 行。但是 55 行的代码更加具有可读性,更加容易维护。

那么如何写出这样的代码呢?从我个人经验的角度,最容易的方法是:首先把你要完成的任务用列表的形式记下来。那么每一个步骤都可能适合编写子函数。比如,我们可以将buildUrl函数分解为:

  1. 初始化 base url 和 options
  2. 添加 path,如果存在的话
  3. 添加 query 参数,如果有的话
  4. 添加 hash(#),如果需要的话

你就会发现,其实每一个步骤都可以直接转化为一个子函数。一旦你熟悉了这个套路,你就会按照自上而下的方式来拆分一个大的任务,你会创建一系列的操作步骤,然后递归地去拆分每一个子步骤。

接下来我们来说单一职责原则,什么意思呢?摘自维基百科:

单一职责原则是一个计算机编程原则,每一个模块或则类应该只负责软件的一个部分的功能,并且该功能可以完全有这个类来封装。所以相关的服务都可以通过它来输出。

Robert Martin 在 Clean Code 中提出了另一种定义:

一个类或则模块只应该因为一个原因而改变。(The SRP states that a class or module should one, and only one, reason to change.)

比如我们要打造一个系统来输出某种类型的报告并且展示出来。一个简单的方法是在一个模块里面存储报告数据,并且实现展示的逻辑。但是这个违反了单一职责原则因为我们有两个理由去改变它。首先,如果我们要更改报告某些域,我们需要更新代码;其次,如果我们要更改呈现的方式,我们也要更新代码。也就是说,我们应该把这两个功能分成两个模块来实现,比如”ReportData”和”ReportDataRender”。

4. 函数不应该有副作用

副作用是魔鬼,它会让你找不到 bug 在哪里。请仔细阅读下面的代码,并指出副作用是什么?

function getUserByEmailAndPassword(email, password) {
let user = UserService.getByEmailAndPassword(email, password);
if (user) {
LoginService.loginUser(user); // Log user in, add cookie (Side effect!!!!)
}
return user;
}

函数的命名直观易懂,通过邮箱和密码来查找用户,一个标准的网页应用的操作。不过,如果不阅读具体源代码。,它有一个隐藏的副作用你甚至不会注意到:登录用户!会创建一个登录 token,加到数据库中,并返回一个 cookie。

这样做是不对的,而且很多地方都不对。

首先,函数的接口不清晰。 函数名表达的是获取用户信息,但是函数实现却把登录操作给做了。即使可以在文档中描述这个副作用,但不是很好。工程师都习惯使用 IDE 集成的 intellisense 工具来自动补全函数,根本不会注意到文档中描述的副作用。

其次,因为有太多的依赖,函数测试也不好做。你需要 mocking HTTP 请求,并且处理登录 token。

再次,将查找和登录如此紧密的  耦合很难满足所有可能的需求状况。在未来的业务上如果有变动,那么可能需要将它们独立拆开。

总的来说,记住我上面提到的四大法则并使用起来吧。

  1. 如果没有经过测试,代码不合格
  2. 使用有意义的名字
  3. 类和函数要足够小,遵循单  一职责原则(SRP)
  4. 函数不应该有副作用

关于Fundebug

Fundebug专注于JavaScript、微信小程序、支付宝小程序线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了80亿+错误事件。欢迎大家免费试用

版权声明

转载时请注明作者 Fundebug以及本文地址:
https://blog.fundebug.com/2018/08/09/four-clean-code-tips/

您的用户遇到BUG了吗?

体验Demo 免费使用