Robert C.Martin's 的 軟件工程師準(zhǔn)則 Clean Code 同樣適用于 PHP。它并不是一個(gè)編碼風(fēng)格指南,它指導(dǎo)我們用 PHP 寫出具有可讀性,可復(fù)用性且可分解的代碼。
并非所有的準(zhǔn)則都必須嚴(yán)格遵守,甚至一些已經(jīng)成為普遍的約定。這僅僅作為指導(dǎo)方針,其中許多都是 Clean Code 作者們多年來的經(jīng)驗(yàn)。
靈感來自于 clean-code-javascript
盡管許多開發(fā)者依舊使用 PHP 5 版本,但是這篇文章中絕大多數(shù)例子都是只能在 PHP 7.1 + 版本下運(yùn)行。
使用有意義的且可讀的變量名
不友好的:
$ymdstr = $moment->format('y-m-d');
友好的:
$currentDate = $moment->format('y-m-d');
對同類型的變量使用相同的詞匯
不友好的:
getUserInfo(); getUserData(); getUserRecord(); getUserProfile();
友好的:
getUser();
使用可搜索的名稱(第一部分)
我們閱讀的代碼超過我們寫的代碼。所以我們寫出的代碼需要具備可讀性、可搜索性,這一點(diǎn)非常重要。要我們?nèi)ダ斫獬绦蛑袥]有名字的變量是非常頭疼的。讓你的變量可搜索吧!
不具備可讀性的代碼:
// 見鬼的 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]);
一般:
這個(gè)好點(diǎn),但我們?nèi)試?yán)重依賴正則表達(dá)式。
$address = 'One Infinite Loop, Cupertino 95014'; $cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/'; preg_match($cityZipCodeRegex, $address, $matches); [, $city, $zipCode] = $matches; saveCityZipCode($city, $zipCode);
很棒:
通過命名子模式減少對正則表達(dá)式的依賴。
$address = 'One Infinite Loop, Cupertino 95014'; $cityZipCodeRegex = '/^[^,]+,\s*(?city>.+?)\s*(?zipCode>\d{5})$/'; preg_match($cityZipCodeRegex, $address, $matches); saveCityZipCode($matches['city'], $matches['zipCode']);
避免嵌套太深和提前返回 (第一部分)
使用太多 if else 表達(dá)式會導(dǎo)致代碼難以理解。
明確優(yōu)于隱式。
不好:
function isShopOpen($day): bool { if ($day) { if (is_string($day)) { $day = strtolower($day); if ($day === 'friday') { return true; } elseif ($day === 'saturday') { return true; } elseif ($day === 'sunday') { return true; } else { return false; } } else { return false; } } else { return false; } }
很棒:
function isShopOpen(string $day): bool { if (empty($day)) { return false; } $openingDays = [ 'friday', 'saturday', 'sunday' ]; return in_array(strtolower($day), $openingDays, true); }
避免嵌套太深和提前返回 (第二部分)
不好:
function fibonacci(int $n) { if ($n 50) { if ($n !== 0) { if ($n !== 1) { return fibonacci($n - 1) + fibonacci($n - 2); } else { return 1; } } else { return 0; } } else { return 'Not supported'; } }
很棒:
function fibonacci(int $n): int { if ($n === 0 || $n === 1) { return $n; } if ($n > 50) { throw new \Exception('Not supported'); } return fibonacci($n - 1) + fibonacci($n - 2); }
避免心理映射
不要迫使你的代碼閱讀者翻譯變量的意義。
明確優(yōu)于隱式。
不好:
$l = ['Austin', 'New York', 'San Francisco']; for ($i = 0; $i count($l); $i++) { $li = $l[$i]; doStuff(); doSomeOtherStuff(); // ... // ... // ... // Wait, what is `$li` for again? dispatch($li); }
很棒:
$locations = ['Austin', 'New York', 'San Francisco']; foreach ($locations as $location) { doStuff(); doSomeOtherStuff(); // ... // ... // ... dispatch($location); }
不要增加不需要的上下文
如果類名或?qū)ο竺嬖V你某些東西后,請不要在變量名中重復(fù)。
小壞壞:
class Car { public $carMake; public $carModel; public $carColor; //... }
好的方式:
class Car { public $make; public $model; public $color; //... }
使用默認(rèn)參數(shù)而不是使用短路運(yùn)算或者是條件判斷
不好的做法:
這是不太好的因?yàn)?$breweryName 可以是 NULL.
function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void { // ... }
還算可以的做法:
這個(gè)做法比上面的更加容易理解,但是它需要很好的去控制變量的值.
function createMicrobrewery($name = null): void { $breweryName = $name ?: 'Hipster Brew Co.'; // ... }
好的做法:
你可以使用 類型提示 而且可以保證 $breweryName 不會為空 NULL.
function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void { // ... }
使用 相等運(yùn)算符
不好的做法:
$a = '42'; $b = 42;
使用簡單的相等運(yùn)算符會把字符串類型轉(zhuǎn)換成數(shù)字類型
if( $a != $b ) { //這個(gè)條件表達(dá)式總是會通過 }
表達(dá)式 $a != $b 會返回 false 但實(shí)際上它應(yīng)該是 true !
字符串類型 '42' 是不同于數(shù)字類型的 42
好的做法:
使用全等運(yùn)算符會對比類型和值
if( $a !== $b ) { //這個(gè)條件是通過的 }
表達(dá)式 $a !== $b 會返回 true。
函數(shù)參數(shù)(2 個(gè)或更少)
限制函數(shù)參數(shù)個(gè)數(shù)極其重要
這樣測試你的函數(shù)容易點(diǎn)。有超過 3 個(gè)可選參數(shù)會導(dǎo)致一個(gè)爆炸式組合增長,你會有成噸獨(dú)立參數(shù)情形要測試。
無參數(shù)是理想情況。1 個(gè)或 2 個(gè)都可以,最好避免 3 個(gè)。
再多就需要加固了。通常如果你的函數(shù)有超過兩個(gè)參數(shù),說明他要處理的事太多了。 如果必須要傳入很多數(shù)據(jù),建議封裝一個(gè)高級別對象作為參數(shù)。
不友好的:
function createMenu(string $title, string $body, string $buttonText, bool $cancellable): void { // ... }
友好的:
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): void { // ... }
函數(shù)應(yīng)該只做一件事情
這是迄今為止軟件工程最重要的原則。函數(shù)做了超過一件事情時(shí),它們將變得難以編寫、測試、推導(dǎo)。 而函數(shù)只做一件事情時(shí),重構(gòu)起來則非常簡單,同時(shí)代碼閱讀起來也非常清晰。掌握了這個(gè)原則,你就會領(lǐng)先許多其他的開發(fā)者。
不好的:
function emailClients(array $clients): void { foreach ($clients as $client) { $clientRecord = $db->find($client); if ($clientRecord->isActive()) { email($client); } } }
好的:
function emailClients(array $clients): void { $activeClients = activeClients($clients); array_walk($activeClients, 'email'); } function activeClients(array $clients): array { return array_filter($clients, 'isClientActive'); } function isClientActive(int $client): bool { $clientRecord = $db->find($client); return $clientRecord->isActive(); }
函數(shù)的名稱要說清楚它做什么
不好的例子:
class Email { //... public function handle(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // What is this? A handle for the message? Are we writing to a file now? $message->handle();
很好的例子:
class Email { //... public function send(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // Clear and obvious $message->send();
函數(shù)只能是一個(gè)抽象級別
當(dāng)你有多個(gè)抽象層次時(shí),你的函數(shù)功能通常是做太多了。 分割函數(shù)功能使得重用性和測試更加容易。.
不好:
function parseBetterJSAlternative(string $code): void { $regexes = [ // ... ]; $statements = explode(' ', $code); $tokens = []; foreach ($regexes as $regex) { foreach ($statements as $statement) { // ... } } $ast = []; foreach ($tokens as $token) { // lex... } foreach ($ast as $node) { // parse... } }
同樣不是很好:
我們已經(jīng)完成了一些功能,但是 parseBetterJSAlternative() 功能仍然非常復(fù)雜,測試起來也比較麻煩。
function tokenize(string $code): array { $regexes = [ // ... ]; $statements = explode(' ', $code); $tokens = []; foreach ($regexes as $regex) { foreach ($statements as $statement) { $tokens[] = /* ... */; } } return $tokens; } function lexer(array $tokens): array { $ast = []; foreach ($tokens as $token) { $ast[] = /* ... */; } return $ast; } function parseBetterJSAlternative(string $code): void { $tokens = tokenize($code); $ast = lexer($tokens); foreach ($ast as $node) { // parse... } }
很好的:
最好的解決方案是取出 parseBetterJSAlternative() 函數(shù)的依賴關(guān)系.
class Tokenizer { public function tokenize(string $code): array { $regexes = [ // ... ]; $statements = explode(' ', $code); $tokens = []; foreach ($regexes as $regex) { foreach ($statements as $statement) { $tokens[] = /* ... */; } } return $tokens; } } class Lexer { public function lexify(array $tokens): array { $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(string $code): void { $tokens = $this->tokenizer->tokenize($code); $ast = $this->lexer->lexify($tokens); foreach ($ast as $node) { // parse... } } }
不要用標(biāo)示作為函數(shù)的參數(shù)
標(biāo)示就是在告訴大家,這個(gè)方法里處理很多事。前面剛說過,一個(gè)函數(shù)應(yīng)當(dāng)只做一件事。 把不同標(biāo)示的代碼拆分到多個(gè)函數(shù)里。
不友好的:
function createFile(string $name, bool $temp = false): void { if ($temp) { touch('./temp/'.$name); } else { touch($name); } }
友好的:
function createFile(string $name): void { touch($name); } function createTempFile(string $name): void { touch('./temp/'.$name); }
避免副作用
一個(gè)函數(shù)應(yīng)該只獲取數(shù)值,然后返回另外的數(shù)值,如果在這個(gè)過程中還做了其他的事情,我們就稱為副作用。副作用可能是寫入一個(gè)文件,修改某些全局變量,或者意外的把你全部的錢給了陌生人。
現(xiàn)在,你的確需要在一個(gè)程序或者場合里要有副作用,像之前的例子,你也許需要寫一個(gè)文件。你需要做的是把你做這些的地方集中起來。不要用幾個(gè)函數(shù)和類來寫入一個(gè)特定的文件。只允許使用一個(gè)服務(wù)來單獨(dú)實(shí)現(xiàn)。
重點(diǎn)是避免常見陷阱比如對象間共享無結(jié)構(gòu)的數(shù)據(jù)、使用可以寫入任何的可變數(shù)據(jù)類型、不集中去處理這些副作用。如果你做了這些你就會比大多數(shù)程序員快樂。
不好的:
// 這個(gè)全局變量在函數(shù)中被使用 // 如果我們在別的方法中使用這個(gè)全局變量,有可能我們會不小心將其修改為數(shù)組類型 $name = 'Ryan McDermott'; function splitIntoFirstAndLastName(): void { global $name; $name = explode(' ', $name); } splitIntoFirstAndLastName(); var_dump($name); // ['Ryan', 'McDermott'];
推薦的:
function splitIntoFirstAndLastName(string $name): array { return explode(' ', $name); } $name = 'Ryan McDermott'; $newName = splitIntoFirstAndLastName($name); var_dump($name); // 'Ryan McDermott'; var_dump($newName); // ['Ryan', 'McDermott'];
不要定義全局函數(shù)
在很多語言中定義全局函數(shù)是一個(gè)壞習(xí)慣,因?yàn)槟愣x的全局函數(shù)可能與其他人的函數(shù)庫沖突,并且,除非在實(shí)際運(yùn)用中遇到異常,否則你的 API 的使用者將無法覺察到這一點(diǎn)。接下來我們來看看一個(gè)例子:當(dāng)你想有一個(gè)配置數(shù)組,你可能會寫一個(gè) config() 的全局函數(shù),但是這樣會與其他人定義的庫沖突。
不好的:
function config(): array { return [ 'foo' => 'bar', ] }
好的:
class Configuration { private $configuration = []; public function __construct(array $configuration) { $this->configuration = $configuration; } public function get(string $key): ?string { return isset($this->configuration[$key]) ? $this->configuration[$key] : null; } }
獲取配置需要先創(chuàng)建 Configuration 類的實(shí)例,如下:
$configuration = new Configuration([ 'foo' => 'bar', ]);
現(xiàn)在,在你的應(yīng)用中必須使用 Configuration 的實(shí)例了。
不要使用單例模式
單例模式是個(gè) 反模式。 以下轉(zhuǎn)述 Brian Button 的觀點(diǎn):
單例模式常用于 全局實(shí)例, 這么做為什么不好呢? 因?yàn)樵谀愕拇a里 你隱藏了應(yīng)用的依賴關(guān)系,而沒有通過接口公開依賴關(guān)系 。避免全局的東西擴(kuò)散使用是一種 代碼味道.
單例模式違反了 單一責(zé)任原則: 依據(jù)的事實(shí)就是 單例模式自己控制自身的創(chuàng)建和生命周期.
單例模式天生就導(dǎo)致代碼緊 耦合。這使得在許多情況下用偽造的數(shù)據(jù) 難于測試。
單例模式的狀態(tài)會留存于應(yīng)用的整個(gè)生命周期。 這會對測試產(chǎn)生第二次打擊,你只能讓被嚴(yán)令需要測試的代碼運(yùn)行不了收場,根本不能進(jìn)行單元測試。為何?因?yàn)槊恳粋€(gè)單元測試應(yīng)該彼此獨(dú)立。
還有些來自 Misko Hevery 的深入思考,關(guān)于單例模式的問題根源。
不好的示范:
class DBConnection { private static $instance; private function __construct(string $dsn) { // ... } public static function getInstance(): DBConnection { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } // ... } $singleton = DBConnection::getInstance();
好的示范:
class DBConnection { public function __construct(string $dsn) { // ... } // ... }
用 DSN 進(jìn)行配置創(chuàng)建的 DBConnection 類實(shí)例。
$connection = new DBConnection($dsn);
現(xiàn)在就必須在你的應(yīng)用中使用 DBConnection 的實(shí)例了。
封裝條件語句
不友好的:
if ($article->state === 'published') { // ... }
友好的:
if ($article->isPublished()) { // ... }
避免用反義條件判斷
不友好的:
function isDOMNodeNotPresent(\DOMNode $node): bool { // ... } if (!isDOMNodeNotPresent($node)) { // ... }
友好的:
function isDOMNodePresent(\DOMNode $node): bool { // ... } if (isDOMNodePresent($node)) { // ... }
避免使用條件語句
這聽起來像是個(gè)不可能實(shí)現(xiàn)的任務(wù)。 當(dāng)?shù)谝淮温牭竭@個(gè)時(shí),大部分人都會說,“沒有 if 語句,我該怎么辦?” 答案就是在很多情況下你可以使用多態(tài)性來實(shí)現(xiàn)同樣的任務(wù)。 接著第二個(gè)問題來了, “聽著不錯(cuò),但我為什么需要那樣做?”,這個(gè)答案就是我們之前所學(xué)的干凈代碼概念:一個(gè)函數(shù)應(yīng)該只做一件事情。如果你的類或函數(shù)有 if 語句,這就告訴了使用者你的類或函數(shù)干了不止一件事情。 記住,只要做一件事情。
不好的:
class Airplane { // ... public function getCruisingAltitude(): int { 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(): int; } class Boeing777 implements Airplane { // ... public function getCruisingAltitude(): int { return $this->getMaxAltitude() - $this->getPassengerCount(); } } class AirForceOne implements Airplane { // ... public function getCruisingAltitude(): int { return $this->getMaxAltitude(); } } class Cessna implements Airplane { // ... public function getCruisingAltitude(): int { return $this->getMaxAltitude() - $this->getFuelExpenditure(); } }
避免類型檢測 (第 1 部分)
PHP 是無類型的,這意味著你的函數(shù)可以接受任何類型的參數(shù)。
有時(shí)這種自由會讓你感到困擾,并且他會讓你自然而然的在函數(shù)中使用類型檢測。有很多方法可以避免這么做。
首先考慮 API 的一致性。
不好的:
function travelToTexas($vehicle): void { if ($vehicle instanceof Bicycle) { $vehicle->pedalTo(new Location('texas')); } elseif ($vehicle instanceof Car) { $vehicle->driveTo(new Location('texas')); } }
好的:
function travelToTexas(Traveler $vehicle): void { $vehicle->travelTo(new Location('texas')); }
避免類型檢查(第 2 部分)
如果你正在使用像 字符串、數(shù)值、或數(shù)組這樣的基礎(chǔ)類型,你使用的是 PHP 版本是 PHP 7+,并且你不能使用多態(tài),但仍然覺得需要使用類型檢測,這時(shí),你應(yīng)該考慮 類型定義 或 嚴(yán)格模式。它為您提供了標(biāo)準(zhǔn) PHP 語法之上的靜態(tài)類型。
手動進(jìn)行類型檢查的問題是做這件事需要這么多的額外言辭,你所得到的虛假的『類型安全』并不能彌補(bǔ)丟失的可讀性。保持你的代碼簡潔,編寫良好的測試,并且擁有好的代碼審查。
否則,使用 PHP 嚴(yán)格的類型聲明或嚴(yán)格模式完成所有這些工作。
不好的:
function combine($val1, $val2): int { if (!is_numeric($val1) || !is_numeric($val2)) { throw new \Exception('Must be of type Number'); } return $val1 + $val2; }
好的:
function combine(int $val1, int $val2): int { return $val1 + $val2; }
移除無用代碼
無用代碼和重復(fù)代碼一樣糟糕。 如果沒有被調(diào)用,就應(yīng)該把它刪除掉,沒必要將它保留在你的代碼庫中!當(dāng)你需要它的時(shí)候,可以在你的歷史版本中找到它。
Bad:
function oldRequestModule(string $url): void { // ... } function newRequestModule(string $url): void { // ... } $request = newRequestModule($requestUrl); inventoryTracker('apples', $request, 'www.inventory-awesome.io');
Good:
function requestModule(string $url): void { // ... } $request = requestModule($requestUrl); inventoryTracker('apples', $request, 'www.inventory-awesome.io');
使用對象封裝
在 PHP 中,你可以在方法中使用關(guān)鍵字,如 public, protected and private。
使用它們,你可以任意的控制、修改對象的屬性。
當(dāng)你除獲取對象屬性外還想做更多的操作時(shí),你不需要修改你的代碼
當(dāng) set 屬性時(shí),易于增加參數(shù)驗(yàn)證。
封裝的內(nèi)部表示。
容易在獲取和設(shè)置屬性時(shí)添加日志和錯(cuò)誤處理。
繼承這個(gè)類,你可以重寫默認(rèn)信息。
你可以延遲加載對象的屬性,比如從服務(wù)器獲取數(shù)據(jù)。
此外,這樣的方式也符合 OOP 開發(fā)中的 [開閉原則](# 開閉原則 (OCP))
不好的:
class BankAccount { public $balance = 1000; } $bankAccount = new BankAccount(); // Buy shoes... $bankAccount->balance -= 100;
好的:
class BankAccount { private $balance; public function __construct(int $balance = 1000) { $this->balance = $balance; } public function withdraw(int $amount): void { if ($amount > $this->balance) { throw new \Exception('Amount greater than available balance.'); } $this->balance -= $amount; } public function deposit(int $amount): void { $this->balance += $amount; } public function getBalance(): int { return $this->balance; } } $bankAccount = new BankAccount(); // Buy shoes... $bankAccount->withdraw($shoesPrice); // Get balance $balance = $bankAccount->getBalance();
讓對象擁有 private/protected 屬性的成員
因此,請默認(rèn)使用 private 屬性,只有當(dāng)需要對外部類提供訪問屬性的時(shí)候才采用 public/protected 屬性。
更多的信息可以參考Fabien Potencier 寫的針對這個(gè)專欄的文章blog post .
Bad:
class Employee { public $name; public function __construct(string $name) { $this->name = $name; } } $employee = new Employee('John Doe'); echo 'Employee name: '.$employee->name; // Employee name: John Doe
Good:
class Employee { private $name; public function __construct(string $name) { $this->name = $name; } public function getName(): string { return $this->name; } } $employee = new Employee('John Doe'); echo 'Employee name: '.$employee->getName(); // Employee name: John Doe
組合優(yōu)于繼承
正如 the Gang of Four 所著的 設(shè)計(jì)模式 中所說,
我們應(yīng)該盡量優(yōu)先選擇組合而不是繼承的方式。使用繼承和組合都有很多好處。
這個(gè)準(zhǔn)則的主要意義在于當(dāng)你本能的使用繼承時(shí),試著思考一下組合是否能更好對你的需求建模。
在一些情況下,是這樣的。
接下來你或許會想,“那我應(yīng)該在什么時(shí)候使用繼承?”
答案依賴于你的問題,當(dāng)然下面有一些何時(shí)繼承比組合更好的說明:
糟糕的:
class Employee { private $name; private $email; public function __construct(string $name, string $email) { $this->name = $name; $this->email = $email; } // ... } // 不好,因?yàn)镋mployees "有" taxdata // 而EmployeeTaxData不是Employee類型的 class EmployeeTaxData extends Employee { private $ssn; private $salary; public function __construct(string $name, string $email, string $ssn, string $salary) { parent::__construct($name, $email); $this->ssn = $ssn; $this->salary = $salary; } // ... }
棒棒噠:
class EmployeeTaxData { private $ssn; private $salary; public function __construct(string $ssn, string $salary) { $this->ssn = $ssn; $this->salary = $salary; } // ... } class Employee { private $name; private $email; private $taxData; public function __construct(string $name, string $email) { $this->name = $name; $this->email = $email; } public function setTaxData(string $ssn, string $salary) { $this->taxData = new EmployeeTaxData($ssn, $salary); } // ... }
避免流式接口
流式接口 是一種面向?qū)ο?API 的方法,旨在通過方法鏈 Method chaining 來提高源代碼的可閱讀性.
流式接口雖然需要一些上下文,需要經(jīng)常構(gòu)建對象,但是這種模式減少了代碼的冗余度 (例如: PHPUnit Mock Builder
或 Doctrine Query Builder)
但是同樣它也帶來了很多麻煩:
更多信息可以參考 Marco Pivetta 撰寫的關(guān)于這個(gè)專題的文章blog post
Bad:
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();
Good:
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();
SOLID
SOLID 是 Michael Feathers 推薦的便于記憶的首字母簡寫,它代表了 Robert Martin 命名的最重要的五個(gè)面向?qū)ο缶幊淘O(shè)計(jì)原則:
職責(zé)單一原則 Single Responsibility Principle (SRP)
正如 Clean Code 書中所述,"修改一個(gè)類應(yīng)該只為一個(gè)理由"。人們總是容易去用一堆方法 "塞滿" 一個(gè)類,就好像當(dāng)我們坐飛機(jī)上只能攜帶一個(gè)行李箱時(shí),會把所有的東西都塞到這個(gè)箱子里。這樣做帶來的后果是:從邏輯上講,這樣的類不是高內(nèi)聚的,并且留下了很多以后去修改它的理由。
將你需要修改類的次數(shù)降低到最小很重要,這是因?yàn)?,?dāng)類中有很多方法時(shí),修改某一處,你很難知曉在整個(gè)代碼庫中有哪些依賴于此的模塊會被影響。
比較糟:
class UserSettings { private $user; public function __construct(User $user) { $this->user = $user; } public function changeSettings(array $settings): void { if ($this->verifyCredentials()) { // ... } } private function verifyCredentials(): bool { // ... } }
棒棒噠:
class UserAuth { private $user; public function __construct(User $user) { $this->user = $user; } public function verifyCredentials(): bool { // ... } } class UserSettings { private $user; private $auth; public function __construct(User $user) { $this->user = $user; $this->auth = new UserAuth($user); } public function changeSettings(array $settings): void { if ($this->auth->verifyCredentials()) { // ... } } }
開閉原則 (OCP)
如 Bertrand Meyer 所述,"軟件實(shí)體 (類,模塊,功能,等) 應(yīng)該對擴(kuò)展開放,但對修改關(guān)閉." 這意味著什么?這個(gè)原則大體上是指你應(yīng)該允許用戶在不修改已有代碼情況下添加功能.
壞的:
abstract class Adapter { protected $name; public function getName(): string { return $this->name; } } class AjaxAdapter extends Adapter { public function __construct() { parent::__construct(); $this->name = 'ajaxAdapter'; } } class NodeAdapter extends Adapter { public function __construct() { parent::__construct(); $this->name = 'nodeAdapter'; } } class HttpRequester { private $adapter; public function __construct(Adapter $adapter) { $this->adapter = $adapter; } public function fetch(string $url): Promise { $adapterName = $this->adapter->getName(); if ($adapterName === 'ajaxAdapter') { return $this->makeAjaxCall($url); } elseif ($adapterName === 'httpNodeAdapter') { return $this->makeHttpCall($url); } } private function makeAjaxCall(string $url): Promise { // request and return promise } private function makeHttpCall(string $url): Promise { // request and return promise } }
好的:
interface Adapter { public function request(string $url): Promise; } class AjaxAdapter implements Adapter { public function request(string $url): Promise { // request and return promise } } class NodeAdapter implements Adapter { public function request(string $url): Promise { // request and return promise } } class HttpRequester { private $adapter; public function __construct(Adapter $adapter) { $this->adapter = $adapter; } public function fetch(string $url): Promise { return $this->adapter->request($url); } }
里氏代換原則 (LSP)
這是一個(gè)簡單概念的可怕術(shù)語。它通常被定義為 “如果 S 是 T 的一個(gè)子類型,則 T 型對象可以替換為 S 型對象”
(i.e., S 類型的對象可以替換 T 型對象) 在不改變程序的任何理想屬性的情況下 (正確性,任務(wù)完成度,etc.)." 這是一個(gè)更可怕的定義.
這個(gè)的最佳解釋是,如果你有個(gè)父類和一個(gè)子類,然后父類和子類可以互換使用而不會得到不正確的結(jié)果。這或許依然令人疑惑,所以我們來看下經(jīng)典的正方形 - 矩形例子。幾何定義,正方形是矩形,但是,如果你通過繼承建立了 “IS-a” 關(guān)系的模型,你很快就會陷入麻煩。.
不好的:
class Rectangle { protected $width = 0; protected $height = 0; public function render(int $area): void { // ... } public function setWidth(int $width): void { $this->width = $width; } public function setHeight(int $height): void { $this->height = $height; } public function getArea(): int { return $this->width * $this->height; } } class Square extends Rectangle { public function setWidth(int $width): void { $this->width = $this->height = $width; } public function setHeight(int $height): void { $this->width = $this->height = $height; } } /** * @param Rectangle[] $rectangles */ function renderLargeRectangles(array $rectangles): void { foreach ($rectangles as $rectangle) { $rectangle->setWidth(4); $rectangle->setHeight(5); $area = $rectangle->getArea(); // BAD: Will return 25 for Square. Should be 20. $rectangle->render($area); } } $rectangles = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles($rectangles);
優(yōu)秀的:
abstract class Shape { abstract public function getArea(): int; public function render(int $area): void { // ... } } class Rectangle extends Shape { private $width; private $height; public function __construct(int $width, int $height) { $this->width = $width; $this->height = $height; } public function getArea(): int { return $this->width * $this->height; } } class Square extends Shape { private $length; public function __construct(int $length) { $this->length = $length; } public function getArea(): int { return pow($this->length, 2); } } /** * @param Rectangle[] $rectangles */ function renderLargeRectangles(array $rectangles): void { foreach ($rectangles as $rectangle) { $area = $rectangle->getArea(); $rectangle->render($area); } } $shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; renderLargeRectangles($shapes);
接口隔離原則 (ISP)
ISP 指出 "客戶不應(yīng)該被強(qiáng)制依賴于他們用不到的接口."
一個(gè)好的例子來觀察證實(shí)此原則的是針對需要大量設(shè)置對象的類,不要求客戶端設(shè)置大量的選項(xiàng)是有益的,因?yàn)槎鄶?shù)情況下他們不需要所有的設(shè)置。使他們可選來避免產(chǎn)生一個(gè) “臃腫的接口”.
壞的:
interface Employee { public function work(): void; public function eat(): void; } class Human implements Employee { public function work(): void { // ....working } public function eat(): void { // ...... eating in lunch break } } class Robot implements Employee { public function work(): void { //.... working much more } public function eat(): void { //.... robot can't eat, but it must implement this method } }
好的:
并不是每個(gè)工人都是雇員,但每個(gè)雇員都是工人.
interface Workable { public function work(): void; } interface Feedable { public function eat(): void; } interface Employee extends Feedable, Workable { } class Human implements Employee { public function work(): void { // ....working } public function eat(): void { //.... eating in lunch break } } // robot can only work class Robot implements Workable { public function work(): void { // ....working } }
依賴反轉(zhuǎn)原則 (DIP)
這一原則規(guī)定了兩項(xiàng)基本內(nèi)容:
高級模塊不應(yīng)依賴于低級模塊。兩者都應(yīng)該依賴于抽象.
抽象類不應(yīng)依賴于實(shí)例。實(shí)例應(yīng)該依賴于抽象.
一開始可能很難去理解,但是你如果工作中使用過 php 框架(如 Symfony), 你應(yīng)該見過以依賴的形式執(zhí)行這一原則
依賴注入 (DI). 雖然他們不是相同的概念,DIP 可以讓高級模塊不需要了解其低級模塊的詳細(xì)信息而安裝它們.
通過依賴注入可以做到。這樣做的一個(gè)巨大好處是減少了模塊之間的耦合。耦合是一種非常糟糕的開發(fā)模式,因?yàn)樗鼓拇a難以重構(gòu).
不好的:
class Employee { public function work(): void { // ....working } } class Robot extends Employee { public function work(): void { //.... working much more } } class Manager { private $employee; public function __construct(Employee $employee) { $this->employee = $employee; } public function manage(): void { $this->employee->work(); } }
優(yōu)秀的:
interface Employee { public function work(): void; } class Human implements Employee { public function work(): void { // ....working } } class Robot implements Employee { public function work(): void { //.... working much more } } class Manager { private $employee; public function __construct(Employee $employee) { $this->employee = $employee; } public function manage(): void { $this->employee->work(); } }
試著去遵循 DRY 原則。
盡你最大的努力去避免復(fù)制代碼,它是一種非常糟糕的行為,復(fù)制代碼通常意味著當(dāng)你需要變更一些邏輯時(shí),你需要修改不止一處。
試想一下,如果你在經(jīng)營一家餐廳,并且你需要記錄你倉庫的進(jìn)銷記錄:包括所有的土豆,洋蔥,大蒜,辣椒,等等。如果你使用多個(gè)表格來管理進(jìn)銷記錄,當(dāng)你用其中一些土豆做菜時(shí),你需要更新所有的表格。如果你只有一個(gè)列表的話就只需要更新一個(gè)地方。
通常情況下你復(fù)制代碼的原因可能是它們大多數(shù)都是一樣的,只不過有兩個(gè)或者多個(gè)略微不同的邏輯,但是由于這些區(qū)別,最終導(dǎo)致你寫出了兩個(gè)或者多個(gè)隔離的但大部分相同的方法,移除重復(fù)的代碼意味著用一個(gè) function/module/class 創(chuàng)建一個(gè)能處理差異的抽象。
正確的抽象是非常關(guān)鍵的,這正是為什么你必須學(xué)習(xí)遵守在 Classes 章節(jié)展開討論的的 SOLID 原則,不合理的抽象比復(fù)制代碼更糟糕,所以請務(wù)必謹(jǐn)慎!說了這么多,如果你能設(shè)計(jì)一個(gè)合理的抽象,就去實(shí)現(xiàn)它!最后再說一遍,不要寫重復(fù)代碼,否則你會發(fā)現(xiàn)當(dāng)你想修改一個(gè)邏輯時(shí),你必須去修改多個(gè)地方!
糟糕的:
function showDeveloperList(array $developers): void { foreach ($developers as $developer) { $expectedSalary = $developer->calculateExpectedSalary(); $experience = $developer->getExperience(); $githubLink = $developer->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); } } function showManagerList(array $managers): void { foreach ($managers as $manager) { $expectedSalary = $manager->calculateExpectedSalary(); $experience = $manager->getExperience(); $githubLink = $manager->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); } }
好的:
function showList(array $employees): void { foreach ($employees as $employee) { $expectedSalary = $employee->calculateExpectedSalary(); $experience = $employee->getExperience(); $githubLink = $employee->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); } }
非常好:
最好讓你的代碼緊湊一點(diǎn)。
function showList(array $employees): void { foreach ($employees as $employee) { render([ $employee->calculateExpectedSalary(), $employee->getExperience(), $employee->getGithubLink() ]); } }
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
標(biāo)簽:郴州 金華 寶雞 佳木斯 香港 通化 自貢 阿克蘇
巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《PHP 代碼簡潔之道(小結(jié))》,本文關(guān)鍵詞 PHP,代碼,簡潔,之道,小結(jié),;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。