前言
Clean Code PHP
目錄
- 介紹
- 變量
- 表達式
- 函數(shù)
- 對象和數(shù)據(jù)結(jié)構(gòu) Objects and Data Structures
- 類
- 類的SOLID原則 SOLID
- 別寫重復代碼 (DRY)
- 翻譯
介紹
本文參考自 Robert C. Martin的Clean Code 書中的軟件工程師的原則
,適用于PHP蚓耽。 這不是風格指南菱父。 這是一個關(guān)于開發(fā)可讀仪搔、可復用并且可重構(gòu)的PHP軟件指南盔憨。
并不是這里所有的原則都得遵循洗搂,甚至很少的能被普遍接受热康。 這些雖然只是指導沛申,但是都是Clean Code作者多年總結(jié)出來的。
本文受到 clean-code-javascript 的啟發(fā)
雖然很多開發(fā)者還在使用PHP5姐军,但是本文中的大部分示例的運行環(huán)境需要PHP 7.1+铁材。
翻譯說明
翻譯完成度100%尖淘,最后更新時間2017-12-25。本文由 php-cpm 基于 yangweijie版本 的clean-code-php翻譯并同步大量原文內(nèi)容著觉。
原文更新頻率較高村生,我的翻譯方法是直接用文本比較工具逐行對比。優(yōu)先保證文字內(nèi)容是最新的饼丘,再逐步提升翻譯質(zhì)量趁桃。
閱讀過程中如果遇到各種鏈接失效、內(nèi)容老舊肄鸽、術(shù)語使用錯誤和其他翻譯錯誤等問題镇辉,歡迎大家積極提交PR。
變量
使用見字知意的變量名
壞:
$ymdstr = $moment->format('y-m-d');
好:
$currentDate = $moment->format('y-m-d');
同一個實體要用相同的變量名
壞:
getUserInfo();
getUserData();
getUserRecord();
getUserProfile();
好:
getUser();
使用便于搜索的名稱 (part 1)
寫代碼是用來讀的贴捡。所以寫出可讀性高忽肛、便于搜索的代碼至關(guān)重要。
命名變量時如果沒有有意義烂斋、不好理解屹逛,那就是在傷害讀者。
請讓你的代碼便于搜索汛骂。
壞:
// What the heck is 448 for?
$result = $serializer->serialize($data, 448);
好:
$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
使用便于搜索的名稱 (part 2)
壞:
// 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);
好:
使用帶名字的子規(guī)則,不用懂正則也能看的懂
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(?<city>.+?)\s*(?<zipCode>\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches['city'], $matches['zipCode']);
避免深層嵌套帘瞭,盡早返回 (part 1)
太多的if else語句通常會導致你的代碼難以閱讀淑掌,直白優(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);
}
避免深層嵌套,盡早返回 (part 2)
糟糕的:
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);
}
不要添加不必要上下文
如果從你的類名、對象名已經(jīng)可以得知一些信息媒殉,就別再在變量名里重復担敌。
壞:
class Car
{
public $carMake;
public $carModel;
public $carColor;
//...
}
好:
class Car
{
public $make;
public $model;
public $color;
//...
}
合理使用參數(shù)默認值,沒必要在方法里再做默認值檢測
不好:
不好廷蓉,$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
{
// ...
}
表達式
使用恒等式
不好:
簡易對比會將字符串轉(zhuǎn)為整形
$a = '42';
$b = 42;
if( $a != $b ) {
//這里始終執(zhí)行不到
}
對比 b 返回了
FALSE
但應(yīng)該返回 TRUE
!
字符串 '42' 跟整數(shù) 42 不相等
好:
使用恒等判斷檢查類型和數(shù)據(jù)
$a = '42';
$b = 42;
if ($a !== $b) {
// The expression is verified
}
The comparison $a !== $b
returns TRUE
.
函數(shù)
函數(shù)參數(shù)(最好少于2個)
限制函數(shù)參數(shù)個數(shù)極其重要,這樣測試你的函數(shù)容易點桃犬。有超過3個可選參數(shù)參數(shù)導致一個爆炸式組合增長刹悴,你會有成噸獨立參數(shù)情形要測試。
無參數(shù)是理想情況攒暇。1個或2個都可以土匀,最好避免3個。再多就需要加固了扯饶。通常如果你的函數(shù)有超過兩個參數(shù)恒削,說明他要處理的事太多了池颈。 如果必須要傳入很多數(shù)據(jù),建議封裝一個高級別對象作為參數(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)該只做一件事
這是迄今為止軟件工程里最重要的一個規(guī)則躯砰。當一個函數(shù)做超過一件事的時候,他們就難于實現(xiàn)携丁、測試和理解琢歇。當你把一個函數(shù)拆分到只剩一個功能時,他們就容易被重構(gòu)梦鉴,然后你的代碼讀起來就更清晰李茫。如果你光遵循這條規(guī)則,你就領(lǐng)先于大多數(shù)開發(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ù)名應(yīng)體現(xiàn)他做了什么事
壞:
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();
函數(shù)里應(yīng)當只有一層抽象abstraction
當你抽象層次過多時時宠互,函數(shù)處理的事情太多了。需要拆分功能來提高可重用性和易用性椭坚,以便簡化測試予跌。
(譯者注:這里從示例代碼看應(yīng)該是指嵌套過多)
壞:
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...
}
}
壞:
我們把一些方法從循環(huán)中提取出來,但是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()
運行是否符合預(yù)期烁焙。
不要用flag作為函數(shù)的參數(shù)
flag就是在告訴大家,這個方法里處理很多事集币。前面剛說過考阱,一個函數(shù)應(yīng)當只做一件事。 把不同flag的代碼拆分到多個函數(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);
}
避免副作用
一個函數(shù)做了比獲取一個值然后返回另外一個值或值們會產(chǎn)生副作用如果。副作用可能是寫入一個文件秽之,修改某些全局變量或者偶然的把你全部的錢給了陌生人当娱。
現(xiàn)在,你的確需要在一個程序或者場合里要有副作用考榨,像之前的例子跨细,你也許需要寫一個文件。你想要做的是把你做這些的地方集中起來河质。不要用幾個函數(shù)和類來寫入一個特定的文件冀惭。用一個服務(wù)來做它震叙,一個只有一個。
重點是避免常見陷阱比如對象間共享無結(jié)構(gòu)的數(shù)據(jù)散休,使用可以寫入任何的可變數(shù)據(jù)類型媒楼,不集中處理副作用發(fā)生的地方。如果你做了這些你就會比大多數(shù)程序員快樂戚丸。
壞:
// 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'];
不要寫全局函數(shù)
在大多數(shù)語言中污染全局變量是一個壞的實踐划址,因為你可能和其他類庫沖突
并且調(diào)用你api的人直到他們捕獲異常才知道踩坑了。讓我們思考一種場景:
如果你想配置一個數(shù)組限府,你可能會寫一個全局函數(shù)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;
}
}
加載配置并創(chuàng)建 Configuration
類的實例
$configuration = new Configuration([
'foo' => 'bar',
]);
現(xiàn)在你必須在程序中用 Configuration
的實例了
不要使用單例模式
單例是一種 反模式. 以下是解釋:Paraphrased from Brian Button:
- 總是被用成全局實例胁勺。They are generally used as a global instance, why is that so bad? Because you hide the dependencies of your application in your code, instead of exposing them through the interfaces. Making something global to avoid passing it around is a code smell.
- 違反了單一響應(yīng)原則They violate the single responsibility principle: by virtue of the fact that they control their own creation and lifecycle.
- 導致代碼強耦合They inherently cause code to be tightly coupled. This makes faking them out under test rather difficult in many cases.
- 在整個程序的生命周期中始終攜帶狀態(tài)世澜。They carry state around for the lifetime of the application. Another hit to testing since you can end up with a situation where tests need to be ordered which is a big no for unit tests. Why? Because each unit test should be independent from the other.
這里有一篇非常好的討論單例模式的[根本問題((http://misko.hevery.com/2008/08/25/root-cause-of-singletons/)的文章,是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)
{
// ...
}
// ...
}
創(chuàng)建 DBConnection
類的實例并通過 DSN 配置.
$connection = new DBConnection($dsn);
現(xiàn)在你必須在程序中 使用 DBConnection
的實例了
封裝條件語句
壞:
if ($article->state === 'published') {
// ...
}
好:
if ($article->isPublished()) {
// ...
}
避免用反義條件判斷
壞:
function isDOMNodeNotPresent(\DOMNode $node): bool
{
// ...
}
if (!isDOMNodeNotPresent($node))
{
// ...
}
好:
function isDOMNodePresent(\DOMNode $node): bool
{
// ...
}
if (isDOMNodePresent($node)) {
// ...
}
避免條件判斷
這看起來像一個不可能任務(wù)寥裂。當人們第一次聽到這句話是都會這么說。
"沒有if語句
我還能做啥蛇捌?" 答案是你可以使用多態(tài)來實現(xiàn)多種場景
的相同任務(wù)抚恒。第二個問題很常見, “這么做可以络拌,但為什么我要這么做俭驮?”
答案是前面我們學過的一個Clean Code原則:一個函數(shù)應(yīng)當只做一件事。
當你有很多含有if
語句的類和函數(shù)時,你的函數(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();
}
}
避免類型檢查 (part 1)
PHP是弱類型的,這意味著你的函數(shù)可以接收任何類型的參數(shù)萍恕。
有時候你為這自由所痛苦并且在你的函數(shù)漸漸嘗試類型檢查逸嘀。
有很多方法去避免這么做。第一種是統(tǒng)一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'));
}
避免類型檢查 (part 2)
如果你正使用基本原始值比如字符串崭倘、整形和數(shù)組,要求版本是PHP 7+类垫,不用多態(tài)司光,需要類型檢測,
那你應(yīng)當考慮類型聲明或者嚴格模式悉患。
提供了基于標準PHP語法的靜態(tài)類型残家。 手動檢查類型的問題是做好了需要好多的廢話,好像為了安全就可以不顧損失可讀性售躁。
保持你的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;
}
移除僵尸代碼
僵尸代碼和重復代碼一樣壞诺擅。沒有理由保留在你的代碼庫中。如果從來沒被調(diào)用過毫玖,就刪掉掀虎!
因為還在代碼版本庫里,因此很安全付枫。
壞:
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');
對象和數(shù)據(jù)結(jié)構(gòu)
使用 getters 和 setters
在PHP中你可以對方法使用public
, protected
, private
來控制對象屬性的變更烹玉。
- 當你想對對象屬性做獲取之外的操作時,你不需要在代碼中去尋找并修改每一個該屬性訪問方法
- 當有
set
對應(yīng)的屬性方法時阐滩,易于增加參數(shù)的驗證 - 封裝內(nèi)部的表示
- 使用set和get時二打,易于增加日志和錯誤控制
- 繼承當前類時,可以復寫默認的方法功能
- 當對象屬性是從遠端服務(wù)器獲取時掂榔,get继效,set易于使用延遲加載
此外,這樣的方式也符合OOP開發(fā)中的開閉原則
壞:
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
修改差不多危險凡简,因為他們對子類可用,他倆的唯一區(qū)別就是可調(diào)用的位置不一樣精肃,對之修改影響所有集成這個類的地方秤涩。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 所著的設(shè)計模式之前所說习柠,
我們應(yīng)該盡量優(yōu)先選擇組合而不是繼承的方式匀谣。使用繼承和組合都有很多好處。
這個準則的主要意義在于當你本能的使用繼承時资溃,試著思考一下組合
是否能更好對你的需求建模振定。
在一些情況下,是這樣的肉拓。
接下來你或許會想,“那我應(yīng)該在什么時候使用繼承梳庆?”
答案依賴于你的問題暖途,當然下面有一些何時繼承比組合更好的說明:
- 你的繼承表達了“是一個”而不是“有一個”的關(guān)系(人類-》動物卑惜,用戶-》用戶詳情)
- 你可以復用基類的代碼(人類可以像動物一樣移動)
- 你想通過修改基類對所有派生類做全局的修改(當動物移動時,修改她們的能量消耗)
糟糕的:
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是一種
旨在提高面向?qū)ο缶幊虝r代碼可讀性的API設(shè)計模式露久,他基于方法鏈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 類
能用時盡量使用 final
關(guān)鍵字:
- 阻止不受控的繼承鏈
- 鼓勵 組合.
- 鼓勵 單一職責模式.
- 鼓勵開發(fā)者用你的公開方法而非通過繼承類獲取受保護方法的訪問權(quán)限.
- 使得在不破壞使用你的類的應(yīng)用的情況下修改代碼成為可能.
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
SOLID 是Michael Feathers推薦的便于記憶的首字母簡寫,它代表了Robert Martin命名的最重要的五個面對對象編碼設(shè)計原則
單一職責原則
Single Responsibility Principle (SRP)
正如在Clean Code所述迟几,"修改一個類應(yīng)該只為一個理由"消请。
人們總是易于用一堆方法塞滿一個類,如同我們只能在飛機上
只能攜帶一個行李箱(把所有的東西都塞到箱子里)类腮。這樣做
的問題是:從概念上這樣的類不是高內(nèi)聚的臊泰,并且留下了很多
理由去修改它。將你需要修改類的次數(shù)降低到最小很重要蚜枢。
這是因為缸逃,當有很多方法在類中時,修改其中一處厂抽,你很難知
曉在代碼庫中哪些依賴的模塊會被影響到需频。
壞:
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 等)
應(yīng)該對擴展開放筷凤,對修改關(guān)閉昭殉。" 然而這句話意味著什么呢?這個原則大體上表示你
應(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);
}
}
里氏替換原則
Liskov Substitution Principle (LSP)
這是一個簡單的原則嵌施,卻用了一個不好理解的術(shù)語饲化。它的正式定義是
"如果S是T的子類型,那么在不改變程序原有既定屬性(檢查吗伤、執(zhí)行
任務(wù)等)的前提下吃靠,任何T類型的對象都可以使用S類型的對象替代
(例如,使用S的對象可以替代T的對象)" 這個定義更難理解:-)足淆。
對這個概念最好的解釋是:如果你有一個父類和一個子類巢块,在不改變
原有結(jié)果正確性的前提下父類和子類可以互換。這個聽起來依舊讓人
有些迷惑巧号,所以讓我們來看一個經(jīng)典的正方形-長方形的例子族奢。從數(shù)學
上講,正方形是一種長方形丹鸿,但是當你的模型通過繼承使用了"is-a"
的關(guān)系時越走,就不對了。
壞:
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)
接口隔離原則表示:"調(diào)用方不應(yīng)該被強制依賴于他不需要的接口"
有一個清晰的例子來說明示范這條原則。當一個類需要一個大量的設(shè)置項蔽豺,
為了方便不會要求調(diào)用方去設(shè)置大量的選項区丑,因為在通常他們不需要所有的
設(shè)置項。使設(shè)置項可選有助于我們避免產(chǎn)生"胖接口"
壞:
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)
這條原則說明兩個基本的要點:
- 高階的模塊不應(yīng)該依賴低階的模塊沧侥,它們都應(yīng)該依賴于抽象
- 抽象不應(yīng)該依賴于實現(xiàn),實現(xiàn)應(yīng)該依賴于抽象
這條起初看起來有點晦澀難懂魄鸦,但是如果你使用過 PHP 框架(例如 Symfony)宴杀,你應(yīng)該見過
依賴注入(DI),它是對這個概念的實現(xiàn)拾因。雖然它們不是完全相等的概念旺罢,依賴倒置原則使高階模塊
與低階模塊的實現(xiàn)細節(jié)和創(chuàng)建分離【罴牵可以使用依賴注入(DI)這種方式來實現(xiàn)它扁达。最大的好處
是它使模塊之間解耦。耦合會導致你難于重構(gòu)蠢熄,它是一種非常糟糕的的開發(fā)模式跪解。
壞:
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)
試著去遵循DRY 原則.
盡你最大的努力去避免復制代碼,它是一種非常糟糕的行為签孔,復制代碼
通常意味著當你需要變更一些邏輯時叉讥,你需要修改不止一處。
試想一下饥追,如果你在經(jīng)營一家餐廳并且你在記錄你倉庫的進銷記錄:所有
的土豆图仓,洋蔥,大蒜但绕,辣椒等救崔。如果你有多個列表來管理進銷記錄,當你
用其中一些土豆做菜時你需要更新所有的列表。如果你只有一個列表的話
只有一個地方需要更新帚豪。
通常情況下你復制代碼是應(yīng)該有兩個或者多個略微不同的邏輯碳竟,它們大多數(shù)
都是一樣的,但是由于它們的區(qū)別致使你必須有兩個或者多個隔離的但大部
分相同的方法狸臣,移除重復的代碼意味著用一個function/module/class創(chuàng)
建一個能處理差異的抽象。
用對抽象非常關(guān)鍵昌执,這正是為什么你必須學習遵守在類章節(jié)寫
的SOLID原則烛亦,不合理的抽象比復制代碼更糟糕,所以務(wù)必謹慎懂拾!說了這么多煤禽,
如果你能設(shè)計一個合理的抽象,那就這么干岖赋!別寫重復代碼檬果,否則你會發(fā)現(xiàn)
任何時候當你想修改一個邏輯時你必須修改多個地方。
壞:
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()
]);
}
}