一、
PHP 简洁代码之道 clean-code-php, 是基于 Clean Code: A Handbook of Agile Software Craftmanship(Clean Code: 敏捷软件开发工艺手册) 这本书做的指南,该书是 Bob Martin 叔叔写的关于如何编写可维护代码的经典书籍。
clean-code-php 指南的灵感来源于 Javascript 版本的 clean-code-javascript ,在其基础加上了 PHP 的特点。
以下是我最喜欢的 clean-code-php 仓库中的一些点:
不添加不必要的上下文
不好的:
<?php
class Car
{
public $carMake;
public $carModel;
public $carColor;
//...
}
好的:
<?php
class Car
{
public $make;
public $model;
public $color;
//...
}
函数参数(不要超过2个)
不好的:
<?php
function createMenu($title, $body, $buttonText, $cancellable) {
// ...
}
好的:
<?php
class MenuConfig
{
public $title;
public $body;
public $buttonText;
public $cancellable = false;
}
$config = new MenuConfig();
$config->title = 'Foo';
$config->body = 'Bar';
$config->buttonText = 'Baz';
$config->cancellable = true;
function createMenu(MenuConfig $config) {
// ...
}
一个函数只做一件事情
不好的:
<?php
function emailClients($clients) {
foreach ($clients as $client) {
$clientRecord = $db->find($client);
if ($clientRecord->isActive()) {
email($client);
}
}
}
好的:
function emailClients($clients) {
$activeClients = activeClients($clients);
array_walk($activeClients, 'email');
}
function activeClients($clients) {
return array_filter($clients, 'isClientActive');
}
function isClientActive($client) {
$clientRecord = $db->find($client);
return $clientRecord->isActive();
}
观点
项目作者概述了本指南的目的如下:
这里的原则并不是所有的都必须遵守,而且几乎只有少部分是被广泛认同的。它们只是一些指导方针,而不是其他,但是它们都是 Clean Code的作者集多年编纂而成的。
在一门动态语言(像 PHP或其他任何语言)领域中,某些开发者可能会不同意其中的某些(或者很多)概念和观点,但是我需要指出的是即使你对其中一些观点不认同,也不要将它们全盘否定掉。
二、变量部分
使用有意义的并且可以读出来的变量名称
不好的:
$ymdstr = $moment->format('y-m-d');
好的:
$currentDate = $moment->format('y-m-d');
对于同一类型的变量使用相同的词汇
不好的:
getUserInfo();
getUserData();
getUserRecord();
getUserProfile();
好的:
getUser();
(译者注:都是要取用户信息,不好的案例中为同一件事起了多个名字,在编码中是要避免的)
使用易于查找的名称(第一部分)
我们读代码的时候要比写代码的时候多的多,所以我们写的代码易读易查找是很重要的。如果不命名好对理解我们的程序有意义的变量,我们会伤害到读我们代码的人。确保你的变量易于查找。
不好的:
// 448 是什么鬼?
$result = $serializer->serialize($data, 448);
好的:
$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
使用易查找的名称(第二部分)
不好的:
// 4 是什么鬼?
if ($user->access & 4) {
// ...
}
好的:
class User
{
const ACCESS_READ = 1;
const ACCESS_CREATE = 2;
const ACCESS_UPDATE = 4;
const ACCESS_DELETE = 8;
}
if ($user->access & User::ACCESS_UPDATE) {
// do edit ...
}
使用可以解释的变量
不好的:
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches[1], $matches[2]);
好一点的:
这个好了一点,但是我们还是非常依赖正则
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);
list(, $city, $zipCode) = $matches;
saveCityZipCode($city, $zipCode);
好的:
通过使用命名子模式我们不必再依赖正则
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,\\]+[,\\\s]+(?<city>.+?)\s*(?<zipCode>\d{5})?$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches['city'], $matches['zipCode']);
不要让读者猜
不要强迫你代码的读者去翻译变量的含义,显式比隐式要好
不好的:
$l = ['Austin', 'New York', 'San Francisco'];
for ($i = 0; $i < count($l); $i++) {
$li = $l[$i];
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// $li 变量代表什么???
dispatch($li);
}
好的:
$locations = ['Austin', 'New York', 'San Francisco'];
foreach ($locations as $location) {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch($location);
}
不增加不必要的语境
如果你的类名或者对象名已经告诉了你一些信息,就不要在方法和和属性上重复他们。
不好的:
class Car
{
public $carMake;
public $carModel;
public $carColor;
//...
}
好的:
class Car
{
public $make;
public $model;
public $color;
//...
}
使用默认参数
不太好的:
这样不太好,因为 $breweryName
可能被传入 NULL
function createMicrobrewery($breweryName = 'Hipster Brew Co.')
{
// ...
}
好一点的:
这个比上一个版本好一点,因为可以保证 $breweryName
不为 Null
function createMicrobrewery($name = null)
{
$breweryName = $name ?: 'Hipster Brew Co.';
// ...
}
好的:
如果你只需要支持 PHP 7
以上的版本, 你可以使用使用类型提示来保证 $breweryName
不为 NULL
。
function createMicrobrewery(string $breweryName = 'Hipster Brew Co.')
{
// ...
}
三、函数部分(一)
1. 函数参数(不要超过两个)
限制函数的参数数量是非常重要的,因为它使你的函数更容易测试。超过三个参数会导致参数之间的组合过多,你必须对每个单独的参数测试大量不同的情况。
没有参数最理想的情况,一个或两个参数是可以接受的,三个以上是应该避免的。这是很重要的。通常,如果你有两个以上的参数,那么你的函数可能试图做的太多,如果不是,你可能需要将一个高级别的对象传当做参数传进去。
不好的
function createMenu($title, $body, $buttonText, $cancellable)
{
// ...
}
好的:
class MenuConfig
{
public $title;
public $body;
public $buttonText;
public $cancellable = false;
}
$config = new MenuConfig();
$config->title = 'Foo';
$config->body = 'Bar';
$config->buttonText = 'Baz';
$config->cancellable = true;
function createMenu(MenuConfig $config)
{
// ...
}
2. 一个函数只做一件事
这是软件工程中最重要的原则。
不好的:
function emailClients($clients)
{
foreach ($clients as $client) {
$clientRecord = $db->find($client);
if ($clientRecord->isActive()) {
email($client);
}
}
}
不好的
function emailClients($clients)
{
$activeClients = activeClients($clients);
array_walk($activeClients, 'email');
}
function activeClients($clients)
{
return array_filter($clients, 'isClientActive');
}
function isClientActive($client)
{
$clientRecord = $db->find($client);
return $clientRecord->isActive();
}
3. 函数名要能说明它是做什么的
不好的:
class Email
{
//...
public function handle()
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 这是什么?一条消息的句柄? 你是要写一个文件么?(读者的疑问)
$message->handle();
好的
class Email
{
//...
public function send()
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 一目了然
$message->send();
4. 函数应该只做一层抽象
当你有多个层次的抽象时,你的函数通常都在试图做的太多。拆分这些函数,可以让代码可重用性更高且更易测试。
不好的:
function parseBetterJSAlternative($code)
{
$regexes = [
// ...
];
$statements = explode(' ', $code);
$tokens = [];
foreach ($regexes as $regex) {
foreach ($statements as $statement) {
// ...
}
}
$ast = [];
foreach ($tokens as $token) {
// lex...
}
foreach ($ast as $node) {
// parse...
}
}
同样不好的:
我们以前从函数中迁出去了一些工作,但是 parseBetterJSAlternative()
函数还是很复杂,不可测试。
function tokenize($code)
{
$regexes = [
// ...
];
$statements = explode(' ', $code);
$tokens = [];
foreach ($regexes as $regex) {
foreach ($statements as $statement) {
$tokens[] = /* ... */;
}
}
return $tokens;
}
function lexer($tokens)
{
$ast = [];
foreach ($tokens as $token) {
$ast[] = /* ... */;
}
return $ast;
}
function parseBetterJSAlternative($code)
{
$tokens = tokenize($code);
$ast = lexer($tokens);
foreach ($ast as $node) {
// parse...
}
}
好的:
最好的解决方案是移除 parseBetterJSAlternative
函数的依赖
class Tokenizer
{
public function tokenize($code)
{
$regexes = [
// ...
];
$statements = explode(' ', $code);
$tokens = [];
foreach ($regexes as $regex) {
foreach ($statements as $statement) {
$tokens[] = /* ... */;
}
}
return $tokens;
}
}
class Lexer
{
public function lexify($tokens)
{
$ast = [];
foreach ($tokens as $token) {
$ast[] = /* ... */;
}
return $ast;
}
}
class BetterJSAlternative
{
private $tokenizer;
private $lexer;
public function __construct(Tokenizer $tokenizer, Lexer $lexer)
{
$this->tokenizer = $tokenizer;
$this->lexer = $lexer;
}
public function parse($code)
{
$tokens = $this->tokenizer->tokenize($code);
$ast = $this->lexer->lexify($tokens);
foreach ($ast as $node) {
// parse...
}
}
}
5.不要使用标志做函数参数
标志相当于告诉使用者,这个函数不止做一件事。函数应该只做一件事,如果函数根据布尔值执行不同代码路径,则你应该将函数拆分。
不好的:
function createFile($name, $temp = false)
{
if ($temp) {
touch('./temp/'.$name);
} else {
touch($name);
}
}
好的:
function createFile($name)
{
touch($name);
}
function createTempFile($name)
{
touch('./temp/'.$name);
}
6.避免副作用
如果一个函数做了“拿到一个值并返回一个值或者多个值”以外的事情,那么这个函数就有可能产生副作用,副作用可能是意外的写入了文件、修改了全局变量、或者打钱给了陌生人。
现在假如你确实要在函数中做一些有可能产生副作用的事情。 比如要写一个文件,你需要做的是将写文件的操作集中到一处,而不是在几个函数或者类里对同一个文件做操作,实现一个服务(函数或者类)去操作它,有且仅有一个。
关键是要能避免常见的陷阱:像是在没有结构的对象之间共享状态、使用可能被写入任何值的可变数据类型、 不集中处理有可能产生副作用的操作。 如果你能做到这些,你会比绝大多数程序员更快乐。
不好的:
// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
$name = 'Ryan McDermott';
function splitIntoFirstAndLastName()
{
global $name;
$name = explode(' ', $name);
}
splitIntoFirstAndLastName();
var_dump($name); // ['Ryan', 'McDermott'];
好的:
function splitIntoFirstAndLastName($name)
{
return explode(' ', $name);
}
$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);
var_dump($name); // 'Ryan McDermott';
var_dump($newName); // ['Ryan', 'McDermott'];
7. 不要修改全局变量
在许多编程语言中污染全局是一种糟糕的做法,因为你的库可能会与另一个库冲突,但是你的库的用户却一无所知,直到在生产环境中爆发异常。让我们来考虑一个例子:如果你想要拿到配置数组怎么办?你可以编写全局函数,如config()
,但是它可能与另一个试图做同样事情的库冲突。
不好的:
function config()
{
return [
'foo' => 'bar',
]
}
好的
class Configuration
{
private $configuration = [];
public function __construct(array $configuration)
{
$this->configuration = $configuration;
}
public function get($key)
{
return isset($this->configuration[$key]) ? $this->configuration[$key] : null;
}
}
加载配置并且创建 Configuration
类的实例
$configuration = new Configuration([
'foo' => 'bar',
]);
现在你应该在你的应用程序中使用 Configuration
类 的实例 。
四、函数部分(二)
8. 不要使用单例模式
(译者注:这一条有些难理解,看不懂就略过吧)
单例模式是一种反模式,Brian Button 的解释:
- 单例通常被用做一个全局的实例,为什么不好?因为你在代码中隐藏了依赖,而不是通过接口暴露他们。通过将一些东西放到全局来避免传递他们是一种“代码异味(code smell)”
code smell 是指能够被开发者察觉到的不好的形式
- 它违反了 SPR原则(single responsibility principle): 由它自己控制自己的创建和生命周期
- 它本身就导致了代码的紧耦合。大多数情况下这使得通过伪造数据来测试变的相当困难。
不好的
class DBConnection
{
private static $instance;
private function __construct($dsn)
{
// ...
}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
// ...
}
$singleton = DBConnection::getInstance();
好的:
class DBConnection
{
public function __construct(array $dsn)
{
// ...
}
// ...
}
创建 DBConneciton
的实例,并配置 DSN
这样你就可以在你的应用程序中使用 DBConnection
的实例了。
9 封装条件判断
不好的:
if ($article->state === 'published') {
// ...
}
好的:
if ($article->isPublished()) {
// ...
}
避免否定类型的判断
不好的:
function isDOMNodeNotPresent($node)
{
// ...
}
if (!isDOMNodeNotPresent($node))
{
// ...
}
好的
function isDOMNodePresent($node)
{
// ...
}
if (isDOMNodePresent($node)) {
// ...
}
10. 避免条件判断
这似乎是一个不可能完成的任务。人们会问“如果不用 if
语句我该怎么做?”,答案是在许多情况下,你可以用多态来实现同样的效果。你可能还会问“这样有什么好处?”,答案是我们之前提到的原则:“一个函数应该只做一件事”, 当你的类或函数中有了 if
语句,相当于告诉别人你的函数做了一件以上的事情。
不好的:
class Airplane
{
// ...
public function getCruisingAltitude()
{
switch ($this->type) {
case '777':
return $this->getMaxAltitude() - $this->getPassengerCount();
case 'Air Force One':
return $this->getMaxAltitude();
case 'Cessna':
return $this->getMaxAltitude() - $this->getFuelExpenditure();
}
}
}
好的:
interface Airplane
{
// ...
public function getCruisingAltitude();
}
class Boeing777 implements Airplane
{
// ...
public function getCruisingAltitude()
{
return $this->getMaxAltitude() - $this->getPassengerCount();
}
}
class AirForceOne implements Airplane
{
// ...
public function getCruisingAltitude()
{
return $this->getMaxAltitude();
}
}
class Cessna implements Airplane
{
// ...
public function getCruisingAltitude()
{
return $this->getMaxAltitude() - $this->getFuelExpenditure();
}
}
11. 避免类型检查(第一部分)
PHP是弱类型语言,意味着你的函数可以接收任何类型的参数。有时你会因为这点自由而受害,这时你可能会在函数中检查参数类型,有许多途径可以避免在函数能做类型检查,首先要做的是有一致的接口。
不好的
function travelToTexas($vehicle)
{
if ($vehicle instanceof Bicycle) {
$vehicle->peddleTo(new Location('texas'));
} elseif ($vehicle instanceof Car) {
$vehicle->driveTo(new Location('texas'));
}
}
好的:
function travelToTexas(Traveler $vehicle)
{
$vehicle->travelTo(new Location('texas'));
}
避免类型检查(第二部分)
如果现在要处理的是基础数据类型,像字符串,整型和数组。现在如果你用的 PHP7以上的版本,而且现在很明显你也不能用多态。现在你应该考虑使用类型检查或者严格模式,它使你能够在php语法层面提供静态类型检查。手动判断类型的问题是,你需要添加许多额外的代码,你获得的人造的“类型安全”抵不过你代码可读性的损失。
不好的
function combine($val1, $val2)
{
if (!is_numeric($val1) || !is_numeric($val2)) {
throw new \Exception('Must be of type Number');
}
return $val1 + $val2;
}
好的
function combine(int $val1, int $val2)
{
return $val1 + $val2;
}
12.移除废弃的代码
废弃的代码跟重复的代码一样不好,没有理由在你的代码库继续保留他们,如果你想找回他们,到版本历史中找回就行了。
不好的
function oldRequestModule($url)
{
// ...
}
function newRequestModule($url)
{
// ...
}
$request = newRequestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');
好的
function requestModule($url)
{
// ...
}
$request = requestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');
五、对象部分
1.使用 setter 和 getter
在 PHP 中,你可以为方法设置 public
, protected
和 private
关键字。使用这些关键字你可以控制一个对象的属性修改权限。
- 如果除了获取对象属性你还想做一些别的事,就不用再到代码库中去寻找并修改每一个修改对象属性的地方(属性入口归一)。
- 方便数据验证
- 封装内部实现
- 获取和设置时方便添加日志和错误处理
- 继承了类,你可以重写默认的函数
- 我们可以延迟加载类的属性,假设它是从服务器获取的
另外,这是开放/封闭原则的一部分,是面向对象的基本设计原则。
不好的
class BankAccount
{
public $balance = 1000;
}
$bankAccount = new BankAccount();
// Buy shoes...
$bankAccount->balance -= 100;
好的
class BankAccount
{
private $balance;
public function __construct($balance = 1000)
{
$this->balance = $balance;
}
public function withdrawBalance($amount)
{
if ($amount > $this->balance) {
throw new \Exception('Amount greater than available balance.');
}
$this->balance -= $amount;
}
public function depositBalance($amount)
{
$this->balance += $amount;
}
public function getBalance()
{
return $this->balance;
}
}
$bankAccount = new BankAccount();
// Buy shoes...
$bankAccount->withdrawBalance($shoesPrice);
// Get balance
$balance = $bankAccount->getBalance();
2.让对象有 私有(private)/受保护的(protected) 的成员
不好的
class Employee
{
public $name;
public function __construct($name)
{
$this->name = $name;
}
}
$employee = new Employee('John Doe');
echo 'Employee name: '.$employee->name; // Employee name: John Doe
好的
class Employee
{
private $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
$employee = new Employee('John Doe');
echo 'Employee name: '.$employee->getName(); // Employee name: John Doe
六、类部分
使用组合而不是继承
“Gang of Four”在设计模式里所声明的,你应该优先选择“组合模式”而不是“继承”
译者注:Gang of Four 译成 四人帮,指代 DesignPatternBook 的四位作者
不论是使用“组合模式”还是使用“继承”都有许多理由。
这个话题的要点是当你本能的要使用继承时就想一想是否“组合模式”能帮你更好的解决问题。
你可能会问,“我什么时候应该用继承?”, 这取决于你手头的问题。这里有一个列表说明什么时候使用继承会更合适:
- 你的继承表达了一个“is-a(是)” 的关系,不是“has-a(有)”的关系(对比人类 -> 动物 和 用户-> 用户详情 )
- 你能从基础类中复用代码(人类能够像所有动物一样移动)
- 你想通过修改全局类来对所有派生类进行修改。(例如改变所有动物移动消耗的热量)
不好的:
class Employee
{
private $name;
private $email;
public function __construct($name, $email)
{
$this->name = $name;
$this->email = $email;
}
// ...
}
// 不好,因为是雇员有税收数据
// 税收数据不是一种雇员,所以使用继承不适合
class EmployeeTaxData extends Employee
{
private $ssn;
private $salary;
public function __construct($name, $email, $ssn, $salary)
{
parent::__construct($name, $email);
$this->ssn = $ssn;
$this->salary = $salary;
}
// ...
}
好的:
class EmployeeTaxData
{
private $ssn;
private $salary;
public function __construct($ssn, $salary)
{
$this->ssn = $ssn;
$this->salary = $salary;
}
// ...
}
class Employee
{
private $name;
private $email;
private $taxData;
public function __construct($name, $email)
{
$this->name = $name;
$this->email = $email;
}
public function setTaxData($ssn, $salary)
{
$this->taxData = new EmployeeTaxData($ssn, $salary);
}
// ...
}
避免使用流式接口(fluent interfaces)
fluent interfaces 是一种面向对象的接口,通过Method chaining(链式操作)来提高代码的可读性。
然而有一些Context,通常是构建对象,这个设计模式虽然减少了代码的啰嗦(例如:PHPUnit Mock Builder 或者 Doctrine Query Builder),大部分时候他会产生如下这些消耗:
- 破坏了封装Encapsulation
- 破坏了装饰器 Decorators
- 难以Mock 一个测试套件
- 使 commit 之间的变更难以阅读
想获取更多信息请阅读,请阅读 Marco Pivetta关于这个话题的完整文章
不好的:
class Car
{
private $make = 'Honda';
private $model = 'Accord';
private $color = 'white';
public function setMake(string $make): self
{
$this->make = $make;
// NOTE: Returning this for chaining
return $this;
}
public function setModel(string $model): self
{
$this->model = $model;
// NOTE: Returning this for chaining
return $this;
}
public function setColor(string $color): self
{
$this->color = $color;
// NOTE: Returning this for chaining
return $this;
}
public function dump(): void
{
var_dump($this->make, $this->model, $this->color);
}
}
$car = (new Car())
->setColor('pink')
->setMake('Ford')
->setModel('F-150')
->dump();
好的:
class Car
{
private $make = 'Honda';
private $model = 'Accord';
private $color = 'white';
public function setMake(string $make): void
{
$this->make = $make;
}
public function setModel(string $model): void
{
$this->model = $model;
}
public function setColor(string $color): void
{
$this->color = $color;
}
public function dump(): void
{
var_dump($this->make, $this->model, $this->color);
}
}
$car = new Car();
$car->setColor('pink');
$car->setMake('Ford');
$car->setModel('F-150');
$car->dump();