c

codeye

V1

2022/09/22阅读:15主题:默认主题

代码质量

这四个 "清洁代码 "的技巧将极大地提高你的工程团队的生产力

几年前,我们肯定是在右边的房间,但我们已经向左边的房间靠近了很多。原文来源:这里。 几年前,在VideoBlocks,我们有一个重大的代码质量问题:大多数文件的逻辑是 "意大利面条",大量的重复,没有测试等等。编写新的功能,甚至小的错误修复,最多需要吃几颗Tums,而整瓶的Pepto-Bismol和Scotch则太频繁了。我们每分钟的 "WTF "次数高得惊人。

今天,我们的代码库的整体质量明显提高,这在很大程度上要归功于我们为提高代码质量所做的刻意努力。几年前,当我们发现这个问题时,我们作为一个团队阅读了罗伯特-马丁的《清洁代码》,并尽力实施他的建议,甚至将 "清洁代码 "作为工程团队的一个核心文化原则。我强烈建议在你开始扩大规模时做这两件事。适当地实施 "清洁代码 "的做法,从长远来看,将使生产力翻倍(至少),并显著提高工程团队的士气。如果有选择的话,谁愿意呆在右边的房间里?

在我们从 "清洁代码 "和其他来源实施的所有想法中,有五个想法至少提供了80%的生产力和团队幸福感的提升。

"如果它没有被测试,它就是坏的" 写大量的测试,尤其是单元测试,否则你会后悔的。 选择有意义的名字 为变量、类和函数使用简短而精确的名称。 类和函数应该很小,并遵守单一责任原则(SRP)。 函数应不超过4行,类应不超过100行。是的,你没看错。它们也应该只做一件事,而且只做一件事。

函数不应该有副作用 副作用(例如,修改一个输入参数)是邪恶的。请确保在你的代码中不出现它们。在可能的情况下,在函数契约中明确规定这一点(例如,传入本地类型或没有设置器的对象)。 让我们来详细了解每一项,以便你能理解并开始在工程团队的日常生活中应用它们。

    1. "如果它没有经过测试,它就是坏的" 我开始定期向我们的工程师重复这句话,因为我们遇到了本应被(不存在的)测试所捕获的错误。除非你建立一种测试文化,否则你也会一次又一次地证明这句话是真的。写大量的测试,特别是单元测试。认真思考集成测试,确保你有足够的数量来覆盖你的核心业务功能。记住,如果一段代码没有测试覆盖,你很可能会在未来破坏它而不自知,直到你的客户发现这个错误。

对你的团队反复强调 "如果它没有被测试,它就会被破坏",直到这个信息深入人心。无论你是一个刚从学校毕业的全新软件工程师还是一个经验丰富的老手,都要言出必行。

  1. 选择有意义的名字 计算机科学中有两个难点:缓存失效和事物的命名。

你可能以前听过这句话,它与你在工程团队中的日常生活再相关不过了。如果你和你的团队不善于在你的代码中命名东西,它将成为一个无法维护的噩梦,你将无法完成任何事情。你会失去你最好的开发人员,你的公司很快就会倒闭。

不过说真的,朋友们不会让朋友使用像data、foobar或myNumber这样糟糕的变量名,他们当然也不会让他们给类命名为SomethingManager这样的东西。确保你的名字简短而精确,但在有冲突时,更倾向于精确。围绕开发人员的效率进行大力优化,通过 "按名称查找 "的IDE快捷方式使文件易于查找。通过代码审查严格执行良好的命名。

    1. 类和函数应该是小的,并遵守单一责任原则(SRP)。 小和SRP就像鸡和蛋一样,是一种美味的良性循环。让我们从小开始。

对函数来说,"小 "是什么意思?不超过4行的代码。是的,你没看错,4行。你现在可能正在关闭标签,但你真的不应该。这看起来有些武断和小,而且你可能一辈子都没有写过这样的代码。然而,4行函数迫使你认真思考,为子函数挑选很多真正好的名字,使你的代码能够自我记录。此外,它们还意味着你不能使用嵌套的IF语句,这迫使你做心理体操来理解所有的代码路径。

