一、
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();