php乾淨程式碼

kakasaber發表於2021-01-12

變數

使用見字知意的變數名

壞:

  1. $ymdstr = $moment->format('y-m-d');

好:

  1. $currentDate = $moment->format('y-m-d');

同一個實體要用相同的變數名

壞:

  1. getUserInfo();
  2. getUserData();
  3. getUserRecord();
  4. getUserProfile();

好:

  1. getUser();

使用便於搜尋的名稱 (part 1)

寫程式碼是用來讀的。所以寫出可讀性高、便於搜尋的程式碼至關重要。 命名變數時如果沒有有意義、不好理解,那就是在傷害讀者。 請讓你的程式碼便於搜尋。

壞:

  1. // 448 ™ 幹啥的?
  2. $result = $serializer->serialize($data, 448);

好:

  1. $json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

使用便於搜尋的名稱 (part 2)

壞:

  1. class User

  2. {

  3. // 7 ™ 幹啥的?

  4. public $access = 7;

  5. }

  6. // 4 ™ 幹啥的?

  7. if ($user->access & 4) {

  8. // ...

  9. }

  10. // 這裡會發生什麼?

  11. $user->access ^= 2;

好:

  1. class User

  2. {

  3. const ACCESS_READ = 1;

  4. const ACCESS_CREATE = 2;

  5. const ACCESS_UPDATE = 4;

  6. const ACCESS_DELETE = 8;

  7. // 預設情況下使用者 具有讀、寫和更新許可權

  8. public $access = self::ACCESS_READ | self::ACCESS_CREATE | self::ACCESS_UPDATE;

  9. }

  10. if ($user->access & User::ACCESS_UPDATE) {

  11. // do edit ...

  12. }

  13. // 禁用建立許可權

  14. $user->access ^= User::ACCESS_CREATE;

使用自解釋型變數

壞:

  1. $address = 'One Infinite Loop, Cupertino 95014';

  2. $cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/';

  3. preg_match($cityZipCodeRegex, $address, $matches);

  4. saveCityZipCode($matches[1], $matches[2]);

不錯:

好一些,但強依賴於正規表示式的熟悉程度

  1. $address = 'One Infinite Loop, Cupertino 95014';

  2. $cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/';

  3. preg_match($cityZipCodeRegex, $address, $matches);

  4. [, $city, $zipCode] = $matches;

  5. saveCityZipCode($city, $zipCode);

好:

使用帶名字的子規則,不用懂正則也能看的懂

  1. $address = 'One Infinite Loop, Cupertino 95014';

  2. $cityZipCodeRegex = '/^[^,]+,\s*(?<city>.+?)\s*(?<zipCode>\d{5})$/';

  3. preg_match($cityZipCodeRegex, $address, $matches);

  4. saveCityZipCode($matches['city'], $matches['zipCode']);

避免深層巢狀,儘早返回 (part 1)

太多的if else語句通常會導致你的程式碼難以閱讀,直白優於隱晦

糟糕:

  1. function isShopOpen($day): bool
  2. {
  3. if ($day) {
  4. if (is_string($day)) {
  5. $day = strtolower($day);
  6. if ($day === 'friday') {
  7. return true;
  8. } elseif ($day === 'saturday') {
  9. return true;
  10. } elseif ($day === 'sunday') {
  11. return true;
  12. } else {
  13. return false;
  14. }
  15. } else {
  16. return false;
  17. }
  18. } else {
  19. return false;
  20. }
  21. }

好:

  1. function isShopOpen(string $day): bool

  2. {

  3. if (empty($day)) {

  4. return false;

  5. }

  6. $openingDays = [

  7. 'friday', 'saturday', 'sunday'

  8. ];

  9. return in_array(strtolower($day), $openingDays, true);

  10. }

避免深層巢狀,儘早返回 (part 2)

糟糕的:

  1. function fibonacci(int $n)
  2. {
  3. if ($n < 50) {
  4. if ($n !== 0) {
  5. if ($n !== 1) {
  6. return fibonacci($n - 1) + fibonacci($n - 2);
  7. } else {
  8. return 1;
  9. }
  10. } else {
  11. return 0;
  12. }
  13. } else {
  14. return 'Not supported';
  15. }
  16. }

好:

  1. function fibonacci(int $n): int

  2. {

  3. if ($n === 0 || $n === 1) {

  4. return $n;

  5. }

  6. if ($n >= 50) {

  7. throw new \Exception('Not supported');

  8. }

  9. return fibonacci($n - 1) + fibonacci($n - 2);

  10. }

少用無意義的變數名

別讓讀你的程式碼的人猜你寫的變數是什麼意思。 寫清楚好過模糊不清。

壞:

  1. $l = ['Austin', 'New York', 'San Francisco'];

  2. for ($i = 0; $i < count($l); $i++) {

  3. $li = $l[$i];

  4. doStuff();

  5. doSomeOtherStuff();

  6. // ...

  7. // ...

  8. // ...

  9. // 等等,$li又代表什麼?

  10. dispatch($li);

  11. }

好:

  1. $locations = ['Austin', 'New York', 'San Francisco'];

  2. foreach ($locations as $location) {

  3. doStuff();

  4. doSomeOtherStuff();

  5. // ...

  6. // ...

  7. // ...

  8. dispatch($location);

  9. }

