文章出處 github.com/php-cpm/cle…javascript
本文參考自 Robert C. Martin的Clean Code 書中的軟件工程師的原則 ,適用於PHP。 這不是風格指南。 這是一個關於開發可讀、可複用而且可重構的PHP軟件指南。php
並非這裏全部的原則都得遵循,甚至不多的能被廣泛接受。 這些雖然只是指導,可是都是Clean Code做者多年總結出來的。html
本文受到 clean-code-javascript 的啓發java
雖然不少開發者還在使用PHP5,可是本文中的大部分示例的運行環境須要PHP 7.1+。node
翻譯完成度100%,最後更新時間2017-12-25。本文由 php-cpm 基於 yangweijie版本 的clean-code-php翻譯並同步大量原文內容。git
原文更新頻率較高,個人翻譯方法是直接用文本比較工具逐行對比。優先保證文字內容是最新的,再逐步提高翻譯質量。程序員
閱讀過程當中若是遇到各類連接失效、內容老舊、術語使用錯誤和其餘翻譯錯誤等問題,歡迎你們積極提交PR。github
壞:ajax
$ymdstr = $moment->format('y-m-d');
複製代碼
好:正則表達式
$currentDate = $moment->format('y-m-d');
複製代碼
壞:
getUserInfo();
getUserData();
getUserRecord();
getUserProfile();
複製代碼
好:
getUser();
複製代碼
寫代碼是用來讀的。因此寫出可讀性高、便於搜索的代碼相當重要。 命名變量時若是沒有有意義、很差理解,那就是在傷害讀者。 請讓你的代碼便於搜索。
壞:
// What the heck is 448 for?
$result = $serializer->serialize($data, 448);
複製代碼
好:
$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
複製代碼
壞:
// What the heck is 4 for?
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);
[, $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']);
複製代碼
太多的if else語句一般會致使你的代碼難以閱讀,直白優於隱晦
糟糕:
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);
}
複製代碼
別讓讀你的代碼的人猜你寫的變量是什麼意思。 寫清楚好過模糊不清。
壞:
$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.'): void {
// ...
}
複製代碼
還行:
比上一個好理解一些,但最好能控制變量的值
function createMicrobrewery($name = null): void {
$breweryName = $name ?: 'Hipster Brew Co.';
// ...
}
複製代碼
好:
若是你的程序只支持 PHP 7+, 那你能夠用 type hinting 保證變量 $breweryName
不是 NULL
.
function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void {
// ...
}
複製代碼
很差:
簡易對比會將字符串轉爲整形
$a = '42';
$b = 42;
if( $a != $b ) {
//這裏始終執行不到
}
複製代碼
對比 b 返回了 FALSE
但應該返回 TRUE
! 字符串 '42' 跟整數 42 不相等
好:
使用恆等判斷檢查類型和數據
$a = '42';
$b = 42;
if ($a !== $b) {
// The expression is verified
}
複製代碼
The comparison $a !== $b
returns TRUE
.
限制函數參數個數極其重要,這樣測試你的函數容易點。有超過3個可選參數參數致使一個爆炸式組合增加,你會有成噸獨立參數情形要測試。
無參數是理想狀況。1個或2個均可以,最好避免3個。再多就須要加固了。一般若是你的函數有超過兩個參數,說明他要處理的事太多了。 若是必需要傳入不少數據,建議封裝一個高級別對象做爲參數。
壞:
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 {
// ...
}
複製代碼
這是迄今爲止軟件工程裏最重要的一個規則。當一個函數作超過一件事的時候,他們就難於實現、測試和理解。當你把一個函數拆分到只剩一個功能時,他們就容易被重構,而後你的代碼讀起來就更清晰。若是你光遵循這條規則,你就領先於大多數開發者了。
壞:
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();
}
複製代碼
壞:
class Email {
//...
public function handle(): void {
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 啥?handle處理一個消息幹嗎了?是往一個文件裏寫嗎?
$message->handle();
複製代碼
好:
class Email {
//...
public function send(): void {
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 簡單明瞭
$message->send();
複製代碼
當你抽象層次過多時時,函數處理的事情太多了。須要拆分功能來提升可重用性和易用性,以便簡化測試。 (譯者注:這裏從示例代碼看應該是指嵌套過多)
壞:
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...
}
}
複製代碼
壞:
咱們把一些方法從循環中提取出來,可是parseBetterJSAlternative()
方法仍是很複雜,並且不利於測試。
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) {
// 解析邏輯...
}
}
複製代碼
好:
最好的解決方案是把 parseBetterJSAlternative()
方法的依賴移除。
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) {
// 解析邏輯...
}
}
}
複製代碼
這樣咱們能夠對依賴作mock,並測試BetterJSAlternative::parse()
運行是否符合預期。
flag就是在告訴你們,這個方法裏處理不少事。前面剛說過,一個函數應當只作一件事。 把不一樣flag的代碼拆分到多個函數裏。
壞:
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);
}
複製代碼
一個函數作了比獲取一個值而後返回另一個值或值們會產生反作用若是。反作用多是寫入一個文件,修改某些全局變量或者偶然的把你所有的錢給了陌生人。
如今,你的確須要在一個程序或者場合裏要有反作用,像以前的例子,你也許須要寫一個文件。你想要作的是把你作這些的地方集中起來。不要用幾個函數和類來寫入一個特定的文件。用一個服務來作它,一個只有一個。
重點是避免常見陷阱好比對象間共享無結構的數據,使用能夠寫入任何的可變數據類型,不集中處理反作用發生的地方。若是你作了這些你就會比大多數程序員快樂。
壞:
// 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(): 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'];
複製代碼
在大多數語言中污染全局變量是一個壞的實踐,由於你可能和其餘類庫衝突 而且調用你api的人直到他們捕獲異常才知道踩坑了。讓咱們思考一種場景: 若是你想配置一個數組,你可能會寫一個全局函數config()
,可是他可能 和試着作一樣事的其餘類庫衝突。
壞:
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;
}
}
複製代碼
加載配置並建立 Configuration
類的實例
$configuration = new Configuration([
'foo' => 'bar',
]);
複製代碼
如今你必須在程序中用 Configuration
的實例了
單例是一種 反模式. 如下是解釋:Paraphrased from Brian Button:
這裏有一篇很是好的討論單例模式的[根本問題((misko.hevery.com/2008/08/25/…)的文章,是Misko Hevery 寫的。
壞:
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) {
// ...
}
// ...
}
複製代碼
建立 DBConnection
類的實例並經過 DSN 配置.
$connection = new DBConnection($dsn);
複製代碼
如今你必須在程序中 使用 DBConnection
的實例了
壞:
if ($article->state === 'published') {
// ...
}
複製代碼
好:
if ($article->isPublished()) {
// ...
}
複製代碼
壞:
function isDOMNodeNotPresent(\DOMNode $node): bool {
// ...
}
if (!isDOMNodeNotPresent($node))
{
// ...
}
複製代碼
好:
function isDOMNodePresent(\DOMNode $node): bool {
// ...
}
if (isDOMNodePresent($node)) {
// ...
}
複製代碼
這看起來像一個不可能任務。當人們第一次聽到這句話是都會這麼說。 "沒有if語句
我還能作啥?" 答案是你可使用多態來實現多種場景 的相同任務。第二個問題很常見, 「這麼作能夠,但爲何我要這麼作?」 答案是前面咱們學過的一個Clean Code原則:一個函數應當只作一件事。 當你有不少含有if
語句的類和函數時,你的函數作了不止一件事。 記住,只作一件事。
壞:
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();
}
}
複製代碼
PHP是弱類型的,這意味着你的函數能夠接收任何類型的參數。 有時候你爲這自由所痛苦而且在你的函數漸漸嘗試類型檢查。 有不少方法去避免這麼作。第一種是統一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'));
}
複製代碼
若是你正使用基本原始值好比字符串、整形和數組,要求版本是PHP 7+,不用多態,須要類型檢測, 那你應當考慮類型聲明或者嚴格模式。 提供了基於標準PHP語法的靜態類型。 手動檢查類型的問題是作好了須要好多的廢話,好像爲了安全就能夠不顧損失可讀性。 保持你的PHP 整潔,寫好測試,作好代碼回顧。作不到就用PHP嚴格類型聲明和嚴格模式來確保安全。
壞:
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;
}
複製代碼
殭屍代碼和重複代碼同樣壞。沒有理由保留在你的代碼庫中。若是歷來沒被調用過,就刪掉! 由於還在代碼版本庫裏,所以很安全。
壞:
function oldRequestModule(string $url): void {
// ...
}
function newRequestModule(string $url): void {
// ...
}
$request = newRequestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');
複製代碼
好:
function requestModule(string $url): void {
// ...
}
$request = requestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');
複製代碼
在PHP中你能夠對方法使用public
, protected
, private
來控制對象屬性的變動。
set
對應的屬性方法時,易於增長參數的驗證此外,這樣的方式也符合OOP開發中的開閉原則
壞:
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();
複製代碼
public
方法和屬性進行修改很是危險,由於外部代碼容易依賴他,而你沒辦法控制。對之修改影響全部這個類的使用者。 public
methods and properties are most dangerous for changes, because some outside code may easily rely on them and you can't control what code relies on them. Modifications in class are dangerous for all users of class.protected
的修改跟對public
修改差很少危險,由於他們對子類可用,他倆的惟一區別就是可調用的位置不同,對之修改影響全部集成這個類的地方。 protected
modifier are as dangerous as public, because they are available in scope of any child class. This effectively means that difference between public and protected is only in access mechanism, but encapsulation guarantee remains the same. Modifications in class are dangerous for all descendant classes.private
的修改保證了這部分代碼只會影響當前類private
modifier guarantees that code is dangerous to modify only in boundaries of single class (you are safe for modifications and you won't have Jenga effect).因此,當你須要控制類裏的代碼能夠被訪問時才用public/protected
,其餘時候都用private
。
能夠讀一讀這篇 博客文章 ,Fabien Potencier寫的.
壞:
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
複製代碼
好:
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
複製代碼
正如 the Gang of Four 所著的設計模式以前所說, 咱們應該儘可能優先選擇組合而不是繼承的方式。使用繼承和組合都有不少好處。 這個準則的主要意義在於當你本能的使用繼承時,試着思考一下組合
是否能更好對你的需求建模。 在一些狀況下,是這樣的。
接下來你或許會想,「那我應該在何時使用繼承?」 答案依賴於你的問題,固然下面有一些什麼時候繼承比組合更好的說明:
糟糕的:
class Employee {
private $name;
private $email;
public function __construct(string $name, string $email) {
$this->name = $name;
$this->email = $email;
}
// ...
}
// 很差,由於 Employees "有" 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);
}
// ...
}
複製代碼
連貫接口Fluent interface是一種 旨在提升面向對象編程時代碼可讀性的API設計模式,他基於方法鏈Method chaining
有上下文的地方能夠下降代碼複雜度,例如PHPUnit Mock Builder 和Doctrine Query Builder ,更多的狀況會帶來較大代價:
While there can be some contexts, frequently builder objects, where this pattern reduces the verbosity of the code (for example the PHPUnit Mock Builder or the Doctrine Query Builder), more often it comes at some costs:
瞭解更多請閱讀 連貫接口爲何很差 ,做者 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();
複製代碼
能用時儘可能使用 final
關鍵字:
The only condition is that your class should implement an interface and no other public methods are defined.
For more informations you can read the blog post on this topic written by Marco Pivetta (Ocramius).
Bad:
final class Car {
private $color;
public function __construct($color) {
$this->color = $color;
}
/** * @return string The color of the vehicle */
public function getColor() {
return $this->color;
}
}
複製代碼
Good:
interface Vehicle {
/** * @return string The color of the vehicle */
public function getColor();
}
final class Car implements Vehicle {
private $color;
public function __construct($color) {
$this->color = $color;
}
/** * {@inheritdoc} */
public function getColor() {
return $this->color;
}
}
複製代碼
SOLID 是Michael Feathers推薦的便於記憶的首字母簡寫,它表明了Robert Martin命名的最重要的五個面對對象編碼設計原則
Single Responsibility Principle (SRP)
正如在Clean Code所述,"修改一個類應該只爲一個理由"。 人們老是易於用一堆方法塞滿一個類,如同咱們只能在飛機上 只能攜帶一個行李箱(把全部的東西都塞到箱子裏)。這樣作 的問題是:從概念上這樣的類不是高內聚的,而且留下了不少 理由去修改它。將你須要修改類的次數下降到最小很重要。 這是由於,當有不少方法在類中時,修改其中一處,你很難知 曉在代碼庫中哪些依賴的模塊會被影響到。
壞:
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()) {
// ...
}
}
}
複製代碼
Open/Closed Principle (OCP)
正如Bertrand Meyer所述,"軟件的工件( classes, modules, functions 等) 應該對擴展開放,對修改關閉。" 然而這句話意味着什麼呢?這個原則大致上表示你 應該容許在不改變已有代碼的狀況下增長新的功能
壞:
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);
}
}
複製代碼
Liskov Substitution Principle (LSP)
這是一個簡單的原則,卻用了一個很差理解的術語。它的正式定義是 "若是S是T的子類型,那麼在不改變程序原有既定屬性(檢查、執行 任務等)的前提下,任何T類型的對象均可以使用S類型的對象替代 (例如,使用S的對象能夠替代T的對象)" 這個定義更難理解:-)。
對這個概念最好的解釋是:若是你有一個父類和一個子類,在不改變 原有結果正確性的前提下父類和子類能夠互換。這個聽起來依舊讓人 有些迷惑,因此讓咱們來看一個經典的正方形-長方形的例子。從數學 上講,正方形是一種長方形,可是當你的模型經過繼承使用了"is-a" 的關係時,就不對了。
壞:
class Rectangle {
protected $width = 0;
protected $height = 0;
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;
}
}
function printArea(Rectangle $rectangle): void {
$rectangle->setWidth(4);
$rectangle->setHeight(5);
// BAD: Will return 25 for Square. Should be 20.
echo sprintf('%s has area %d.', get_class($rectangle), $rectangle->getArea()).PHP_EOL;
}
$rectangles = [new Rectangle(), new Square()];
foreach ($rectangles as $rectangle) {
printArea($rectangle);
}
複製代碼
好:
最好是將這兩種四邊形分別對待,用一個適合兩種類型的更通用子類型來代替。
儘管正方形和長方形看起來很類似,但他們是不一樣的。 正方形更接近菱形,而長方形更接近平行四邊形。但他們不是子類型。 儘管類似,正方形、長方形、菱形、平行四邊形都是有本身屬性的不一樣形狀。
interface Shape {
public function getArea(): int;
}
class Rectangle implements Shape {
private $width = 0;
private $height = 0;
public function __construct(int $width, int $height) {
$this->width = $width;
$this->height = $height;
}
public function getArea(): int {
return $this->width * $this->height;
}
}
class Square implements Shape {
private $length = 0;
public function __construct(int $length) {
$this->length = $length;
}
public function getArea(): int {
return $this->length ** 2;
}
}
function printArea(Shape $shape): void {
echo sprintf('%s has area %d.', get_class($shape), $shape->getArea()).PHP_EOL;
}
$shapes = [new Rectangle(4, 5), new Square(5)];
foreach ($shapes as $shape) {
printArea($shape);
}
複製代碼
Interface Segregation Principle (ISP)
接口隔離原則表示:"調用方不該該被強制依賴於他不須要的接口"
有一個清晰的例子來講明示範這條原則。當一個類須要一個大量的設置項, 爲了方便不會要求調用方去設置大量的選項,由於在一般他們不須要全部的 設置項。使設置項可選有助於咱們避免產生"胖接口"
壞:
interface Employee {
public function work(): void;
public function eat(): void;
}
class HumanEmployee implements Employee {
public function work(): void {
// ....working
}
public function eat(): void {
// ...... eating in lunch break
}
}
class RobotEmployee implements Employee {
public function work(): void {
//.... working much more
}
public function eat(): void {
//.... robot can't eat, but it must implement this method
}
}
複製代碼
好:
不是每個工人都是僱員,可是每個僱員都是一個工人
interface Workable {
public function work(): void;
}
interface Feedable {
public function eat(): void;
}
interface Employee extends Feedable, Workable {
}
class HumanEmployee implements Employee {
public function work(): void {
// ....working
}
public function eat(): void {
//.... eating in lunch break
}
}
// robot can only work
class RobotEmployee implements Workable {
public function work(): void {
// ....working
}
}
複製代碼
Dependency Inversion Principle (DIP)
這條原則說明兩個基本的要點:
這條起初看起來有點晦澀難懂,可是若是你使用過 PHP 框架(例如 Symfony),你應該見過 依賴注入(DI),它是對這個概念的實現。雖然它們不是徹底相等的概念,依賴倒置原則使高階模塊 與低階模塊的實現細節和建立分離。可使用依賴注入(DI)這種方式來實現它。最大的好處 是它使模塊之間解耦。耦合會致使你難於重構,它是一種很是糟糕的的開發模式。
壞:
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();
}
}
複製代碼
好:
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 原則.
盡你最大的努力去避免複製代碼,它是一種很是糟糕的行爲,複製代碼 一般意味着當你須要變動一些邏輯時,你須要修改不止一處。
試想一下,若是你在經營一家餐廳而且你在記錄你倉庫的進銷記錄:全部 的土豆,洋蔥,大蒜,辣椒等。若是你有多個列表來管理進銷記錄,當你 用其中一些土豆作菜時你須要更新全部的列表。若是你只有一個列表的話 只有一個地方須要更新。
一般狀況下你複製代碼是應該有兩個或者多個略微不一樣的邏輯,它們大多數 都是同樣的,可是因爲它們的區別導致你必須有兩個或者多個隔離的但大部 分相同的方法,移除重複的代碼意味着用一個function/module/class創 建一個能處理差別的抽象。
用對抽象很是關鍵,這正是爲何你必須學習遵照在類章節寫 的SOLID原則,不合理的抽象比複製代碼更糟糕,因此務必謹慎!說了這麼多, 若是你能設計一個合理的抽象,那就這麼幹!別寫重複代碼,不然你會發現 任什麼時候候當你想修改一個邏輯時你必須修改多個地方。
壞:
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);
}
}
複製代碼
極好:
最好讓代碼緊湊一點
function showList(array $employees): void {
foreach ($employees as $employee) {
render([
$employee->calculateExpectedSalary(),
$employee->getExperience(),
$employee->getGithubLink()
]);
}
}
複製代碼