别纠结,提高代码整洁度也没那么难!

我的梦境 提交于 2020-08-04 22:25:59

作者 | Jonathan Fulton

译者 | 弯月,责编 | 屠敏

头图 | CSDN 下载自东方 IC

出品 | CSDN(ID:CSDNnews)

以下为译文:

几年前,我们曾遇到过重大的代码质量问题:大多数文件中的逻辑纠缠夹杂、大量重复、没有测试。无论是编写新功能还是修复很小的bug都需要付出呕心沥血的代价,常常气到你吐血。令我们苦不堪言。

如今,我们的代码库的整体质量明显提高了,这在很大程度上要归功于我们为提高代码质量而做出的不懈努力。几年前,在发现代码质量问题后,我们整个团队一起阅读了Robert Martin的《代码整洁之道》,然后竭尽全力贯彻了他的建议,甚至引入了“清洁规范”作为工程团队的核心文化。如果你打算扩张团队,那么我强烈建议你现在就开始实施这两项措施。从长远来看,恰当地实施“干净的代码”实践可以提高一倍生产力,并显著提高工程团队的士气。有了选择,谁还会愿意进入上图右边那个Bad code的房间呢?

在我们实施的“清洁规范”以及其他想法之中,有四项措施将团队的生产力和幸福指数提高了80%。

  1. 没有经过测试的代码一概不安全。

    你需要编写大量测试,尤其是单元测试,否则你会追悔莫及。

  2. 选择有意义的名称。

    为变量、类和函数选择言简意赅的名称。

  3. 类与函数保持最小,遵守单一功能原则

    函数不应超过4行,而类不应超过100行。是的,你没看错。而且它们应该只做一件事。

  4. 函数不能有副作用

    副作用(例如,修改输入参数)是有害的。请确保你的代码中没有副作用。尽可能在函数声明中明确规定这一点(例如,传入基本类型,或者传递没有setter的对象)。

下面我们来详细介绍上述每一点,帮助你理解并应用到工程团队的日常工作中。


没有经过测试的代码一概不安全

每当遇到原本应该在测试捕捉到的bug时,我就会对我们的工程师重复这句话。除非你建立了测试的文化,否则你也会一次又一次地引用这句话。你需要编写大量测试,尤其是单元测试。认真考虑集成测试,并确保你的测试用例足够涵盖核心业务功能。请记住,如果有一段代码没有被测试覆盖到,那么将来肯定会出问题,而且你根本意识不到,直到被你的客户发现。

你需要一遍又一遍地向团队成员重复这句话:“没有经过测试的代码一概不安全”,直到这句话在每个人的心里生根。无论你是刚毕业的新手软件工程师还是经验丰富的资深软件工程师,都应该时刻履行这个实践。


选择有意义的名称

 

计算机科学界有两大难题:缓存失效和命名。

可能你曾听过这句话,这与工程团队的日常工作有着莫大的关系。如果你和你的团队成员不擅长代码中的命名,那么你们的维护工作就会变成一场噩梦,而且你将一事无成。你会失去最优秀的开发人员,而且你的公司距离倒闭也不远了。

这个问题非常严重,你不应该使用诸如data、foobar或myNumber之类不恰当的变量名,而且也绝对不能将SomethingManager作为类名称。务必使用言简意赅的名称,确保在发生冲突时能准确找到。良好的命名不仅可以大幅提高开发人员的效率,而且还可以通过IDE的快捷方式“按名称查找” 等轻松查找文件。另外,良好的命名需要通过严格的代码审核贯彻。


类与函数保持最小,遵守单一功能原则

 

这两大原则的关系就像鸡和鸡蛋一样,无论先有鸡还是先有蛋,有了二者的因果轮回,才有我们无尽的美味。下面先来谈谈类与函数保持最小。

“小”对函数意味着什么?不超过4行代码。是的,你没看错,就是4行。你可能现在就想告辞了,但是千万别走。虽然这个数字看起来有些武断,而且太少,你一辈子可能都没写过像这样的代码。但是,只有4行代码的函数会强迫你认真思考,并为子函数选择真正的好名字,而这些子函数就是代码最好的文档。另外,4行代码意味着你不会使用嵌套的IF语句,省得你需要耗费大量脑力搞清楚所有的代码路径。

