其实今天本来我是想翻译这一篇的:
Code Review Best Practices
作者:https://medium.com/@palantir
如果你熟悉美国公司的话会知道这是个CIA投资的大数据公司,所以没有头像没有作者名字。
不过我很欣喜的看到已经有人干啦,哈哈。
[译] 代码审查之最佳实践
于是今天我们换一篇,来看这个:
These four “clean code” tips will dramatically improve your engineering team’s productivity
作者:Jonathan Fulton
任何对此文感兴趣的读者请花费50美元/年或5美元/月的价格参加Medium会员计划,否则每月只能查看三篇文章。
一个月一餐盒饭钱,我觉得并不贵,你说呢?
以下为本人对文章的翻译。不对内容负责,也不对任何翻译错误负责,同时不包含任何图片内容,如需查看图片请付费查看原文链接,谢谢。
此处有图,基于本博客译文原则略去。
原始出处:http://www.osnews.com/story/19266/WTFs_m
几年前,在VideoBlocks,我们遇到了一个主要的代码质量问题:大多数文件中的“千层饼”逻辑,大量重复代码,没有测试等等。编写新功能甚至是小错误修复都麻烦得要死。我们的每天都恨不得操程序它祖宗。
今天,我们的代码库的整体质量明显更好,这在很大程度上要归功于为提高代码质量而进行的刻意努力。几年前,当我们发现问题时,我们将Robert Martin的代码整洁之道 作为一个团队必修课,并尽力实施他的建议,甚至将“整洁代码”作为工程团队的核心文化部分。我强烈建议你在开始扩张时同时做到以上两件事。适当地实施“整洁代码”实践将使生产率从长远来看(最低限度)翻倍,并显着提高工程团队的士气。可以选的话谁愿意呆在右边的房间呢?
胖达注:右边的房间请参照图理解。
在我们从“代码整洁之道”和其他来源实施的所有想法中,有五个提供了至少80%的生产力收益和团队幸福感。
胖达注:美国人的数学这么差的么,我数来数去只有四个啊!标题也是四个啊!!简直要被作者逼疯啦!!!
- “如果没有经过测试,它一定不能用”
编写大量测试,特别是单元测试,否则等着后悔吧。 - 选择有意义的名称
对变量,类和函数使用简短且精确的名称。 - 类和函数应该很小并遵守单一责任原则(Single Responsibility Principle, SRP)
函数不应超过4行,并且类不超过100行。是的,你没看错。他们也应该只实现一个功能。 - 函数应该没有副作用
副作用(例如,修改入参的值)是邪恶的。确保不要在代码中发生。Specify this explicitly in the function contracts where possible (e.g., pass in native types or objects that have no setters)
胖达注:这一句很难翻译精确所以直接看原文吧。
让我们逐个详细介绍,以便您了解并开始在工程团队的日常生活中应用它们。
1. “如果没有经过测试,它一定不能用”
每次当我们遇到了应该被(缺失的)测试捕获的错误,我就开始经常向我们的工程师重复这句话。除非你建立一种测试文化,否则你也会一次又一次地证明这句话的正确性。不断的写很多测试,尤其是单元测试。仔细考虑集成测试,并确保有足够的数量来覆盖您的核心业务功能。请记住,如果有一段代码被测试遗漏了,您可能会在没有意识到的情况下将其破坏,直到你的客户发现这有个bug。
重复“如果没有经过测试,它一定是坏的”,一遍又一遍地向你的团队发出消息,直到信息被吸收。不断实践这段内容,无论你是一个刚毕业的全新软件工程师还是一个头发花白的老手。
2. 选择有意义的名称
计算机科学中存在两个难题:缓存失效和命名事物。
您之前可能已经听过这个说法,并且您在工程团队中的日常生活绝对会不断发生。如果您和您的团队不擅长在代码中命名,那么它将成为一个难以维护的噩梦,您将无法完成任何工作。您将失去最优秀的开发人员,您的公司很快就会破产。
但是说真的,朋友,千万别使用像data,foobar或myNumber这样的错误变量名,而且绝对不要让他们像SomethingManager那样命名类。确保你的名字简短而准确,而在两者冲突时精确更重要。通过“按名称查找”IDE快捷方式,可以极大地优化开发人员的效率并轻松查找文件。通过代码审查严格执行良好的命名。
3. 类和函数应该很小并遵守单一责任原则(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行。这并不是非常难理解,但如果我们应用我们的“小”原则来分解helper函数,那么理解可能要容易得多。下面是更新和改进的版本。
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行更易于维护,更容易阅读。
怎么才能写出这样的代码呢?我个人觉得最简单的方法是先把您希望完成的任务所需的步骤按列表记录下来。这些步骤中的每一步都可能是子/helper函数的良好候选者。例如,我们可以按如下步骤描述buildUrl函数:
- 初始化我们的基本网址和选项
- 添加路径(如果有)
- 添加查询参数(如果有)
- 添加哈希(如果有的话)
请注意这些步骤中的每一步几乎都能直接转换为子函数。一旦养成习惯,你最终会使用这种自上而下的方法编写所有代码,在这种方法中你创建一个步骤列表,用函数实现,并像这样递归地继续进入每个子函数创建一个列表步骤,再用函数实现等等。
继续我们的相关概念,单一责任原则(SRP)。这是什么意思?直接从维基百科引用:
单一责任原则(SRP)是一种计算机编程原则,规定每个模块或类应对软件提供的单个功能部分负责,并且该责任应完全由类封装。其所有服务应与该责任严格一致。
代码整洁之道中 Robert Martin 提供了一个并行定义:
SRP表明一个类或则模块只应该因为一个原因而改变。
假设我们正在构建一个需要某种类型报告并显示它的系统。一种简单的方法可能是构建一个存储报告数据的单个模块/类以及用于显示报告的逻辑。但是,这违反了SRP,因为你将可能因为两个高级别的原因修改类代码。首先,如果报告字段发生变化,我们需要更新它。其次,如果报告可视化要求发生变化,我们将需要更新该类。因此,我们应该将这些概念和所有权区域分成两个不同的类,比如ReportData和ReportDataRenderer或类似的东西,而不是单个类同时实现存储数据和呈现数据的逻辑。
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;
}
函数的功能如名字所示,旨在通过电子邮件/密码组合查找用户,这是任何Web应用程序的标准操作。但是,它还有一个隐藏的副作用,作为函数使用者除非你读取实现代码否则你是不知道的:它让用户被登录了,创建了登录令牌,再将其添加到数据库,最后将cookie发送回函数找到的用户,用户被“登录”了。
这有很多问题。
首先,如果不阅读实现代码,就不容易理解函数界面/接口。就算登录副作用在文档中写了,它仍然不理想。工程师倾向于在现代IDE中使用intellisense,因此不会认为他们需要来阅读文档来理解如此简单命名的函数。他们倾向于仅使用该功能来获取用户对象,没有意识到他们正在向请求添加cookie,这可能导致各种有趣的难以发现的错误。
其次,考虑到所有依赖关系,测试函数是相当具有挑战性的。验证您是否可以通过电子邮件/密码查看用户需要模拟HTTP响应以及处理对登录令牌表的写入。
第三,用户查找和登录之间的紧密耦合不可避免地不能满足将来的所有用例,您可能需要查找用户或独立登录用户。换句话说,它不是“未来可用的”。
总之,请务必记住并应用这四个“代码”原则,以显着提高团队的工作效率:
- “如果没有经过测试,它一定不能用”
- 选择有意义的名称
- 类和函数应该很小并遵守单一责任原则(Single Responsibility Principle, SRP)
- 函数应该没有副作用
在稍后的博客文章中,我将介绍配套设计模式,包括不变性,服务/工厂/价值对象(VO)三元组等。