不要新增不必要上下文

如果從你的類名、物件名已經可以得知一些資訊,就別再在變數名裡重複。

壞:

  1. class Car

  2. {

  3. public $carMake;

  4. public $carModel;

  5. public $carColor;

  6. //...

  7. }

好:

  1. class Car

  2. {

  3. public $make;

  4. public $model;

  5. public $color;

  6. //...

  7. }

合理使用引數預設值,沒必要在方法裡再做預設值檢測

不好:

不好,$breweryName 可能為 NULL.

  1. function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void
  2. {
  3. // ...
  4. }

還行:

比上一個好理解一些,但最好能控制變數的值

  1. function createMicrobrewery($name = null): void
  2. {
  3. $breweryName = $name ?: 'Hipster Brew Co.';
  4. // ...
  5. }

好:

如果你的程式只支援 PHP 7+, 那你可以用 type hinting 保證變數 $breweryName 不是 NULL.

  1. function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void
  2. {
  3. // ...
  4. }

表示式

使用恆等式

不好:

簡易對比會將字串轉為整形

  1. $a = '42';

  2. $b = 42;

  3. if( $a != $b ) {

  4. //這裡始終執行不到

  5. }

對比 $a != $b 返回了 FALSE 但應該返回 TRUE ! 字串 ‘42’ 跟整數 42 不相等

好:

使用恆等判斷檢查型別和資料

  1. $a = '42';

  2. $b = 42;

  3. if ($a !== $b) {

  4. // The expression is verified

  5. }

The comparison $a !== $b returns TRUE.

函式

函式引數(最好少於2個)
限制函式引數個數極其重要,這樣測試你的函式容易點。有超過3個可選引數引數導致一個爆炸式組合增長,你會有成噸獨立引數情形要測試。

無引數是理想情況。1個或2個都可以,最好避免3個。再多就需要加固了。通常如果你的函式有超過兩個引數,說明他要處理的事太多了。 如果必須要傳入很多資料,建議封裝一個高階別物件作為引數。

壞:

  1. function createMenu(string $title, string $body, string $buttonText, bool $cancellable): void
  2. {
  3. // ...
  4. }

好:

  1. class MenuConfig

  2. {

  3. public $title;

  4. public $body;

  5. public $buttonText;

  6. public $cancellable = false;

  7. }

  8. $config = new MenuConfig();

  9. $config->title = 'Foo';

  10. $config->body = 'Bar';

  11. $config->buttonText = 'Baz';

  12. $config->cancellable = true;

  13. function createMenu(MenuConfig $config): void

  14. {

  15. // ...

  16. }

函式應該只做一件事

這是迄今為止軟體工程裡最重要的一個規則。當一個函式做超過一件事的時候,他們就難於實現、測試和理解。當你把一個函式拆分到只剩一個功能時,他們就容易被重構,然後你的程式碼讀起來就更清晰。如果你光遵循這條規則,你就領先於大多數開發者了。

壞:

  1. function emailClients(array $clients): void
  2. {
  3. foreach ($clients as $client) {
  4. $clientRecord = $db->find($client);
  5. if ($clientRecord->isActive()) {
  6. email($client);
  7. }
  8. }
  9. }

好:

  1. function emailClients(array $clients): void

  2. {

  3. $activeClients = activeClients($clients);

  4. array_walk($activeClients, 'email');

  5. }

  6. function activeClients(array $clients): array

  7. {

  8. return array_filter($clients, 'isClientActive');

  9. }

  10. function isClientActive(int $client): bool

  11. {

  12. $clientRecord = $db->find($client);

  13. return $clientRecord->isActive();

  14. }

函式名應體現他做了什麼事

壞:

  1. class Email

  2. {

  3. //...

  4. public function handle(): void

  5. {

  6. mail($this->to, $this->subject, $this->body);

  7. }

  8. }

  9. $message = new Email(...);

  10. // 啥?handle處理一個訊息幹嘛了?是往一個檔案裡寫嗎?

  11. $message->handle();

好:

  1. class Email

  2. {

  3. //...

  4. public function send(): void

  5. {

  6. mail($this->to, $this->subject, $this->body);

  7. }

  8. }

  9. $message = new Email(...);

  10. // 簡單明瞭

  11. $message->send();

函式裡應當只有一層抽象abstraction

當你抽象層次過多時時,函式處理的事情太多了。需要拆分功能來提高可重用性和易用性,以便簡化測試。 (譯者注:這裡從示例程式碼看應該是指巢狀過多)

壞:

  1. function parseBetterJSAlternative(string $code): void

  2. {

  3. $regexes = [

  4. // ...

  5. ];

  6. $statements = explode(' ', $code);

  7. $tokens = [];

  8. foreach ($regexes as $regex) {

  9. foreach ($statements as $statement) {

  10. // ...

  11. }

  12. }

  13. $ast = [];

  14. foreach ($tokens as $token) {

  15. // lex...

  16. }

  17. foreach ($ast as $node) {

  18. // parse...

  19. }

  20. }

壞:

