目錄
- 介紹
- 變量
- 使用見字知意的變量名
- 同一個(gè)實(shí)體要用相同的變量名
- 使用便于搜索的名稱 (part 1)
- 使用便于搜索的名稱 (part 2)
- 使用自解釋型變量
- 避免深層嵌套,盡早返回 (part 1)
- 避免深層嵌套,盡早返回 (part 2)
- 少用無(wú)意義的變量名
- 不要添加不必要上下文
- 合理使用參數(shù)默認(rèn)值,沒(méi)必要在方法里再做默認(rèn)值檢測(cè)
- 表達(dá)式
- 使用恒等式
- 函數(shù)
- 函數(shù)參數(shù)(最好少于2個(gè))
- 函數(shù)應(yīng)該只做一件事
- 函數(shù)名應(yīng)體現(xiàn)他做了什么事
- 函數(shù)里應(yīng)當(dāng)只有一層抽象abstraction
- 不要用flag作為函數(shù)的參數(shù)
- 避免副作用
- 不要寫全局函數(shù)
- 不要使用單例模式
- 封裝條件語(yǔ)句
- 避免用反義條件判斷
- 避免條件判斷
- 避免類型檢查 (part 1)
- 避免類型檢查 (part 2)
- 移除僵尸代碼
- 對(duì)象和數(shù)據(jù)結(jié)構(gòu) Objects and Data Structures
- 使用 getters 和 setters Use object encapsulation
- 給對(duì)象使用私有或受保護(hù)的成員變量
- 類
- 少用繼承多用組合
- 避免連貫接口
- 推薦使用 final 類
- 類的SOLID原則 SOLID
- S: 單一職責(zé)原則 Single Responsibility Principle (SRP)
- O: 開閉原則 Open/Closed Principle (OCP)
- L: 里氏替換原則 Liskov Substitution Principle (LSP)
- I: 接口隔離原則 Interface Segregation Principle (ISP)
- D: 依賴倒置原則 Dependency Inversion Principle (DIP)
- 別寫重復(fù)代碼 (DRY)
- 翻譯
介紹
本文參考自 Robert C. Martin的Clean Code 書中的軟件工程師的原則 ,適用于php。 這不是風(fēng)格指南。 這是一個(gè)關(guān)于開發(fā)可讀、可復(fù)用并且可重構(gòu)的PHP軟件指南。
并不是這里所有的原則都得遵循,甚至很少的能被普遍接受。 這些雖然只是指導(dǎo),但是都是Clean Code作者多年總結(jié)出來(lái)的。
本文受到 clean-code-JAVAscript 的啟發(fā)
雖然很多開發(fā)者還在使用PHP5,但是本文中的大部分示例的運(yùn)行環(huán)境需要PHP 7.1+。
翻譯說(shuō)明
翻譯完成度100%,最后更新時(shí)間2020-10-26。本文由 php-cpm 基于 yangweijie版本 的clean-code-php翻譯并同步大量原文內(nèi)容。
原文更新頻率較高,我的翻譯方法是直接用文本比較工具逐行對(duì)比。優(yōu)先保證文字內(nèi)容是最新的,再逐步提升翻譯質(zhì)量。
閱讀過(guò)程中如果遇到各種鏈接失效、內(nèi)容老舊、術(shù)語(yǔ)使用錯(cuò)誤和其他翻譯錯(cuò)誤等問(wèn)題,歡迎大家積極提交PR。
變量
使用見字知意的變量名
壞:
$ymdstr = $moment->format('y-m-d');
好:
$currentDate = $moment->format('y-m-d');
? 返回頂部
同一個(gè)實(shí)體要用相同的變量名
壞:
getUserInfo();
getUserData();
getUserRecord();
getUserProfile();
好:
getUser();
? 返回頂部
使用便于搜索的名稱 (part 1)
寫代碼是用來(lái)讀的。所以寫出可讀性高、便于搜索的代碼至關(guān)重要。 命名變量時(shí)如果沒(méi)有有意義、不好理解,那就是在傷害讀者。 請(qǐng)讓你的代碼便于搜索。
壞:
// 448 ™ 干啥的?
$result = $serializer->serialize($data, 448);
好:
$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
使用便于搜索的名稱 (part 2)
壞:
class User
{
// 7 ™ 干啥的?
public $access = 7;
}
// 4 ™ 干啥的?
if ($user->access & 4) {
// ...
}
// 這里會(huì)發(fā)生什么?
$user->access ^= 2;
好:
class User
{
public const ACCESS_READ = 1;
public const ACCESS_CREATE = 2;
public const ACCESS_UPDATE = 4;
public const ACCESS_DELETE = 8;
// 默認(rèn)情況下用戶 具有讀、寫和更新權(quán)限
public $access = self::ACCESS_READ | self::ACCESS_CREATE | self::ACCESS_UPDATE;
}
if ($user->access & User::ACCESS_UPDATE) {
// do edit ...
}
// 禁用創(chuàng)建權(quán)限
$user->access ^= User::ACCESS_CREATE;
? 返回頂部
使用自解釋型變量
壞:
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,s*(.+?)s*(d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);
saveCityZipCode($matches[1], $matches[2]);
不錯(cuò):
好一些,但強(qiáng)依賴于正則表達(dá)式的熟悉程度
$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ǔ)句通常會(huì)導(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);
}
? 返回頂部
避免深層嵌套,盡早返回 (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);
}
? 返回頂部
少用無(wú)意義的變量名
別讓讀你的代碼的人猜你寫的變量是什么意思。 寫清楚好過(guò)模糊不清。
壞:
$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);
}
? 返回頂部
不要添加不必要上下文
如果從你的類名、對(duì)象名已經(jīng)可以得知一些信息,就別再在變量名里重復(fù)。
壞:
class Car
{
public $carMake;
public $carModel;
public $carColor;
//...
}
好:
class Car
{
public $make;
public $model;
public $color;
//...
}
? 返回頂部
合理使用參數(shù)默認(rèn)值,沒(méi)必要在方法里再做默認(rèn)值檢測(cè)
不好:
不好,$breweryName 可能為 NULL.
function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void
{
// ...
}
還行:
比上一個(gè)好理解一些,但最好能控制變量的值
function createMicrobrewery($name = null): void
{
$breweryName = $name ?: 'Hipster Brew Co.';
// ...
}
好:
如果你的程序只支持 PHP 7+, 那你可以用 type hinting 保證變量 $breweryName 不是 NULL.
function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void
{
// ...
}
? 返回頂部
表達(dá)式
使用恒等式
不好:
簡(jiǎn)易對(duì)比會(huì)將字符串轉(zhuǎn)為整形
$a = '42';
$b = 42;
if( $a != $b ) {
//這里始終執(zhí)行不到
}
對(duì)比 $a != $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.
? 返回頂部
Null coalescing operator
Null coalescing is a new operator introduced in PHP 7. The null coalescing operator ?? has been added as syntactic sugar for the common case of needing to use a ternary in conjunction with isset(). It returns its first operand if it exists and is not null; otherwise it returns its second operand.
Bad:
if (isset($_GET['name'])) {
$name = $_GET['name'];
} elseif (isset($_POST['name'])) {
$name = $_POST['name'];
} else {
$name = 'nobody';
}
Good:
$name = $_GET['name'] ?? $_POST['name'] ?? 'nobody';
? back to top
函數(shù)
函數(shù)參數(shù)(最好少于2個(gè))
限制函數(shù)參數(shù)個(gè)數(shù)極其重要,這樣測(cè)試你的函數(shù)容易點(diǎn)。有超過(guò)3個(gè)可選參數(shù)參數(shù)導(dǎo)致一個(gè)爆炸式組合增長(zhǎng),你會(huì)有成噸獨(dú)立參數(shù)情形要測(cè)試。
無(wú)參數(shù)是理想情況。1個(gè)或2個(gè)都可以,最好避免3個(gè)。再多就需要加固了。通常如果你的函數(shù)有超過(guò)兩個(gè)參數(shù),說(shuō)明他要處理的事太多了。 如果必須要傳入很多數(shù)據(jù),建議封裝一個(gè)高級(jí)別對(duì)象作為參數(shù)。
壞:
class Questionnaire
{
public function __construct(
string $firstname,
string $lastname,
string $patronymic,
string $region,
string $district,
string $city,
string $phone,
string $email
) {
// ...
}
}
好:
class Name
{
private $firstname;
private $lastname;
private $patronymic;
public function __construct(string $firstname, string $lastname, string $patronymic)
{
$this->firstname = $firstname;
$this->lastname = $lastname;
$this->patronymic = $patronymic;
}
// getters ...
}
class City
{
private $region;
private $district;
private $city;
public function __construct(string $region, string $district, string $city)
{
$this->region = $region;
$this->district = $district;
$this->city = $city;
}
// getters ...
}
class Contact
{
private $phone;
private $email;
public function __construct(string $phone, string $email)
{
$this->phone = $phone;
$this->email = $email;
}
// getters ...
}
class Questionnaire
{
public function __construct(Name $name, City $city, Contact $contact)
{
// ...
}
}
? 返回頂部
函數(shù)名應(yīng)體現(xiàn)他做了什么事
壞:
class Email
{
//...
public function handle(): void
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 啥?handle處理一個(gè)消息干嘛了?是往一個(gè)文件里寫嗎?
$message->handle();
好:
class Email
{
//...
public function send(): void
{
mail($this->to, $this->subject, $this->body);
}
}
$message = new Email(...);
// 簡(jiǎn)單明了
$message->send();
? 返回頂部
函數(shù)里應(yīng)當(dāng)只有一層抽象abstraction
當(dāng)你抽象層次過(guò)多時(shí)時(shí),函數(shù)處理的事情太多了。需要拆分功能來(lái)提高可重用性和易用性,以便簡(jiǎn)化測(cè)試。 (譯者注:這里從示例代碼看應(yīng)該是指嵌套過(guò)多)
壞:
function parseBetterPHPAlternative(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)中提取出來(lái),但是parseBetterJSAlternative()方法還是很復(fù)雜,而且不利于測(cè)試。
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 parseBetterPHPAlternative(string $code): void
{
$tokens = tokenize($code);
$ast = lexer($tokens);
foreach ($ast as $node) {
// 解析邏輯...
}
}
好:
最好的解決方案是把 parseBetterPHPAlternative()方法的依賴移除。
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 BetterPHPAlternative
{
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) {
// 解析邏輯...
}
}
}
? 返回頂部
不要用flag作為函數(shù)的參數(shù)
flag就是在告訴大家,這個(gè)方法里處理很多事。前面剛說(shuō)過(guò),一個(gè)函數(shù)應(yīng)當(dāng)只做一件事。 把不同flag的代碼拆分到多個(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ù)做了比獲取一個(gè)值然后返回另外一個(gè)值或值們會(huì)產(chǎn)生副作用如果。副作用可能是寫入一個(gè)文件,修改某些全局變量或者偶然地把你全部的錢給了陌生人。
現(xiàn)在,你的確需要在一個(gè)程序或者場(chǎng)合里要有副作用,像之前的例子,你也許需要寫一個(gè)文件。你想要做的是把你做這些的地方集中起來(lái)。不要用幾個(gè)函數(shù)和類來(lái)寫入一個(gè)特定的文件。用一個(gè)服務(wù)來(lái)做它,一個(gè)只有一個(gè)。
重點(diǎn)是避免常見陷阱比如對(duì)象間共享無(wú)結(jié)構(gòu)的數(shù)據(jù),使用可以寫入任何的可變數(shù)據(jù)類型,不集中處理副作用發(fā)生的地方。如果你做了這些你就會(huì)比大多數(shù)程序員快樂(lè)。
壞:
// 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ù)語(yǔ)言中污染全局變量是一個(gè)壞的實(shí)踐,因?yàn)槟憧赡芎推渌悗?kù)沖突 并且調(diào)用你api的人直到他們捕獲異常才知道踩坑了。讓我們思考一種場(chǎng)景: 如果你想配置一個(gè)數(shù)組,你可能會(huì)寫一個(gè)全局函數(shù)config(),但是他可能 和試著做同樣事的其他類庫(kù)沖突。
壞:
function config(): array
{
return [
'foo' => 'bar',
]
}
好:
class Configuration
{
private $configuration = [];
public function __construct(array $configuration)
{
$this->configuration = $configuration;
}
public function get(string $key): ?string
{
// null coalescing operator
return $this->configuration[$key] ?? null;
}
}
加載配置并創(chuàng)建 Configuration 類的實(shí)例
$configuration = new Configuration([
'foo' => 'bar',
]);
現(xiàn)在你必須在程序中用 Configuration 的實(shí)例了
? 返回頂部
不要使用單例模式
單例是一種 反模式. 以下是解釋:Paraphrased from Brian Button:
- 總是被用成全局實(shí)例。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.
- 導(dǎo)致代碼強(qiáng)耦合They inherently cause code to be tightly coupled. This makes faking them out under test rather difficult in many cases.
- 在整個(gè)程序的生命周期中始終攜帶狀態(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.
這里有一篇非常好的討論單例模式的[根本問(wèn)題((
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 類的實(shí)例并通過(guò) DSN 配置.
$connection = new DBConnection($dsn);
現(xiàn)在你必須在程序中 使用 DBConnection 的實(shí)例了
? 返回頂部
封裝條件語(yǔ)句
壞:
if ($article->state === 'published') {
// ...
}
好:
if ($article->isPublished()) {
// ...
}
? 返回頂部
避免用反義條件判斷
壞:
function isDOMNodeNotPresent(DOMNode $node): bool
{
// ...
}
if (!isDOMNodeNotPresent($node))
{
// ...
}
好:
function isDOMNodePresent(DOMNode $node): bool
{
// ...
}
if (isDOMNodePresent($node)) {
// ...
}
? 返回頂部
避免條件判斷
這看起來(lái)像一個(gè)不可能任務(wù)。當(dāng)人們第一次聽到這句話是都會(huì)這么說(shuō)。 "沒(méi)有if語(yǔ)句我還能做啥?" 答案是你可以使用多態(tài)來(lái)實(shí)現(xiàn)多種場(chǎng)景 的相同任務(wù)。第二個(gè)問(wèn)題很常見, “這么做可以,但為什么我要這么做?” 答案是前面我們學(xué)過(guò)的一個(gè)Clean Code原則:一個(gè)函數(shù)應(yīng)當(dāng)只做一件事。 當(dāng)你有很多含有if語(yǔ)句的類和函數(shù)時(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í)候你為這自由所痛苦并且在你的函數(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(Vehicle $vehicle): void
{
$vehicle->travelTo(new Location('texas'));
}
? 返回頂部
避免類型檢查 (part 2)
如果你正使用基本原始值比如字符串、整形和數(shù)組,要求版本是PHP 7+,不用多態(tài),需要類型檢測(cè), 那你應(yīng)當(dāng)考慮類型聲明或者嚴(yán)格模式。 提供了基于標(biāo)準(zhǔn)PHP語(yǔ)法的靜態(tài)類型。 手動(dòng)檢查類型的問(wèn)題是做好了需要好多的廢話,好像為了安全就可以不顧損失可讀性。 保持你的PHP 整潔,寫好測(cè)試,做好代碼回顧。做不到就用PHP嚴(yán)格類型聲明和嚴(yán)格模式來(lái)確保安全。
壞:
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ù)代碼一樣壞。沒(méi)有理由保留在你的代碼庫(kù)中。如果從來(lái)沒(méi)被調(diào)用過(guò),就刪掉! 因?yàn)檫€在代碼版本庫(kù)里,因此很安全。
壞:
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');
? 返回頂部
對(duì)象和數(shù)據(jù)結(jié)構(gòu)
使用 getters 和 setters
在PHP中你可以對(duì)方法使用public, protected, private 來(lái)控制對(duì)象屬性的變更。
- 當(dāng)你想對(duì)對(duì)象屬性做獲取之外的操作時(shí),你不需要在代碼中去尋找并修改每一個(gè)該屬性訪問(wèn)方法
- 當(dāng)有set對(duì)應(yīng)的屬性方法時(shí),易于增加參數(shù)的驗(yàn)證
- 封裝內(nèi)部的表示
- 使用set和get時(shí),易于增加日志和錯(cuò)誤控制
- 繼承當(dāng)前類時(shí),可以復(fù)寫默認(rèn)的方法功能
- 當(dāng)對(duì)象屬性是從遠(yuǎn)端服務(wù)器獲取時(shí),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();
? 返回頂部
給對(duì)象使用私有或受保護(hù)的成員變量
- 對(duì)public方法和屬性進(jìn)行修改非常危險(xiǎn),因?yàn)橥獠看a容易依賴他,而你沒(méi)辦法控制。對(duì)之修改影響所有這個(gè)類的使用者。 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.
- 對(duì)protected的修改跟對(duì)public修改差不多危險(xiǎn),因?yàn)樗麄儗?duì)子類可用,他倆的唯一區(qū)別就是可調(diào)用的位置不一樣,對(duì)之修改影響所有集成這個(gè)類的地方。 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.
- 對(duì)private的修改保證了這部分代碼只會(huì)影響當(dāng)前類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).
所以,當(dāng)你需要控制類里的代碼可以被訪問(wèn)時(shí)才用public/protected,其他時(shí)候都用private。
可以讀一讀這篇 博客文章 ,F(xiàn)abien 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è)計(jì)模式之前所說(shuō), 我們應(yīng)該盡量?jī)?yōu)先選擇組合而不是繼承的方式。使用繼承和組合都有很多好處。 這個(gè)準(zhǔn)則的主要意義在于當(dāng)你本能的使用繼承時(shí),試著思考一下組合是否能更好對(duì)你的需求建模。 在一些情況下,是這樣的。
接下來(lái)你或許會(huì)想,“那我應(yīng)該在什么時(shí)候使用繼承?” 答案依賴于你的問(wèn)題,當(dāng)然下面有一些何時(shí)繼承比組合更好的說(shuō)明:
- 你的繼承表達(dá)了“是一個(gè)”而不是“有一個(gè)”的關(guān)系(人類-》動(dòng)物,用戶-》用戶詳情)
- 你可以復(fù)用基類的代碼(人類可以像動(dòng)物一樣移動(dòng))
- 你想通過(guò)修改基類對(duì)所有派生類做全局的修改(當(dāng)動(dòng)物移動(dòng)時(shí),修改她們的能量消耗)
糟糕的:
class Employee
{
private $name;
private $email;
public function __construct(string $name, string $email)
{
$this->name = $name;
$this->email = $email;
}
// ...
}
// 不好,因?yàn)?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(EmployeeTaxData $taxData)
{
$this->taxData = $taxData;
}
// ...
}
? 返回頂部
避免連貫接口
連貫接口Fluent interface是一種 旨在提高面向?qū)ο缶幊虝r(shí)代碼可讀性的API設(shè)計(jì)模式,他基于方法鏈Method chaining
有上下文的地方可以降低代碼復(fù)雜度,例如PHPUnit Mock Builder 和Doctrine Query Builder ,更多的情況會(huì)帶來(lái)較大代價(jià):
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:
- 破壞了 對(duì)象封裝
- 破壞了 裝飾器模式
- 在測(cè)試組件中不好做mock
- 導(dǎo)致提交的diff不好閱讀
了解更多請(qǐng)閱讀 連貫接口為什么不好 ,作者 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 類
能用時(shí)盡量使用 final 關(guān)鍵字:
- 阻止不受控的繼承鏈
- 鼓勵(lì) 組合.
- 鼓勵(lì) 單一職責(zé)模式.
- 鼓勵(lì)開發(fā)者用你的公開方法而非通過(guò)繼承類獲取受保護(hù)方法的訪問(wèn)權(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推薦的便于記憶的首字母簡(jiǎn)寫,它代表了Robert Martin命名的最重要的五個(gè)面對(duì)對(duì)象編碼設(shè)計(jì)原則
- S: 單一職責(zé)原則 (SRP)
- O: 開閉原則 (OCP)
- L: 里氏替換原則 (LSP)
- I: 接口隔離原則 (ISP)
- D: 依賴倒置原則 (DIP)
單一職責(zé)原則
Single Responsibility Principle (SRP)
正如在Clean Code所述,"修改一個(gè)類應(yīng)該只為一個(gè)理由"。 人們總是易于用一堆方法塞滿一個(gè)類,如同我們只能在飛機(jī)上 只能攜帶一個(gè)行李箱(把所有的東西都塞到箱子里)。這樣做 的問(wèn)題是:從概念上這樣的類不是高內(nèi)聚的,并且留下了很多 理由去修改它。將你需要修改類的次數(shù)降低到最小很重要。 這是因?yàn)椋?dāng)有很多方法在類中時(shí),修改其中一處,你很難知 曉在代碼庫(kù)中哪些依賴的模塊會(huì)被影響到。
壞:
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)該對(duì)擴(kuò)展開放,對(duì)修改關(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);
}
}
? 返回頂部
里氏替換原則
Liskov Substitution Principle (LSP)
這是一個(gè)簡(jiǎn)單的原則,卻用了一個(gè)不好理解的術(shù)語(yǔ)。它的正式定義是 "如果S是T的子類型,那么在不改變程序原有既定屬性(檢查、執(zhí)行 任務(wù)等)的前提下,任何T類型的對(duì)象都可以使用S類型的對(duì)象替代 (例如,使用S的對(duì)象可以替代T的對(duì)象)" 這個(gè)定義更難理解:-)。
對(duì)這個(gè)概念最好的解釋是:如果你有一個(gè)父類和一個(gè)子類,在不改變 原有結(jié)果正確性的前提下父類和子類可以互換。這個(gè)聽起來(lái)依舊讓人 有些迷惑,所以讓我們來(lái)看一個(gè)經(jīng)典的正方形-長(zhǎng)方形的例子。從數(shù)學(xué) 上講,正方形是一種長(zhǎng)方形,但是當(dāng)你的模型通過(guò)繼承使用了"is-a" 的關(guān)系時(shí),就不對(duì)了。
壞:
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);
}
好:
最好是將這兩種四邊形分別對(duì)待,用一個(gè)適合兩種類型的更通用子類型來(lái)代替。
盡管正方形和長(zhǎng)方形看起來(lái)很相似,但他們是不同的。 正方形更接近菱形,而長(zhǎng)方形更接近平行四邊形。但他們不是子類型。 盡管相似,正方形、長(zhǎng)方形、菱形、平行四邊形都是有自己屬性的不同形狀。
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)該被強(qiáng)制依賴于他不需要的接口"
有一個(gè)清晰的例子來(lái)說(shuō)明示范這條原則。當(dāng)一個(gè)類需要一個(gè)大量的設(shè)置項(xiàng), 為了方便不會(huì)要求調(diào)用方去設(shè)置大量的選項(xiàng),因?yàn)樵谕ǔK麄儾恍枰械?設(shè)置項(xiàng)。使設(shè)置項(xiàng)可選有助于我們避免產(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
}
}
好:
不是每一個(gè)工人都是雇員,但是每一個(gè)雇員都是一個(gè)工人
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)
這條原則說(shuō)明兩個(gè)基本的要點(diǎn):
- 高階的模塊不應(yīng)該依賴低階的模塊,它們都應(yīng)該依賴于抽象
- 抽象不應(yīng)該依賴于實(shí)現(xiàn),實(shí)現(xiàn)應(yīng)該依賴于抽象
這條起初看起來(lái)有點(diǎn)晦澀難懂,但是如果你使用過(guò) PHP 框架(例如 Symfony),你應(yīng)該見過(guò) 依賴注入(DI),它是對(duì)這個(gè)概念的實(shí)現(xiàn)。雖然它們不是完全相等的概念,依賴倒置原則使高階模塊 與低階模塊的實(shí)現(xiàn)細(xì)節(jié)和創(chuàng)建分離??梢允褂靡蕾囎⑷耄―I)這種方式來(lái)實(shí)現(xiàn)它。最大的好處 是它使模塊之間解耦。耦合會(huì)導(dǎo)致你難于重構(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();
}
}
? 返回頂部
別寫重復(fù)代碼 (DRY)
試著去遵循DRY 原則.
盡你最大的努力去避免復(fù)制代碼,它是一種非常糟糕的行為,復(fù)制代碼 通常意味著當(dāng)你需要變更一些邏輯時(shí),你需要修改不止一處。
試想一下,如果你在經(jīng)營(yíng)一家餐廳并且你在記錄你倉(cāng)庫(kù)的進(jìn)銷記錄:所有 的土豆,洋蔥,大蒜,辣椒等。如果你有多個(gè)列表來(lái)管理進(jìn)銷記錄,當(dāng)你 用其中一些土豆做菜時(shí)你需要更新所有的列表。如果你只有一個(gè)列表的話 只有一個(gè)地方需要更新。
通常情況下你復(fù)制代碼是應(yīng)該有兩個(gè)或者多個(gè)略微不同的邏輯,它們大多數(shù) 都是一樣的,但是由于它們的區(qū)別致使你必須有兩個(gè)或者多個(gè)隔離的但大部 分相同的方法,移除重復(fù)的代碼意味著用一個(gè)function/module/class創(chuàng) 建一個(gè)能處理差異的抽象。
用對(duì)抽象非常關(guān)鍵,這正是為什么你必須學(xué)習(xí)遵守該類章節(jié)寫 的SOLID原則,不合理的抽象比復(fù)制代碼更糟糕,所以務(wù)必謹(jǐn)慎!說(shuō)了這么多, 如果你能設(shè)計(jì)一個(gè)合理的抽象,那就這么干!別寫重復(fù)代碼,否則你會(huì)發(fā)現(xiàn) 任何時(shí)候當(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()
]);
}
}