编码指南
在上一节中,我们谈到了为什么要引入编码标准。一旦引入了编码标准,你就应该考虑制定编码指南。这两个话题听起来都很熟悉,事实上也的确如此。然而,编码标准通常侧重于如何格式化代码,而编码指南则定义如何编写代码。这当然包括定义使用哪种编码标准,但还包括更多内容,你将在本节中了解到。
如何编写代码的确切含义是什么?在编写软件时,通常有不止一种方法可以达到目的。以广为人知的模型-视图-控制器(MVC)模式为例。它用于将应用程序逻辑划分为三种相互关联的元素—模型、视图和控制器。但它并没有明确定义业务逻辑的位置。业务逻辑应该放在控制器内部,还是放在模型内部?
这个问题没有明确的对错答案。但我们建议采用 "胖模型,瘦控制器" 的方法:业务逻辑不应写在控制器中,因为控制器是视图和特定问题代码之间的绑定元素。此外,控制器通常包含大量特定于框架的代码,因此尽可能不在业务逻辑中使用这些代码是一种很好的做法。
无论我们的建议如何,你都应该在项目的编码指南中明确你认为你的团队应该如何处理这个问题。否则,你的代码库中很可能会同时存在这两种方法。
通常,编码指南会涉及如何命名方法、函数和属性等问题。正如你可能从 "计算机科学中只有两件难事:缓存失效和命名 "这句名言中了解到的,找到正确的名称确实不是一个小问题。因此,在这一主题上制定惯例至少可以减少开发人员试图找到合适名称的时间。此外,与编码标准一样,它们也有助于减少认知摩擦。
编码指南可以帮助团队中经验较少的开发人员或刚入门的开发人员找到解决方案,否则他们就需要在代码中或互联网上搜索。正如我们在 第 3 章 "代码,不要做特技" 中所讨论的,它还能避免不良做法,从而帮助编写可维护的代码。为了帮助你开始设置第一套规则,我们将在下一节中给出一些示例。
编码指南示例
从一张白纸(虚拟)开始是很困难的,因此在本节中,我们收集了一系列现实世界中的示例,这些示例可以成为你编码指南的一部分。请注意,尽管这些规则基于最佳实践,但它们并不完美,也不是唯一的真理。我们只是希望为你提供一个良好的起点,以便你讨论和举例说明应使用编码指南来明确哪些主题。
命名规范
通过使用 命名约定,我们可以确保代码中的某些元素以统一、易懂的方式命名。这减少了认知上的摩擦,使新团队成员的入职培训更加容易。
Services, repositories 和 models
以 UpperCamelCase 书写。使用类型作为后缀。
下面是一些例子:
-
UserService
-
ProductRepository
-
OrderModel
Events
用 UpperCamelCase 书写。使用正确的时态来表示事件是在实际事件之前还是之后触发的。
下面是一些示例:
-
DeletingUser 是删除前的事件
-
DeleteUser 是实际事件
-
UserDeleted 是删除后的事件
PHP 一般惯例
即使你已经使用 PSR-12 等编码标准,它们也有某些方面没有涉及。我们将在本节中介绍其中的一些内容。
Comments 和 DocBlocks
尽可能避免使用注释,因为注释往往会过时,从而造成混淆而非帮助。只保留无法用不言自明的名称或简化代码来替代的注释,这样代码就更容易理解,也不再需要注释了。
只有在添加了信息(如代码质量工具的注释)时才添加 DocBlocks。特别是从 PHP 8 开始,大多数 DocBlocks 都可以用类型提示来代替,所有现代集成开发环境都能理解。如果使用类型提示,大多数 DocBlocks 都可以移除:
// Redundant DocBlock
/**
* @param int $property
* @return void
*/
public function setProperty(int $property): void {
// ...
}
通常,DocBlocks 是由集成开发环境自动生成的。如果不进行更新,它们充其量只是无用之物,甚至可能是完全错误的:
// Useless DocBlock
/**
* @param $property
*/
public function setProperty(int $property): void {
// ...
}
// Wrong DocBlock
/**
* @param string $property
*/
public function setProperty(int $property): void {
// ...
}
DocBlocks 仍应用于 PHP 语言特性至今仍无法提供的信息,例如指定数组的内容或将函数标记为已废弃:
// Useful DocBlock
/**
* @return string[]
*/
public function getList(): array {
return [
'foo',
'bar',
];
}
/**
* @deprecated use function fooBar() instead
*/
public function foo(): bool {
// ...
}
三元运算符
每个部分都应写成一行,以增加可读性。非常简短的语句可以例外:
// Example for short statement
$isFoo ? 'foo' : 'bar';
// Usual notation
$isLongerVariable
? 'longerFoo'
: 'longerBar';
不要使用嵌套的三元运算符,因为它们很难读取和调试:
// Example for nested operators
$number > 0 ? 'Positive' : ($number < 0 ? 'Negative' : 'Zero');
Constructor
如果使用 PHP 8 或更高版本,可使用构造函数属性推广来缩短类的长度。在最后一个属性后保留逗号,这样可以方便添加或注释行:
// Before PHP 8+
class ExampleDTO
{
public string $name;
public function __construct(
string $name
) {
$this->name = $name;
}
}
// Since PHP 8+
class ExampleDTO
{
public function __construct(
public string $name,
) {}
}
Arrays
始终使用短数组符号,并在最后一个条目后保留逗号(参见上一节构造函数的解释):
// Old notation
$myArray = array(
'first entry',
'second entry'
);
// Short array notation
$myArray = [
'first entry',
'second entry',
];
控制结构
始终使用括号,即使是单行也不例外。这样可以减少认知上的摩擦,也便于以后添加更多行代码:
// Bad
if ($statement === true)
do_something();
// Good
if ($statement === true) {
do_something();
}
避免使用 else 语句和提前返回语句,因为这样更易于阅读,并能降低代码的复杂性:
// Bad
if ($statement) {
// Statement was successful
return;
} else {
// Statement was not successful
return;
}
// Good
if (!$statement) {
// Statement was not successful
return;
}
// Statement was successful
return;
制定指导方针
制定编码指南的过程需要时间,通常需要召开几次研讨会来讨论规则。这需要技术负责人等人的调节;否则,你可能会陷入无休止的讨论中。
如果无法立即就所有议题达成一致,也不要担心。要提醒自己,团队中的每个人都有不同的背景、经验和技能水平,没有人会因为突然出现了他们不理解或不接受的规则,就直接抛弃他们个人的编码方式。
确保建立一个流程,不时检查准则是否需要更新。也许有些规则会随着时间的推移而过时,或者必须加入新的语言特性。在定期召开的团队会议上提出一个行动要点就是一个很好的机会。
指导原则应易于以书面形式访问,如维基或公司内部知识库,并能跟踪版本历史。每个团队成员都应能在上面写下评论,以便一旦出现疑问或问题就能立即得到处理。最后,所有团队成员都应自动获知新的变更。
一旦你的团队就一系列规则达成一致,请务必利用你在前面章节中学到的代码质量工具来自动检查这些规则是否得到遵守。例如,你可以使用 PHPStan 来检测空的 catch 块,或者使用 PHPMD 来执行 if 而不使用 else。
如何确保编码指南得到执行?显然,我们应尽可能使用代码质量工具。但是,如果这些工具不包含我们想要执行的规则呢?只要在互联网上搜索一下,也许就能找到第三方实现。如果找不到,也可以自己编写自定义规则,因为所有的静态代码分析器都是可扩展的。
对于过于复杂而无法自动检查的规则,我们必须手动检查它们是否被正确使用。这可能发生在代码审查中,我们认为它们非常重要,值得在本章中单独列出一节。
如果不确保编码指南得到遵守,那么仅仅制定编码指南只会浪费时间。我们可以自动检查所有与编码风格相关的规则以及相当数量的编码指南。但目前,对于那些涉及框架指南或架构方面的规则,自动化已不再可行,我们必须跳出来,接手检查工作。此时,代码审查就开始发挥作用了。让我们在下一节详细了解一下。