我們把一些方法從迴圈中提取出來,但是parseBetterJSAlternative()方法還是很複雜,而且不利於測試。

  1. function tokenize(string $code): array

  2. {

  3. $regexes = [

  4. // ...

  5. ];

  6. $statements = explode(' ', $code);

  7. $tokens = [];

  8. foreach ($regexes as $regex) {

  9. foreach ($statements as $statement) {

  10. $tokens[] = /* ... */;

  11. }

  12. }

  13. return $tokens;

  14. }

  15. function lexer(array $tokens): array

  16. {

  17. $ast = [];

  18. foreach ($tokens as $token) {

  19. $ast[] = /* ... */;

  20. }

  21. return $ast;

  22. }

  23. function parseBetterJSAlternative(string $code): void

  24. {

  25. $tokens = tokenize($code);

  26. $ast = lexer($tokens);

  27. foreach ($ast as $node) {

  28. // 解析邏輯...

  29. }

  30. }

好:

最好的解決方案是把 parseBetterJSAlternative()方法的依賴移除。

  1. class Tokenizer

  2. {

  3. public function tokenize(string $code): array

  4. {

  5. $regexes = [

  6. // ...

  7. ];

  8. $statements = explode(' ', $code);

  9. $tokens = [];

  10. foreach ($regexes as $regex) {

  11. foreach ($statements as $statement) {

  12. $tokens[] = /* ... */;

  13. }

  14. }

  15. return $tokens;

  16. }

  17. }

  18. class Lexer

  19. {

  20. public function lexify(array $tokens): array

  21. {

  22. $ast = [];

  23. foreach ($tokens as $token) {

  24. $ast[] = /* ... */;

  25. }

  26. return $ast;

  27. }

  28. }

  29. class BetterJSAlternative

  30. {

  31. private $tokenizer;

  32. private $lexer;

  33. public function __construct(Tokenizer $tokenizer, Lexer $lexer)

  34. {

  35. $this->tokenizer = $tokenizer;

  36. $this->lexer = $lexer;

  37. }

  38. public function parse(string $code): void

  39. {

  40. $tokens = $this->tokenizer->tokenize($code);

  41. $ast = $this->lexer->lexify($tokens);

  42. foreach ($ast as $node) {

  43. // 解析邏輯...

  44. }

  45. }

  46. }

這樣我們可以對依賴做mock,並測試BetterJSAlternative::parse()執行是否符合預期。

不要用flag作為函式的引數

flag就是在告訴大家,這個方法裡處理很多事。前面剛說過,一個函式應當只做一件事。 把不同flag的程式碼拆分到多個函式裡。

壞:

  1. function createFile(string $name, bool $temp = false): void
  2. {
  3. if ($temp) {
  4. touch('./temp/'.$name);
  5. } else {
  6. touch($name);
  7. }
  8. }

好:

  1. function createFile(string $name): void

  2. {

  3. touch($name);

  4. }

  5. function createTempFile(string $name): void

  6. {

  7. touch('./temp/'.$name);

  8. }

避免副作用

一個函式做了比獲取一個值然後返回另外一個值或值們會產生副作用如果。副作用可能是寫入一個檔案,修改某些全域性變數或者偶然的把你全部的錢給了陌生人。

現在,你的確需要在一個程式或者場合裡要有副作用,像之前的例子,你也許需要寫一個檔案。你想要做的是把你做這些的地方集中起來。不要用幾個函式和類來寫入一個特定的檔案。用一個服務來做它,一個只有一個。

重點是避免常見陷阱比如物件間共享無結構的資料,使用可以寫入任何的可變資料型別,不集中處理副作用發生的地方。如果你做了這些你就會比大多數程式設計師快樂。

壞:

  1. // Global variable referenced by following function.

  2. // If we had another function that used this name, now it'd be an array and it could break it.

  3. $name = 'Ryan McDermott';

  4. function splitIntoFirstAndLastName(): void

  5. {

  6. global $name;

  7. $name = explode(' ', $name);

  8. }

  9. splitIntoFirstAndLastName();

  10. var_dump($name); // ['Ryan', 'McDermott'];

好:

  1. function splitIntoFirstAndLastName(string $name): array

  2. {

  3. return explode(' ', $name);

  4. }

  5. $name = 'Ryan McDermott';

  6. $newName = splitIntoFirstAndLastName($name);

  7. var_dump($name); // 'Ryan McDermott';

  8. var_dump($newName); // ['Ryan', 'McDermott'];

不要寫全域性函式

在大多數語言中汙染全域性變數是一個壞的實踐,因為你可能和其他類庫衝突 並且呼叫你api的人直到他們捕獲異常才知道踩坑了。讓我們思考一種場景: 如果你想配置一個陣列,你可能會寫一個全域性函式config(),但是他可能 和試著做同樣事的其他類庫衝突。

壞:

  1. function config(): array
  2. {
  3. return [
  4. 'foo' => 'bar',
  5. ]
  6. }

好:

  1. class Configuration

  2. {

  3. private $configuration = [];

  4. public function __construct(array $configuration)

  5. {

  6. $this->configuration = $configuration;

  7. }

  8. public function get(string $key): ?string

  9. {

  10. return isset($this->configuration[$key]) ? $this->configuration[$key] : null;

  11. }

  12. }