下面让我们一起来看一个例子。Node有一个名为“build-url”的npm模块,用途如其名所示:构建URL。你可以通过这个链接(https://github.com/steverydz/build-url/blob/master/src/build-url.js)查看源文件。以下是相关代码。

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行。虽然理解起来也不是非常难,但如果我们应用“小”原则来重构辅助函数,则会大大简化。更新和改进后的版本如下。

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行的原则,但我们创建了几个相对“较小”的函数。每个函数仅完成一项任务,你可以根据函数名轻松理解这段代码。如果需要,你甚至可以针对每个函数进行单元测试,而不是只测试一个大型的buildUrl函数。你可能还会注意到,这种方法产生的代码略多一些,从35行变成了55行。这完全可以接受,因为这55行代码比原来的35行更加方便维护和阅读。

如何才能编写出这样的代码?我个人认为,最简单的方法是列出你希望逐步完成的各项任务。每一步都可以是建立某个子函数或辅助函数。例如,针对上述buildUrl函数我们希望完成如下工作:

  1. 初始化baseUrl和options

  2. 添加路径(如果有的话)

  3. 添加查询参数(如果有的话)

  4. 添加锚点(如果有的话)

请注意,上述每一步都可以直接转化为子函数。一旦养成了这样的习惯,你就可以使用这种自顶向下的方法编写所有代码,然后根据上述步骤列表,建立子函数,再针对每个子函数继续递归,创建步骤列表、建立子函数,以此类推。

下面再来谈谈单一功能原则。根据维基百科,单一功能原则的定义如下:

在面向对象编程领域中,单一功能原则(Single Responsibility Principle)规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。所有它的(这个类的)服务都应该严密的和该功能平行(功能平行,意味着没有依赖)。

在《代码整洁之道》中,Robert Martin给出了另一个定义:

单一功能原则表明,类或模块应有且只有一条加以修改的理由。

假设我们正在建立一个需要某种报告以及显示报告的系统。比较朴素的做法是构建一个存储报告数据以及用于显示报告的逻辑模块/类。但是,这违反了单一功能原则,因为修改该类的高层原因出现了两个。首先,如果报告字段发生变化,我们需要修改类;其次,如果报表可视化要求发生变化,我们也需要修改类。因此,我们不提倡利用一个类存储数据和显示数据的做法,我们应该将这些概念和所有权区域划分为两个不同的类,例如ReportData和ReportDataRenderer等。


函数不能有副作用

 

副作用确实是罪恶之源,因为副作用的存在,编写没有错误的代码会非常困难。看看下面的例子,你能看出副作用吗?

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

根据函数名所示,这个函数的目的是通过电子邮件/密码组合查找用户,这是所有Web应用程序的标准操作。然而,如果你没有阅读代码的实现,就不知道这个函数还有一个隐藏的副作用:在用户登录时,创建一个登录令牌,将其添加到数据库中,然后将cookie发送给用户,而用户则“成功登录”。

这中间有很多问题。

首先,不阅读实现代码就不知道该函数的功能/接口。即使你通过文档说明该函数登录的副作用,也仍然不是理想的做法。工程师喜欢使用现代IDE中的智能提示,因此当遇到一个简单的函数名时,大部分人都不会阅读相应的文档。他们会利用这个函数来获取用户对象,却没有意识到他们正在请求中添加Cookie,这可能会引发很多棘手且不易发现的bug。

其次,考虑到所有的依赖关系,测试这个函数相当困难。你需要验证是否可以通过电子邮件/密码顺利找到用户,需要模拟HTTP响应以及登录令牌的写入。

第三,用户查找和登录之间的紧密结合必然无法满足将来的所有用例,例如,你可能需要单独查找用户或登录用户。换句话说,这个函数不具有前瞻性。


总结

 

总的来说,你需要牢记以下四个提高代码整洁度的原则,并通过这些原则提高团队的生产力:

  1. 没有经过测试的代码一概不安全

  2. 选择有意义的名称

  3. 类与函数保持最小,遵守单一功能原则

  4. 函数不能有副作用

感谢您的阅读!

原文https://engineering.videoblocks.com/these-four-clean-code-tips-will-dramatically-improve-your-engineering-teams-productivity-b5bd121dd150

本文为 CSDN 翻译,转载请注明来源出处。

更多精彩推荐
☞Python 编程语言的核心是什么?
☞新一代视频编解码标准正式公布!
☞B 站 Up 主自制秃头生成器,独秃头不如众秃头?
☞干货!仅有 100k 参数的高效显著性检测方法
☞看完这篇 HashMap ,和面试官扯皮就没问题了
☞密码学应用的四个进化阶段 | 博文精选
点分享点点赞点在看
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!