让我们一起走过一个例子。Node有一个名为 "build-url "的npm模块,它的作用与它的名字一样:构建URL。你可以在这里找到我们要看的源文件的链接。下面是相关的代码。

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函数进行测试。你可能还注意到,这种方法产生的代码略多,是55行而不是35行。这是完全可以接受的,因为这55行比35行更容易维护和阅读。

你怎么写这样的代码?我个人认为,最简单的做法是把完成你希望完成的任务所需的步骤列表写下来。这些步骤中的每一个都可能是一个子函数/辅助函数的好候选人。例如,我们可以这样描述buildUrl函数。

初始化我们的基本URL和选项 添加路径(如果有的话) 添加查询参数(如果有) 添加哈希值(如果有的话) 请注意,这些步骤中的每一步几乎都直接转化为一个子函数。一旦你养成了这种习惯,你最终会使用这种自上而下的方法来编写你的所有代码,在这里你创建一个步骤列表,存根函数,并继续像这样递归到每个子函数中,创建一个步骤列表,存根,等等。

接着是我们的相关概念,单一责任原则。这意味着什么呢?直接引用维基百科的内容。

单一责任原则(SRP)是一个计算机编程原则,它指出每个模块或类应该对软件所提供的功能的单一部分负责,而且这个责任应该完全被类所封装。它的所有服务都应该与这一责任紧密结合起来。

Robert Martin在Clean Code中提供了一个平行的定义。

SRP指出,一个类或模块应该有一个,而且只有一个,改变的理由。

比方说,我们正在建立一个系统,需要某种类型的报告并显示它。一个天真的方法可能是建立一个单一的模块/类来存储报告数据以及显示报告的逻辑。然而,这违反了SRP,因为有两个高级别的理由来修改这个类。首先,如果报告字段发生变化,我们就需要更新它。第二,如果报告的可视化要求改变了,我们就需要更新这个类。因此,我们不应该用一个类来存储数据和渲染数据的逻辑,而应该把这些概念和所有权领域分成两个不同的类,比如ReportData和ReportDataRenderer或类似的东西。

  1. 函数不应该有副作用 副作用是真正邪恶的,它使创建没有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;
}

这个函数被设计成通过电子邮件/密码组合来查找用户,这是任何网络应用的一个标准操作。然而,它也有一个隐藏的副作用,作为函数的消费者,你在没有阅读实现代码的情况下并不知道:它将用户登录,从而创建一个登录令牌,将其添加到数据库中,并将cookie的值发回给我们的用户,这样他们随后就 "登录 "了。

这里面有很多问题。

首先,如果不看实现代码,函数契约/接口就不容易理解。即使登录的副作用被记录下来了,但这仍然是不理想的。工程师们倾向于使用现代IDE中的intellisense,所以不会认为他们需要根据简单的函数名称来阅读文档。他们会倾向于只使用这个函数来获取用户对象,而没有意识到他们在请求中加入了一个cookie,这可能会导致各种有趣的难以发现的bug。

第二,考虑到所有的依赖性,测试这个函数是相当具有挑战性的。验证你是否可以通过电子邮件/密码查找用户,需要模拟出一个HTTP响应,以及处理对登录令牌表的写入。

第三,用户查询和登录之间的紧密耦合不可避免地不能满足未来的所有用例,在这些用例中,你可能需要独立地查询一个用户或登录一个用户。换句话说,这不是 "面向未来 "的。

总之,一定要记住并应用这四条 "清洁代码 "原则,以极大地提高你的团队的生产力。

"如果它没有经过测试,它就是坏的" 选择有意义的名字 类和函数应该是小的,并遵守单一责任原则(SRP)。 函数不应该有副作用 在以后的博文中,我将介绍包括不可更改性、服务/工厂/价值对象(VO)三者之间的必然设计模式等等。

分类:

后端

标签:

后端

作者介绍

c
codeye
V1