本文共 16813 字,大约阅读时间需要 56 分钟。
一、
PHP 简洁代码之道 , 是基于 Clean Code: A Handbook of Agile Software Craftmanship(Clean Code: 敏捷软件开发工艺手册) 这本书做的指南,该书是 写的关于如何编写可维护代码的经典书籍。
clean-code-php 指南的灵感来源于 Javascript 版本的 ,在其基础加上了 PHP 的特点。
以下是我最喜欢的 clean-code-php 仓库中的一些点:
不好的:
好的:
不好的:
好的:
title = 'Foo';$config->body = 'Bar';$config->buttonText = 'Baz';$config->cancellable = true;function createMenu(MenuConfig $config) { // ...}
不好的:
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]+(?.+?)\s*(? \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.'){ // ...}
三、
限制函数的参数数量是非常重要的,因为它使你的函数更容易测试。超过三个参数会导致参数之间的组合过多,你必须对每个单独的参数测试大量不同的情况。
没有参数最理想的情况,一个或两个参数是可以接受的,三个以上是应该避免的。这是很重要的。通常,如果你有两个以上的参数,那么你的函数可能试图做的太多,如果不是,你可能需要将一个高级别的对象传当做参数传进去。
不好的
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){ // ...}
这是软件工程中最重要的原则。
不好的: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();}
不好的:
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();
当你有多个层次的抽象时,你的函数通常都在试图做的太多。拆分这些函数,可以让代码可重用性更高且更易测试。
不好的:
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... } }}
标志相当于告诉使用者,这个函数不止做一件事。函数应该只做一件事,如果函数根据布尔值执行不同代码路径,则你应该将函数拆分。
不好的:
function createFile($name, $temp = false){ if ($temp) { touch('./temp/'.$name); } else { touch($name); }}
好的:
function createFile($name){ touch($name);}function createTempFile($name){ touch('./temp/'.$name);}
如果一个函数做了“拿到一个值并返回一个值或者多个值”以外的事情,那么这个函数就有可能产生副作用,副作用可能是意外的写入了文件、修改了全局变量、或者打钱给了陌生人。
现在假如你确实要在函数中做一些有可能产生副作用的事情。 比如要写一个文件,你需要做的是将写文件的操作集中到一处,而不是在几个函数或者类里对同一个文件做操作,实现一个服务(函数或者类)去操作它,有且仅有一个。
关键是要能避免常见的陷阱:像是在没有结构的对象之间共享状态、使用可能被写入任何值的可变数据类型、 不集中处理有可能产生副作用的操作。 如果你能做到这些,你会比绝大多数程序员更快乐。
不好的:
// 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'];
在许多编程语言中污染全局是一种糟糕的做法,因为你的库可能会与另一个库冲突,但是你的库的用户却一无所知,直到在生产环境中爆发异常。让我们来考虑一个例子:如果你想要拿到配置数组怎么办?你可以编写全局函数,如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
类 的实例 。
四、
(译者注:这一条有些难理解,看不懂就略过吧)
单例模式是一种反模式,Brian Button 的解释:
code smell 是指能够被开发者察觉到的不好的形式
不好的
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
的实例,并配置
这样你就可以在你的应用程序中使用 DBConnection
的实例了。
不好的:
if ($article->state === 'published') { // ...}
好的:
if ($article->isPublished()) { // ...}
不好的:
function isDOMNodeNotPresent($node){ // ...}if (!isDOMNodeNotPresent($node)){ // ...}
好的
function isDOMNodePresent($node){ // ...}if (isDOMNodePresent($node)) { // ...}
这似乎是一个不可能完成的任务。人们会问“如果不用 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(); }}
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;}
废弃的代码跟重复的代码一样不好,没有理由在你的代码库继续保留他们,如果你想找回他们,到版本历史中找回就行了。
不好的
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');
五、
在 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();
不好的
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 译成 四人帮,指代 DesignPatternBook 的四位作者
不论是使用“组合模式”还是使用“继承”都有许多理由。
这个话题的要点是当你本能的要使用继承时就想一想是否“组合模式”能帮你更好的解决问题。你可能会问,“我什么时候应该用继承?”, 这取决于你手头的问题。这里有一个列表说明什么时候使用继承会更合适:
不好的:
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); } // ...}
是一种面向对象的接口,通过(链式操作)来提高代码的可读性。
然而有一些Context,通常是构建对象,这个设计模式虽然减少了代码的啰嗦(例如: 或者 Doctrine Query Builder),大部分时候他会产生如下这些消耗:
不好的:
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();
转载地址:http://uruws.baihongyu.com/