載入配置並建立 Configuration 類的例項

  1. $configuration = new Configuration([
  2. 'foo' => 'bar',
  3. ]);

現在你必須在程式中用 Configuration 的例項了

封裝條件語句

壞:

  1. if ($article->state === 'published') {
  2. // ...
  3. }

好:

  1. if ($article->isPublished()) {
  2. // ...
  3. }

避免用反義條件判斷

壞:

  1. function isDOMNodeNotPresent(\DOMNode $node): bool

  2. {

  3. // ...

  4. }

  5. if (!isDOMNodeNotPresent($node))

  6. {

  7. // ...

  8. }

好:

  1. function isDOMNodePresent(\DOMNode $node): bool

  2. {

  3. // ...

  4. }

  5. if (isDOMNodePresent($node)) {

  6. // ...

  7. }

避免條件判斷

這看起來像一個不可能任務。當人們第一次聽到這句話是都會這麼說。 “沒有if語句我還能做啥?” 答案是你可以使用多型來實現多種場景 的相同任務。第二個問題很常見, “這麼做可以,但為什麼我要這麼做?” 答案是前面我們學過的一個Clean Code原則:一個函式應當只做一件事。 當你有很多含有if語句的類和函式時,你的函式做了不止一件事。 記住,只做一件事。

壞:

  1. class Airplane

  2. {

  3. // ...

  4. public function getCruisingAltitude(): int

  5. {

  6. switch ($this->type) {

  7. case '777':

  8. return $this->getMaxAltitude() - $this->getPassengerCount();

  9. case 'Air Force One':

  10. return $this->getMaxAltitude();

  11. case 'Cessna':

  12. return $this->getMaxAltitude() - $this->getFuelExpenditure();

  13. }

  14. }

  15. }

好:

  1. interface Airplane

  2. {

  3. // ...

  4. public function getCruisingAltitude(): int;

  5. }

  6. class Boeing777 implements Airplane

  7. {

  8. // ...

  9. public function getCruisingAltitude(): int

  10. {

  11. return $this->getMaxAltitude() - $this->getPassengerCount();

  12. }

  13. }

  14. class AirForceOne implements Airplane

  15. {

  16. // ...

  17. public function getCruisingAltitude(): int

  18. {

  19. return $this->getMaxAltitude();

  20. }

  21. }

  22. class Cessna implements Airplane

  23. {

  24. // ...

  25. public function getCruisingAltitude(): int

  26. {

  27. return $this->getMaxAltitude() - $this->getFuelExpenditure();

  28. }

  29. }

避免型別檢查 (part 1)

PHP是弱型別的,這意味著你的函式可以接收任何型別的引數。 有時候你為這自由所痛苦並且在你的函式漸漸嘗試型別檢查。 有很多方法去避免這麼做。第一種是統一API。

壞:

  1. function travelToTexas($vehicle): void
  2. {
  3. if ($vehicle instanceof Bicycle) {
  4. $vehicle->pedalTo(new Location('texas'));
  5. } elseif ($vehicle instanceof Car) {
  6. $vehicle->driveTo(new Location('texas'));
  7. }
  8. }

好:

  1. function travelToTexas(Vehicle $vehicle): void
  2. {
  3. $vehicle->travelTo(new Location('texas'));
  4. }

避免型別檢查 (part 2)

如果你正使用基本原始值比如字串、整形和陣列,要求版本是PHP 7+,不用多型,需要型別檢測, 那你應當考慮型別宣告或者嚴格模式。 提供了基於標準PHP語法的靜態型別。 手動檢查型別的問題是做好了需要好多的廢話,好像為了安全就可以不顧損失可讀性。 保持你的PHP 整潔,寫好測試,做好程式碼回顧。做不到就用PHP嚴格型別宣告和嚴格模式來確保安全。

壞:

  1. function combine($val1, $val2): int

  2. {

  3. if (!is_numeric($val1) || !is_numeric($val2)) {

  4. throw new \Exception('Must be of type Number');

  5. }

  6. return $val1 + $val2;

  7. }

好:

  1. function combine(int $val1, int $val2): int
  2. {
  3. return $val1 + $val2;
  4. }

移除殭屍程式碼

殭屍程式碼和重複程式碼一樣壞。沒有理由保留在你的程式碼庫中。如果從來沒被呼叫過,就刪掉! 因為還在程式碼版本庫裡,因此很安全。

壞:

  1. function oldRequestModule(string $url): void

  2. {

  3. // ...

  4. }

  5. function newRequestModule(string $url): void

  6. {

  7. // ...

  8. }

  9. $request = newRequestModule($requestUrl);

  10. inventoryTracker('apples', $request, 'www.inventory-awesome.io');

好:

  1. function requestModule(string $url): void

  2. {

  3. // ...

  4. }

  5. $request = requestModule($requestUrl);

  6. inventoryTracker('apples', $request, 'www.inventory-awesome.io');

物件和資料結構

使用 getters 和 setters
在PHP中你可以對方法使用public, protected, private 來控制物件屬性的變更。

當你想對物件屬性做獲取之外的操作時,你不需要在程式碼中去尋找並修改每一個該屬性訪問方法
當有set對應的屬性方法時,易於增加引數的驗證
封裝內部的表示
使用set和get時,易於增加日誌和錯誤控制
繼承當前類時,可以複寫預設的方法功能
當物件屬性是從遠端伺服器獲取時,get,set易於使用延遲載入
此外,這樣的方式也符合OOP開發中的開閉原則

壞:

  1. class BankAccount

  2. {

  3. public $balance = 1000;

  4. }

  5. $bankAccount = new BankAccount();

  6. // Buy shoes...

  7. $bankAccount->balance -= 100;

好:

  1. class BankAccount

  2. {

  3. private $balance;

  4. public function __construct(int $balance = 1000)

  5. {

  6. $this->balance = $balance;

  7. }

  8. public function withdraw(int $amount): void

  9. {

  10. if ($amount > $this->balance) {

  11. throw new \Exception('Amount greater than available balance.');

  12. }

  13. $this->balance -= $amount;

  14. }

  15. public function deposit(int $amount): void

  16. {

  17. $this->balance += $amount;

  18. }

  19. public function getBalance(): int

  20. {

  21. return $this->balance;

  22. }

  23. }

  24. $bankAccount = new BankAccount();

  25. // Buy shoes...

  26. $bankAccount->withdraw($shoesPrice);

  27. // Get balance

  28. $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寫的.

壞:

  1. class Employee

  2. {

  3. public $name;

  4. public function __construct(string $name)

  5. {

  6. $this->name = $name;

  7. }

  8. }

  9. $employee = new Employee('John Doe');

  10. echo 'Employee name: '.$employee->name; // Employee name: John Doe

好:

  1. class Employee

  2. {

  3. private $name;

  4. public function __construct(string $name)

  5. {

  6. $this->name = $name;

  7. }

  8. public function getName(): string

  9. {

  10. return $this->name;

  11. }

  12. }

  13. $employee = new Employee('John Doe');

  14. echo 'Employee name: '.$employee->getName(); // Employee name: John Doe

少用繼承多用組合

正如 the Gang of Four 所著的設計模式之前所說, 我們應該儘量優先選擇組合而不是繼承的方式。使用繼承和組合都有很多好處。 這個準則的主要意義在於當你本能的使用繼承時,試著思考一下組合是否能更好對你的需求建模。 在一些情況下,是這樣的。

接下來你或許會想,“那我應該在什麼時候使用繼承?” 答案依賴於你的問題,當然下面有一些何時繼承比組合更好的說明:

你的繼承表達了“是一個”而不是“有一個”的關係(人類-》動物,使用者-》使用者詳情)
你可以複用基類的程式碼(人類可以像動物一樣移動)
你想通過修改基類對所有派生類做全域性的修改(當動物移動時,修改她們的能量消耗)
糟糕的:

  1. class Employee

  2. {

  3. private $name;

  4. private $email;

  5. public function __construct(string $name, string $email)

  6. {

  7. $this->name = $name;

  8. $this->email = $email;

  9. }

  10. // ...

  11. }

  12. // 不好,因為 Employees "有" taxdata

  13. // 而 EmployeeTaxData 不是 Employee 型別的

  14. class EmployeeTaxData extends Employee

  15. {

  16. private $ssn;

  17. private $salary;

  18. public function __construct(string $name, string $email, string $ssn, string $salary)

  19. {

  20. parent::__construct($name, $email);

  21. $this->ssn = $ssn;

  22. $this->salary = $salary;

  23. }

  24. // ...

  25. }

好:

  1. class EmployeeTaxData

  2. {

  3. private $ssn;

  4. private $salary;

  5. public function __construct(string $ssn, string $salary)

  6. {

  7. $this->ssn = $ssn;

  8. $this->salary = $salary;

  9. }

  10. // ...

  11. }

  12. class Employee

  13. {

  14. private $name;

  15. private $email;

  16. private $taxData;

  17. public function __construct(string $name, string $email)

  18. {

  19. $this->name = $name;

  20. $this->email = $email;

  21. }

  22. public function setTaxData(string $ssn, string $salary)

  23. {

  24. $this->taxData = new EmployeeTaxData($ssn, $salary);

  25. }

  26. // ...

  27. }

避免連貫介面

連貫介面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:

破壞了 物件封裝
破壞了 裝飾器模式
在測試元件中不好做mock
導致提交的diff不好閱讀
瞭解更多請閱讀 連貫介面為什麼不好 ,作者 Marco Pivetta.

壞:

  1. class Car

  2. {

  3. private $make = 'Honda';

  4. private $model = 'Accord';

  5. private $color = 'white';

  6. public function setMake(string $make): self

  7. {

  8. $this->make = $make;

  9. // NOTE: Returning this for chaining

  10. return $this;

  11. }

  12. public function setModel(string $model): self

  13. {

  14. $this->model = $model;

  15. // NOTE: Returning this for chaining

  16. return $this;

  17. }

  18. public function setColor(string $color): self

  19. {

  20. $this->color = $color;

  21. // NOTE: Returning this for chaining

  22. return $this;

  23. }

  24. public function dump(): void

  25. {

  26. var_dump($this->make, $this->model, $this->color);

  27. }

  28. }

  29. $car = (new Car())

  30. ->setColor('pink')

  31. ->setMake('Ford')

  32. ->setModel('F-150')

  33. ->dump();

好:

  1. class Car

  2. {

  3. private $make = 'Honda';

  4. private $model = 'Accord';

  5. private $color = 'white';

  6. public function setMake(string $make): void

  7. {

  8. $this->make = $make;

  9. }

  10. public function setModel(string $model): void

  11. {

  12. $this->model = $model;

  13. }

  14. public function setColor(string $color): void

  15. {

  16. $this->color = $color;

  17. }

  18. public function dump(): void

  19. {

  20. var_dump($this->make, $this->model, $this->color);

  21. }

  22. }

  23. $car = new Car();

  24. $car->setColor('pink');

  25. $car->setMake('Ford');

  26. $car->setModel('F-150');

  27. $car->dump();

推薦使用 final 類

能用時儘量使用 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:

  1. final class Car

  2. {

  3. private $color;

  4. public function __construct($color)

  5. {

  6. $this->color = $color;

  7. }

  8. /**

  9. * @return string The color of the vehicle

  10. */

  11. public function getColor()

  12. {

  13. return $this->color;

  14. }

  15. }

Good:

  1. interface Vehicle

  2. {

  3. /**

  4. * @return string The color of the vehicle

  5. */

  6. public function getColor();

  7. }

  8. final class Car implements Vehicle

  9. {

  10. private $color;

  11. public function __construct($color)

  12. {

  13. $this->color = $color;

  14. }

  15. /**

  16. * {@inheritdoc}

  17. */

  18. public function getColor()

  19. {

  20. return $this->color;

  21. }

  22. }

SOLID

SOLID 是Michael Feathers推薦的便於記憶的首字母簡寫,它代表了Robert Martin命名的最重要的五個面對物件編碼設計原則

S: 單一職責原則 (SRP)
O: 開閉原則 (OCP)
L: 里氏替換原則 (LSP)
I: 介面隔離原則 (ISP)
D: 依賴倒置原則 (DIP)
單一職責原則
Single Responsibility Principle (SRP)

正如在Clean Code所述,”修改一個類應該只為一個理由”。 人們總是易於用一堆方法塞滿一個類,如同我們只能在飛機上 只能攜帶一個行李箱(把所有的東西都塞到箱子裡)。這樣做 的問題是:從概念上這樣的類不是高內聚的,並且留下了很多 理由去修改它。將你需要修改類的次數降低到最小很重要。 這是因為,當有很多方法在類中時,修改其中一處,你很難知 曉在程式碼庫中哪些依賴的模組會被影響到。

壞:

  1. class UserSettings

  2. {

  3. private $user;

  4. public function __construct(User $user)

  5. {

  6. $this->user = $user;

  7. }

  8. public function changeSettings(array $settings): void

  9. {

  10. if ($this->verifyCredentials()) {

  11. // ...

  12. }

  13. }

  14. private function verifyCredentials(): bool

  15. {

  16. // ...

  17. }

  18. }

好:

  1. class UserAuth

  2. {

  3. private $user;

  4. public function __construct(User $user)

  5. {

  6. $this->user = $user;

  7. }

  8. public function verifyCredentials(): bool

  9. {

  10. // ...

  11. }

  12. }

  13. class UserSettings

  14. {

  15. private $user;

  16. private $auth;

  17. public function __construct(User $user)

  18. {

  19. $this->user = $user;

  20. $this->auth = new UserAuth($user);

  21. }

  22. public function changeSettings(array $settings): void

  23. {

  24. if ($this->auth->verifyCredentials()) {

  25. // ...

  26. }

  27. }

  28. }

開閉原則

Open/Closed Principle (OCP)

正如Bertrand Meyer所述,”軟體的工件( classes, modules, functions 等) 應該對擴充套件開放,對修改關閉。” 然而這句話意味著什麼呢?這個原則大體上表示你 應該允許在不改變已有程式碼的情況下增加新的功能

壞:

  1. abstract class Adapter

  2. {

  3. protected $name;

  4. public function getName(): string

  5. {

  6. return $this->name;

  7. }

  8. }

  9. class AjaxAdapter extends Adapter

  10. {

  11. public function __construct()

  12. {

  13. parent::__construct();

  14. $this->name = 'ajaxAdapter';

  15. }

  16. }

  17. class NodeAdapter extends Adapter

  18. {

  19. public function __construct()

  20. {

  21. parent::__construct();

  22. $this->name = 'nodeAdapter';

  23. }

  24. }

  25. class HttpRequester

  26. {

  27. private $adapter;

  28. public function __construct(Adapter $adapter)

  29. {

  30. $this->adapter = $adapter;

  31. }

  32. public function fetch(string $url): Promise

  33. {

  34. $adapterName = $this->adapter->getName();

  35. if ($adapterName === 'ajaxAdapter') {

  36. return $this->makeAjaxCall($url);

  37. } elseif ($adapterName === 'httpNodeAdapter') {

  38. return $this->makeHttpCall($url);

  39. }

  40. }

  41. private function makeAjaxCall(string $url): Promise

  42. {

  43. // request and return promise

  44. }

  45. private function makeHttpCall(string $url): Promise

  46. {

  47. // request and return promise

  48. }

  49. }

好:

  1. interface Adapter

  2. {

  3. public function request(string $url): Promise;

  4. }

  5. class AjaxAdapter implements Adapter

  6. {

  7. public function request(string $url): Promise

  8. {

  9. // request and return promise

  10. }

  11. }

  12. class NodeAdapter implements Adapter

  13. {

  14. public function request(string $url): Promise

  15. {

  16. // request and return promise

  17. }

  18. }

  19. class HttpRequester

  20. {

  21. private $adapter;

  22. public function __construct(Adapter $adapter)

  23. {

  24. $this->adapter = $adapter;

  25. }

  26. public function fetch(string $url): Promise

  27. {

  28. return $this->adapter->request($url);

  29. }

  30. }

里氏替換原則

Liskov Substitution Principle (LSP)

這是一個簡單的原則,卻用了一個不好理解的術語。它的正式定義是 “如果S是T的子型別,那麼在不改變程式原有既定屬性(檢查、執行 任務等)的前提下,任何T型別的物件都可以使用S型別的物件替代 (例如,使用S的物件可以替代T的物件)” 這個定義更難理解:-)。

對這個概念最好的解釋是:如果你有一個父類和一個子類,在不改變 原有結果正確性的前提下父類和子類可以互換。這個聽起來依舊讓人 有些迷惑,所以讓我們來看一個經典的正方形-長方形的例子。從數學 上講,正方形是一種長方形,但是當你的模型通過繼承使用了”is-a” 的關係時,就不對了。

壞:

  1. class Rectangle

  2. {

  3. protected $width = 0;

  4. protected $height = 0;

  5. public function setWidth(int $width): void

  6. {

  7. $this->width = $width;

  8. }

  9. public function setHeight(int $height): void

  10. {

  11. $this->height = $height;

  12. }

  13. public function getArea(): int

  14. {

  15. return $this->width * $this->height;

  16. }

  17. }

  18. class Square extends Rectangle

  19. {

  20. public function setWidth(int $width): void

  21. {

  22. $this->width = $this->height = $width;

  23. }

  24. public function setHeight(int $height): void

  25. {

  26. $this->width = $this->height = $height;

  27. }

  28. }

  29. function printArea(Rectangle $rectangle): void

  30. {

  31. $rectangle->setWidth(4);

  32. $rectangle->setHeight(5);

  33. // BAD: Will return 25 for Square. Should be 20.

  34. echo sprintf('%s has area %d.', get_class($rectangle), $rectangle->getArea()).PHP_EOL;

  35. }

  36. $rectangles = [new Rectangle(), new Square()];

  37. foreach ($rectangles as $rectangle) {

  38. printArea($rectangle);

  39. }

好:

最好是將這兩種四邊形分別對待,用一個適合兩種型別的更通用子型別來代替。

儘管正方形和長方形看起來很相似,但他們是不同的。 正方形更接近菱形,而長方形更接近平行四邊形。但他們不是子型別。 儘管相似,正方形、長方形、菱形、平行四邊形都是有自己屬性的不同形狀。

  1. interface Shape

  2. {

  3. public function getArea(): int;

  4. }

  5. class Rectangle implements Shape

  6. {

  7. private $width = 0;

  8. private $height = 0;

  9. public function __construct(int $width, int $height)

  10. {

  11. $this->width = $width;

  12. $this->height = $height;

  13. }

  14. public function getArea(): int

  15. {

  16. return $this->width * $this->height;

  17. }

  18. }

  19. class Square implements Shape

  20. {

  21. private $length = 0;

  22. public function __construct(int $length)

  23. {

  24. $this->length = $length;

  25. }

  26. public function getArea(): int

  27. {

  28. return $this->length ** 2;

  29. }

  30. }

  31. function printArea(Shape $shape): void

  32. {

  33. echo sprintf('%s has area %d.', get_class($shape), $shape->getArea()).PHP_EOL;

  34. }

  35. $shapes = [new Rectangle(4, 5), new Square(5)];

  36. foreach ($shapes as $shape) {

  37. printArea($shape);

  38. }

介面隔離原則

Interface Segregation Principle (ISP)

介面隔離原則表示:”呼叫方不應該被強制依賴於他不需要的介面”

有一個清晰的例子來說明示範這條原則。當一個類需要一個大量的設定項, 為了方便不會要求呼叫方去設定大量的選項,因為在通常他們不需要所有的 設定項。使設定項可選有助於我們避免產生”胖介面”

壞:

  1. interface Employee

  2. {

  3. public function work(): void;

  4. public function eat(): void;

  5. }

  6. class HumanEmployee implements Employee

  7. {

  8. public function work(): void

  9. {

  10. // ....working

  11. }

  12. public function eat(): void

  13. {

  14. // ...... eating in lunch break

  15. }

  16. }

  17. class RobotEmployee implements Employee

  18. {

  19. public function work(): void

  20. {

  21. //.... working much more

  22. }

  23. public function eat(): void

  24. {

  25. //.... robot can't eat, but it must implement this method

  26. }

  27. }

好:

不是每一個工人都是僱員,但是每一個僱員都是一個工人

  1. interface Workable

  2. {

  3. public function work(): void;

  4. }

  5. interface Feedable

  6. {

  7. public function eat(): void;

  8. }

  9. interface Employee extends Feedable, Workable

  10. {

  11. }

  12. class HumanEmployee implements Employee

  13. {

  14. public function work(): void

  15. {

  16. // ....working

  17. }

  18. public function eat(): void

  19. {

  20. //.... eating in lunch break

  21. }

  22. }

  23. // robot can only work

  24. class RobotEmployee implements Workable

  25. {

  26. public function work(): void

  27. {

  28. // ....working

  29. }

  30. }

依賴倒置原則

Dependency Inversion Principle (DIP)

這條原則說明兩個基本的要點:

高階的模組不應該依賴低階的模組,它們都應該依賴於抽象
抽象不應該依賴於實現,實現應該依賴於抽象
這條起初看起來有點晦澀難懂,但是如果你使用過 PHP 框架(例如 Symfony),你應該見過 依賴注入(DI),它是對這個概念的實現。雖然它們不是完全相等的概念,依賴倒置原則使高階模組 與低階模組的實現細節和建立分離。可以使用依賴注入(DI)這種方式來實現它。最大的好處 是它使模組之間解耦。耦合會導致你難於重構,它是一種非常糟糕的的開發模式。

壞:

  1. class Employee

  2. {

  3. public function work(): void

  4. {

  5. // ....working

  6. }

  7. }

  8. class Robot extends Employee

  9. {

  10. public function work(): void

  11. {

  12. //.... working much more

  13. }

  14. }

  15. class Manager

  16. {

  17. private $employee;

  18. public function __construct(Employee $employee)

  19. {

  20. $this->employee = $employee;

  21. }

  22. public function manage(): void

  23. {

  24. $this->employee->work();

  25. }

  26. }

好:

  1. interface Employee

  2. {

  3. public function work(): void;

  4. }

  5. class Human implements Employee

  6. {

  7. public function work(): void

  8. {

  9. // ....working

  10. }

  11. }

  12. class Robot implements Employee

  13. {

  14. public function work(): void

  15. {

  16. //.... working much more

  17. }

  18. }

  19. class Manager

  20. {

  21. private $employee;

  22. public function __construct(Employee $employee)

  23. {

  24. $this->employee = $employee;

  25. }

  26. public function manage(): void

  27. {

  28. $this->employee->work();

  29. }

  30. }

別寫重複程式碼 (DRY)

試著去遵循DRY 原則.

盡你最大的努力去避免複製程式碼,它是一種非常糟糕的行為,複製程式碼 通常意味著當你需要變更一些邏輯時,你需要修改不止一處。

試想一下,如果你在經營一家餐廳並且你在記錄你倉庫的進銷記錄:所有 的土豆,洋蔥,大蒜,辣椒等。如果你有多個列表來管理進銷記錄,當你 用其中一些土豆做菜時你需要更新所有的列表。如果你只有一個列表的話 只有一個地方需要更新。

通常情況下你複製程式碼是應該有兩個或者多個略微不同的邏輯,它們大多數 都是一樣的,但是由於它們的區別致使你必須有兩個或者多個隔離的但大部 分相同的方法,移除重複的程式碼意味著用一個function/module/class創 建一個能處理差異的抽象。

用對抽象非常關鍵,這正是為什麼你必須學習遵守在類章節寫 的SOLID原則,不合理的抽象比複製程式碼更糟糕,所以務必謹慎!說了這麼多, 如果你能設計一個合理的抽象,那就這麼幹!別寫重複程式碼,否則你會發現 任何時候當你想修改一個邏輯時你必須修改多個地方。

壞:

  1. function showDeveloperList(array $developers): void

  2. {

  3. foreach ($developers as $developer) {

  4. $expectedSalary = $developer->calculateExpectedSalary();

  5. $experience = $developer->getExperience();

  6. $githubLink = $developer->getGithubLink();

  7. $data = [

  8. $expectedSalary,

  9. $experience,

  10. $githubLink

  11. ];

  12. render($data);

  13. }

  14. }

  15. function showManagerList(array $managers): void

  16. {

  17. foreach ($managers as $manager) {

  18. $expectedSalary = $manager->calculateExpectedSalary();

  19. $experience = $manager->getExperience();

  20. $githubLink = $manager->getGithubLink();

  21. $data = [

  22. $expectedSalary,

  23. $experience,

  24. $githubLink

  25. ];

  26. render($data);

  27. }

  28. }

好:

  1. function showList(array $employees): void

  2. {

  3. foreach ($employees as $employee) {

  4. $expectedSalary = $employee->calculateExpectedSalary();

  5. $experience = $employee->getExperience();

  6. $githubLink = $employee->getGithubLink();

  7. $data = [

  8. $expectedSalary,

  9. $experience,

  10. $githubLink

  11. ];

  12. render($data);

  13. }

  14. }

極好:

最好讓程式碼緊湊一點

function showList(array $employees): void
{
foreach ($employees as $employee) {
render([
$employee->calculateExpectedSalary(),
$employee->getExperience(),
$employee->getGithubLink()
]);
}
}

轉摘

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章