PHP簡潔之道

前言

原文地址:https://github.com/jupeter/cl...
譯文地址:https://github.com/nineyang/c...,歡迎starphp


變量

  • 使用更有意義和更加直白的命名方式

不友好的:html

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

友好的:node

$currentDate = $moment->format('y-m-d');
  • 對於同一實體使用相同變量名

不友好的:git

getUserInfo();
getUserData();
getUserRecord();
getUserProfile();

友好的:github

getUser();
  • 使用能夠查找到的變量

咱們讀的代碼量遠比咱們寫過的多。所以,寫出可閱讀和便於搜索的代碼是及其重要的。在咱們的程序中寫出一些難以理解的變量名
到最後甚至會讓本身很是傷腦筋。
所以,讓你的名字便於搜索吧。ajax

不友好的:json

// 這裏的448表明什麼意思呢?
$result = $serializer->serialize($data, 448);

友好的:數組

$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | >JSON_UNESCAPED_UNICODE);
  • 使用解釋型變量

不友好的promise

$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);

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

不至於那麼糟糕的:安全

稍微好一些,可是這取決於咱們對正則的熟練程度。
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);

[, $city, $zipCode] = $matches;
saveCityZipCode($city, $zipCode);

友好的:

經過對子模式的重命名減小了咱們對正則的熟悉和依賴程度。
$address = 'One Infinite Loop, Cupertino 95014';
$cityZipCodeRegex = '/^[^,]+,\s*(?<city>.+?)\s*(?<zipCode>\d{5})$/';
preg_match($cityZipCodeRegex, $address, $matches);

saveCityZipCode($matches['city'], $matches['zipCode']);
  • 嵌套無邊,懸崖勒馬

太多的if else嵌套會讓你的代碼難以閱讀和維護。更加直白的代碼會好不少。

  1. demo1

不友好的:

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);
}
  1. demo2

不友好的:

function fibonacci(int $n)
{
   if ($n < 50) {
       if ($n !== 0) {
           if ($n !== 1) {
               return fibonacci($n - 1) + fibonacci($n - 2);
           } else {
               return 1;
           }
       } else {
           return 0;
       }
   } else {
       return 'Not supported';
   }
}

友好的:

function fibonacci(int $n): int
{
   if ($n === 0 || $n === 1) {
       return $n;
   }

   if ($n > 50) {
       throw new \Exception('Not supported');
   }

   return fibonacci($n - 1) + fibonacci($n - 2);
}
  • 避免使用不合理的變量名

別讓其餘人去猜你的變量名的意思。
更加直白的代碼會好不少。

不友好的:

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

for ($i = 0; $i < count($l); $i++) {
   $li = $l[$i];
   doStuff();
   doSomeOtherStuff();
   // ...
   // ...
   // ...
   // 等等,這個$li是什麼意思?
   dispatch($li);
}

友好的:

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

foreach ($locations as $location) {
   doStuff();
   doSomeOtherStuff();
   // ...
   // ...
   // ...
   dispatch($location);
}
  • 別添加不必的上下文

若是你的類或對象的名字已經傳達了一些信息,那麼請別在變量名中重複。

不友好的

class Car
{
   public $carMake;
   public $carModel;
   public $carColor;

   //...
}

友好的

class Car
{
   public $make;
   public $model;
   public $color;

   //...
}
  • 用參數默認值代替短路運算或條件運算

不友好的

這裏不太合理,由於變量$breweryName有多是NULL

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

不至於那麼糟糕的

這種寫法要比上一版稍微好理解一些,可是若是能控制變量值獲取會更好。

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

友好的

若是你僅支持 PHP 7+,那麼你可使用類型約束而且保證$http://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration變量不會爲NULL

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

函數

  • 函數參數應該控制在兩個如下

限制函數的參數對於在對函數作測試來講至關重要。有超過三個可選的參數會給你的測試工做量帶來倍速增加。

最理想的狀況是沒有參數。1-2個參數也還湊合,可是三個參數就應該避免了。參數越多,咱們須要維護的就越多。一般,若是你的函>數有超過2個的參數,那麼你的這個函數須要處理的事情就太多了。若是的確須要這麼多參數,那麼在大多數狀況下, 用一個對象來處理可能會更合適。

不友好的:

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

友好的:

class MenuConfig
{
   public $title;
   public $body;
   public $buttonText;
   public $cancellable = false;
}

$config = new MenuConfig();
$config->title = 'Foo';
$config->body = 'Bar';
$config->buttonText = 'Baz';
$config->cancellable = true;

function createMenu(MenuConfig $config): void
{
   // ...
}
  • 一個函數只作一件事情

在軟件工程行業,這是最重要的準則。當函數所處理的事情超過一件,他就會變得難以實現,測試和理解。當你能讓一個函數僅僅負責一個事情,他們就會變得容易重構而且理解起來越清晰。光是執行這樣一條原則就能讓你成爲開發者中的佼佼者了。

不友好的:

function emailClients(array $clients): void
{
   foreach ($clients as $client) {
       $clientRecord = $db->find($client);
       if ($clientRecord->isActive()) {
           email($client);
       }
   }
}

友好的:

function emailClients(array $clients): void
{
   $activeClients = activeClients($clients);
   array_walk($activeClients, 'email');
}

function activeClients(array $clients): array
{
   return array_filter($clients, 'isClientActive');
}

function isClientActive(int $client): bool
{
   $clientRecord = $db->find($client);

   return $clientRecord->isActive();
}
  • 函數名應該說到作到

不友好的:

class Email
{
   //...

   public function handle(): void
   {
       mail($this->to, $this->subject, $this->body);
   }
}

$message = new Email(...);
// 這是什麼?這個`handle`方法是什麼?咱們如今應該寫入到一個文件嗎?
$message->handle();

友好的:

class Email 
{
   //...

   public function send(): void
   {
       mail($this->to, $this->subject, $this->body);
   }
}

$message = new Email(...);
// 清晰而且顯而易見
$message->send();
  • 函數應該只有一層抽象

當你的函數有超過一層的抽象時便意味着這個函數作了太多事情。解耦這個函數導致其變得可重用和更易測試。

不友好的:

function parseBetterJSAlternative(string $code): void
{
   $regexes = [
       // ...
   ];

   $statements = explode(' ', $code);
   $tokens = [];
   foreach ($regexes as $regex) {
       foreach ($statements as $statement) {
           // ...
       }
   }

   $ast = [];
   foreach ($tokens as $token) {
       // lex...
   }

   foreach ($ast as $node) {
       // parse...
   }
}

一樣不太友好:

咱們已經從函數中拆分除了一些東西出來,可是 parseBetterJSAlternative()這個函數仍是太複雜以致於難以測試。
function tokenize(string $code): array
{
   $regexes = [
       // ...
   ];

   $statements = explode(' ', $code);
   $tokens = [];
   foreach ($regexes as $regex) {
       foreach ($statements as $statement) {
           $tokens[] = /* ... */;
       }
   }

   return $tokens;
}

function lexer(array $tokens): array
{
   $ast = [];
   foreach ($tokens as $token) {
       $ast[] = /* ... */;
   }

   return $ast;
}

function parseBetterJSAlternative(string $code): void
{
   $tokens = tokenize($code);
   $ast = lexer($tokens);
   foreach ($ast as $node) {
       // parse...
   }
}

友好的

最優解就是把 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) {
           // parse...
       }
   }
}
  • 不要在函數中帶入flag相關的參數

當你使用flag時便意味着你的函數作了超過一件事情。前面咱們也提到了,函數應該只作一件事情。若是你的代碼取決於一個boolean,那麼仍是把這些內容拆分出來吧。

不友好的

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);
}
  • 避免函數帶來的反作用

當函數有數據的輸入和輸出時可能會產生反作用。這個反作用可能被寫入一個文件,修改一些全局變量,或者意外的把你的錢轉給一個陌生人。

此刻,你可能有時候會須要這些反作用。正如前面所說,你可能須要寫入到一個文件中。你須要注意的是把這些你所作的東西在你的掌控之下。別讓某些個別函數或者類寫入了一個特別的文件。對於全部的都應該一視同仁。有且僅有一個結果。

重要的是要避免那些譬如共享無結構的對象,使用能夠寫入任何類型的可變數據,不對反作用進行集中處理等常見的陷阱。若是你能夠作到,你將會比大多數程序猿更加輕鬆。

不友好的

// 全局變量被下面的函數引用了。
// 若是咱們在另一個函數中使用了這個`$name`變量,那麼可能會變成一個數組或者程序被打斷。
$name = 'Ryan McDermott';

function splitIntoFirstAndLastName(): void
{
   global $name;

   $name = explode(' ', $name);
}

splitIntoFirstAndLastName();

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

友好的

function splitIntoFirstAndLastName(string $name): array
{
   return explode(' ', $name);
}

$name = 'Ryan McDermott';
$newName = splitIntoFirstAndLastName($name);

var_dump($name); // 'Ryan McDermott';
var_dump($newName); // ['Ryan', 'McDermott'];
  • 避免寫全局方法

在大多數語言中,全局變量被污染都是一個不太好的實踐,由於當你引入另外的包時會起衝突而且使用你的API的人知道拋出了一個異常才明白。咱們假設一個簡單的例子:若是你想要一個配置數組。你可能會寫一個相似於config()的全局的函數,可是在引入其餘包並在其餘地方嘗試作一樣的事情時會起衝突。

不友好的

function config(): array
{
   return  [
       'foo' => 'bar',
   ]
}

不友好的

class Configuration
{
   private $configuration = [];

   public function __construct(array $configuration)
   {
       $this->configuration = $configuration;
   }

   public function get(string $key): ?string
   {
       return isset($this->configuration[$key]) ? $this->configuration[$key] : null;
   }
}
經過建立 Configuration類的實例來引入配置
$configuration = new Configuration([
   'foo' => 'bar',
]);
至此,你就能夠是在你的項目中使用這個配置了。
  • 避免使用單例模式

單例模式是一種反模式。爲何不建議使用:

  1. 他們一般使用一個全局實例,爲何這麼糟糕?由於你隱藏了依賴關係在你的項目的代碼中,而不是經過接口暴露出來。你應該有意識的去避免那些全局的東西。
  2. 他們違背了單一職責原則:他們會本身控制本身的生命週期
  3. 這種模式會天然而然的使代碼耦合在一塊兒。這會讓他們在測試中,不少狀況下都理所固然的不一致
  4. 他們持續在整個項目的生命週期中。另一個嚴重的打擊是當你須要排序測試的時候,在單元測試中這會是一個不小的麻煩。爲何?由於每一個單元測試都應該依賴於另一個。

不友好的

class DBConnection
{
   private static $instance;

   private function __construct(string $dsn)
   {
       // ...
   }

   public static function getInstance(): DBConnection
   {
       if (self::$instance === null) {
           self::$instance = new self();
       }

       return self::$instance;
   }

   // ...
}

$singleton = DBConnection::getInstance();

友好的

class DBConnection
{
   public function __construct(string $dsn)
   {
       // ...
   }

    // ...
}
使用 DSN配置來建立一個 DBConnection類的單例。
$connection = new DBConnection($dsn);
此時,在你的項目中必須使用 DBConnection的單例。
  • 對條件判斷進行包裝

不友好的

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

友好的

if ($article->isPublished()) {
   // ...
}
  • 避免對條件取反

不友好的

function isDOMNodeNotPresent(\DOMNode $node): bool
{
   // ...
}

if (!isDOMNodeNotPresent($node))
{
   // ...
}

友好的

function isDOMNodePresent(\DOMNode $node): bool
{
   // ...
}

if (isDOMNodePresent($node)) {
   // ...
}
  • 避免太多的條件嵌套

這彷佛是一個不可能的任務。不少人的腦海中可能會在第一時間縈繞「若是沒有if條件我還能作什麼呢?」。答案就是,在大多數狀況下,你可使用多態去處理這個難題。此外,可能有人又會說了,「即便多態能夠作到,可是咱們爲何要這麼作呢?」,對此咱們的解釋是,一個函數應該只作一件事情,這也正是咱們在前面所提到的讓代碼更加整潔的原則。當你的函數中使用了太多的if條件時,便意味着你的函數作了超過一件事情。牢記:要專注。

不友好的:

class Airplane
{
   // ...

   public function getCruisingAltitude(): int
   {
       switch ($this->type) {
           case '777':
               return $this->getMaxAltitude() - $this->getPassengerCount();
           case 'Air Force One':
               return $this->getMaxAltitude();
           case 'Cessna':
               return $this->getMaxAltitude() - $this->getFuelExpenditure();
       }
   }
}

友好的:

interface Airplane
{
   // ...

   public function getCruisingAltitude(): int;
}

class Boeing777 implements Airplane
{
   // ...

   public function getCruisingAltitude(): int
   {
       return $this->getMaxAltitude() - $this->getPassengerCount();
   }
}

class AirForceOne implements Airplane
{
   // ...

   public function getCruisingAltitude(): int
   {
       return $this->getMaxAltitude();
   }
}

class Cessna implements Airplane
{
   // ...

   public function getCruisingAltitude(): int
   {
       return $this->getMaxAltitude() - $this->getFuelExpenditure();
   }
}
  • 避免類型檢測 (part 1)

PHP是一門弱類型語言,這意味着你的函數可使用任何類型的參數。他在給予你無限的自由的同時又讓你困擾,由於有有時候你須要作類型檢測。這裏有不少方式去避免這種事情,第一種方式就是統一API

不友好的:

function travelToTexas($vehicle): void
{
   if ($vehicle instanceof Bicycle) {
       $vehicle->pedalTo(new Location('texas'));
   } elseif ($vehicle instanceof Car) {
       $vehicle->driveTo(new Location('texas'));
   }
}

友好的:

function travelToTexas(Traveler $vehicle): void
{
   $vehicle->travelTo(new Location('texas'));
}
  • 避免類型檢測 (part 2)

若是你正使用諸如字符串、整型和數組等基本類型,且要求版本是PHP 7+,不能使用多態,須要類型檢測,那你應當考慮類型聲明或者嚴格模式。它提供了基於標準PHP語法的靜態類型。手動檢查類型的問題是作好了須要好多的廢話,好像爲了安全就能夠不顧損失可讀性。保持你的PHP代碼整潔,寫好測試,保持良好的回顧代碼的習慣。不然的話,那就仍是用PHP嚴格類型聲明和嚴格模式來確保安全吧。

不友好的:

function combine($val1, $val2): int
{
   if (!is_numeric($val1) || !is_numeric($val2)) {
       throw new \Exception('Must be of type Number');
   }

   return $val1 + $val2;
}

友好的:

function combine(int $val1, int $val2): int
{
   return $val1 + $val2;
}
  • 移除那些沒有使用的代碼

沒有再使用的代碼就比如重複代碼同樣糟糕。在你的代碼庫中徹底沒有必要保留。若是肯定再也不使用,那就把它刪掉吧!若是有一天你要使用,你也能夠在你的版本記錄中找到它。

不友好的:

function oldRequestModule(string $url): void
{
   // ...
}

function newRequestModule(string $url): void
{
   // ...
}

$request = newRequestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');

友好的:

function requestModule(string $url): void
{
   // ...
}

$request = requestModule($requestUrl);
inventoryTracker('apples', $request, 'www.inventory-awesome.io');

對象和數據結構

  • 使用對象封裝

PHP中你能夠設置publicprotected,和private關鍵詞來修飾你的方法。當你使用它們,你就能夠在一個對象中控制這些屬性的修改權限了。

  • 當你想要對對象的屬性進行除了「獲取」以外的操做時,你沒必要再去瀏覽並在代碼庫中修改權限。
  • 當你要作一些修改屬性的操做時,你更易於在代碼中作邏輯驗證。
  • 封裝內部表示。
  • 當你在作獲取和設置屬性的操做時,更易於添加logerror的操做。
  • 當其餘class繼承了這個基類,你能夠重寫默認的方法。
  • 你能夠爲一個服務延遲的去獲取這個對象的屬性值。

不太友好的:

class BankAccount
{
   public $balance = 1000;
}

$bankAccount = new BankAccount();

// Buy shoes...
$bankAccount->balance -= 100;

友好的:

class BankAccount
{
   private $balance;

   public function __construct(int $balance = 1000)
   {
     $this->balance = $balance;
   }

   public function withdraw(int $amount): void
   {
       if ($amount > $this->balance) {
           throw new \Exception('Amount greater than available balance.');
       }

       $this->balance -= $amount;
   }

   public function deposit(int $amount): void
   {
       $this->balance += $amount;
   }

   public function getBalance(): int
   {
       return $this->balance;
   }
}

$bankAccount = new BankAccount();

// Buy shoes...
$bankAccount->withdraw($shoesPrice);

// Get balance
$balance = $bankAccount->getBalance();
  • 在對象的屬性上可使用private/protected限定
  • public修飾的方法和屬性同上來講被修改是比較危險的,由於一些外部的代碼能夠輕易的依賴於他們而且你沒辦法控制哪些代碼依賴於他們。對於全部用戶的類來講,在類中能夠修改是至關危險的。
  • protected修飾器和public一樣危險,由於他們在繼承鏈中一樣能夠操做。兩者的區別僅限於權限機制,而且封裝保持不變。對於全部子類來講,在類中修改也是至關危險的。
  • private修飾符保證了代碼只有在本身類的內部修改纔是危險的。

所以,當你在須要對外部的類設置權限時使用private修飾符去取代public/protected吧。

若是須要了解更多信息你能夠讀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在著名的Design Patterns中所說,你應該儘量的使用組合而不是繼承。無論是使用組合仍是繼承都有不少的優勢。最重要的一個準則在於當你本能的想要使用繼承時,不妨思考一下組合是否能讓你的問題解決的更加優雅。在某些時候確實如此。

你可能會這麼問了,「那到底何時我應該使用繼承呢?」這徹底取決你你手頭上的問題,下面正好有一些繼承優於組合的例子:

  1. 你的繼承表達了「是一個」而不是「有一個」的關係(Human->Animal vs. User->UserDetails)。
  2. 你可能會重複的使用基類的代碼(Humans can move like all animals)。
  3. 你渴望在修改代碼的時候經過基類來統一調度(Change the caloric expenditure of all animals when they move)。

不友好的:

class Employee 
{
   private $name;
   private $email;

   public function __construct(string $name, string $email)
   {
       $this->name = $name;
       $this->email = $email;
   }

   // ...
}

// 這裏不太合理的緣由在於並不是全部的職員都有`tax`這個特徵。

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);
   }

   // ...
}
  • 避免鏈式調用(連貫接口)

在使用一些鏈式方法時,這種連貫接口能夠不斷地指向當前對象讓咱們的代碼顯得更加清晰可讀。

一般狀況下,咱們在構建對象時均可以利用他的上下文這一特徵,由於這種模式能夠減小代碼的冗餘,不過在PHPUnit Mock Builder或者Doctrine Query Builder所說起的,有時候這種方式會帶來一些麻煩:

  1. 破壞封裝
  2. 破壞設計
  3. 難以測試
  4. 可能會難以閱讀

若是須要了解更多信息你能夠讀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();

SOLID

SOLID最開始是由Robert Martin提出的五個準則,並最後由Michael Feathers命名的簡寫,這五個是在面對對象設計中的五個基本原則。

  • S: 職責單一原則 (SRP)
  • O: 開閉原則 (OCP)
  • L: 里氏替換原則 (LSP)
  • I: 接口隔離原則 (ISP)
  • D: 依賴反轉原則 (DIP)
  • 職責單一原則 (SRP)

正如Clean Code所述,「修改類應該只有一個理由」。咱們老是喜歡在類中寫入太多的方法,就像你在飛機上塞滿你的行李箱。在這種狀況下你的類沒有高內聚的概念而且留下了不少能夠修改的理由。儘量的減小你須要去修改類的時間是很是重要的。若是在你的單個類中有太多的方法而且你常常修改的話,那麼若是其餘代碼庫中有引入這樣的模塊的話會很是難以理解。

不友好的:

class UserSettings
{
   private $user;

   public function __construct(User $user)
   {
       $this->user = $user;
   }

   public function changeSettings(array $settings): void
   {
       if ($this->verifyCredentials()) {
           // ...
       }
   }

   private function verifyCredentials(): bool
   {
       // ...
   }
}

友好的:

class UserAuth 
{
   private $user;

   public function __construct(User $user)
   {
       $this->user = $user;
   }
 
   public function verifyCredentials(): bool
   {
       // ...
   }
}

class UserSettings 
{
   private $user;
   private $auth;

   public function __construct(User $user) 
   {
       $this->user = $user;
       $this->auth = new UserAuth($user);
   }

   public function changeSettings(array $settings): void
   {
       if ($this->auth->verifyCredentials()) {
           // ...
       }
   }
}
  • 開閉原則 (OCP)

正如Bertrand Meyer所說,「軟件開發應該對擴展開發,對修改關閉。」這是什麼意思呢?這個原則的意思大概就是說你應該容許其餘人在不修改已經存在的功能的狀況下去增長新功能。

不友好的

abstract class Adapter
{
   protected $name;

   public function getName(): string
   {
       return $this->name;
   }
}

class AjaxAdapter extends Adapter
{
   public function __construct()
   {
      parent::__construct();

       $this->name = 'ajaxAdapter';
   }
}

class NodeAdapter extends Adapter
{
   public function __construct()
   {
       parent::__construct();

       $this->name = 'nodeAdapter';
   }
}

class HttpRequester
{
   private $adapter;

   public function __construct(Adapter $adapter)
   {
       $this->adapter = $adapter;
   }

   public function fetch(string $url): Promise
   {
       $adapterName = $this->adapter->getName();

       if ($adapterName === 'ajaxAdapter') {
           return $this->makeAjaxCall($url);
       } elseif ($adapterName === 'httpNodeAdapter') {
           return $this->makeHttpCall($url);
       }
   }

   private function makeAjaxCall(string $url): Promise
   {
       // request and return promise
   }

   private function makeHttpCall(string $url): Promise
   {
       // request and return promise
   }
}

友好的:

interface Adapter
{
   public function request(string $url): Promise;
}

class AjaxAdapter implements Adapter
{
   public function request(string $url): Promise
   {
       // request and return promise
   }
}

class NodeAdapter implements Adapter
{
   public function request(string $url): Promise
   {
       // request and return promise
   }
}

class HttpRequester
{
   private $adapter;

   public function __construct(Adapter $adapter)
   {
       $this->adapter = $adapter;
   }

   public function fetch(string $url): Promise
   {
       return $this->adapter->request($url);
   }
}
  • 里氏替換原則 (LSP)

這自己是一個很是簡單的原則卻起了一個不太容易理解的名字。這個原則一般的定義是「若是S是T的一個子類,那麼對象T能夠在沒有任何警告的狀況下被他的子類替換(例如:對象S可能代替對象T)一些更合適的屬性。」好像更難理解了。

最好的解釋就是說若是你有一個父類和子類,那麼你的父類和子類能夠在原來的基礎上任意交換。這個可能仍是難以理解,咱們舉一個正方形-長方形的例子吧。在數學中,一個矩形屬於長方形,可是若是在你的模型中經過繼承使用了「is-a」的關係就不對了。

不友好的:

class Rectangle
{
   protected $width = 0;
   protected $height = 0;

   public function render(int $area): void
   {
       // ...
   }

   public function setWidth(int $width): void
   {
       $this->width = $width;
   }

   public function setHeight(int $height): void
   {
       $this->height = $height;
   }

   public function getArea(): int
   {
      return $this->width * $this->height;
   }
}

class Square extends Rectangle
{
   public function setWidth(int $width): void
   {
       $this->width = $this->height = $width;
   }

   public function setHeight(int $height): void
   {
       $this->width = $this->height = $height;
   }
}

function renderLargeRectangles(array $rectangles): void
{
   foreach ($rectangles as $rectangle) {
       $rectangle->setWidth(4);
       $rectangle->setHeight(5);
       $area = $rectangle->getArea(); // BAD: Will return 25 for Square. Should be 20.
       $rectangle->render($area);
   }
}

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

友好的:

abstract class Shape
{
   protected $width = 0;
   protected $height = 0;

   abstract public function getArea(): int;

   public function render(int $area): void
  {
       // ...
   }
}

class Rectangle extends Shape
{
   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 Shape
{
   private $length = 0;

   public function setLength(int $length): void
   {
       $this->length = $length;
   }

   public function getArea(): int
   {
       return pow($this->length, 2);
   }
}


function renderLargeRectangles(array $rectangles): void
{
   foreach ($rectangles as $rectangle) {
       if ($rectangle instanceof Square) {
           $rectangle->setLength(5);
       } elseif ($rectangle instanceof Rectangle) {
           $rectangle->setWidth(4);
           $rectangle->setHeight(5);
       }

       $area = $rectangle->getArea(); 
       $rectangle->render($area);
   }
}

$shapes = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles($shapes);
  • 接口隔離原則 (ISP)

ISP的意思就是說「使用者不該該強制使用它不須要的接口」。

當一個類須要大量的設置是一個不錯的例子去解釋這個原則。爲了方便去調用這個接口須要作大量的設置,可是大多數狀況下是不須要的。強制讓他們使用這些設置會讓整個接口顯得臃腫。

不友好的:

interface Employee
{
   public function work(): void;

   public function eat(): void;
}

class Human implements Employee
{
   public function work(): void
   {
       // ....working
   }

   public function eat(): void
   {
       // ...... eating in lunch break
   }
}

class Robot implements Employee
{
   public function work(): void
   {
       //.... working much more
   }

   public function eat(): void
   {
       //.... robot can't eat, but it must implement this method
   }
}

友好的:

並不是每個工人都是職員,可是每個職員都是工人。
interface Workable
{
   public function work(): void;
}

interface Feedable
{
   public function eat(): void;
}

interface Employee extends Feedable, Workable
{
}

class Human implements Employee
{
   public function work(): void
   {
       // ....working
   }

   public function eat(): void
   {
       //.... eating in lunch break
   }
}

// robot can only work
class Robot implements Workable
{
   public function work(): void
   {
       // ....working
   }
}
  • 依賴反轉原則 (DIP)

這個原則有兩個須要注意的地方:

  1. 高階模塊不能依賴於低階模塊。他們都應該依賴於抽象。
  2. 抽象不該該依賴於實現,實現應該依賴於抽象。

第一點可能有點難以理解,可是若是你有使用過像SymfonyPHP框架,你應該有見到過依賴注入這樣的原則的實現。儘管他們是不同的概念,DIP讓高階模塊從咱們所知道的低階模塊中分離出去。能夠經過DI這種方式實現。一個巨大的好處在於它解耦了不一樣的模塊。耦合是一個很是很差的開發模式,由於它會讓你的代碼難以重構。

不友好的:

class Employee
{
   public function work(): void
   {
       // ....working
   }
}

class Robot extends Employee
{
   public function work(): void
   {
       //.... working much more
   }
}

class Manager
{
   private $employee;

   public function __construct(Employee $employee)
   {
       $this->employee = $employee;
   }

   public function manage(): void
   {
       $this->employee->work();
   }
}

友好的

interface Employee
{
   public function work(): void;
}

class Human implements Employee
{
   public function work(): void
   {
       // ....working
   }
}

class Robot implements Employee
{
   public function work(): void
   {
       //.... working much more
   }
}

class Manager
{
   private $employee;

   public function __construct(Employee $employee)
   {
       $this->employee = $employee;
   }

   public function manage(): void
   {
       $this->employee->work();
   }
}

別重複你的代碼 (DRY)

嘗試去研究DRY原則。

儘量別去複製代碼。複製代碼很是很差,由於這意味着未來有須要修改的業務邏輯時你須要修改不止一處。

想象一下你在經營一個餐館而且你須要常常整理你的存貨清單:你全部的土豆,洋蔥,大蒜,辣椒等。若是你有多個列表來管理進銷記錄,當你用其中一些土豆作菜時你須要更新全部的列表。若是你只有一個列表的話只有一個地方須要更新!

大多數狀況下你有重複的代碼是由於你有超過兩處細微的差異,他們大部分都是相同的,可是他們的不一樣之處又不得不讓你去分紅不一樣的方法去處理相同的事情。移除這些重複的代碼意味着你須要建立一個能夠用一個方法/模塊/類來處理的抽象。

使用一個抽象是關鍵的,這也是爲何在類中你要遵循SOLID原則的緣由。一個不優雅的抽象每每比重複的代碼更糟糕,因此要謹慎使用!說了這麼多,若是你已經能夠構造一個優雅的抽象,那就趕忙去作吧!別重複你的代碼,不然當你須要修改時你會發現你要修改許多地方。

不友好的:

function showDeveloperList(array $developers): void
{
   foreach ($developers as $developer) {
       $expectedSalary = $developer->calculateExpectedSalary();
       $experience = $developer->getExperience();
       $githubLink = $developer->getGithubLink();
       $data = [
           $expectedSalary,
           $experience,
           $githubLink
       ];

       render($data);
   }
}

function showManagerList(array $managers): void
{
   foreach ($managers as $manager) {
       $expectedSalary = $manager->calculateExpectedSalary();
       $experience = $manager->getExperience();
       $githubLink = $manager->getGithubLink();
       $data = [
           $expectedSalary,
           $experience,
           $githubLink
       ];

       render($data);
   }
}

友好的:

function showList(array $employees): void
{
   foreach ($employees as $employee) {
       $expectedSalary = $employee->calculateExpectedSalary();
       $experience = $employee->getExperience();
       $githubLink = $employee->getGithubLink();
       $data = [
           $expectedSalary,
           $experience,
           $githubLink
       ];

       render($data);
   }
}

很是優雅的:

若是能更簡潔那就更好了。
function showList(array $employees): void
{
   foreach ($employees as $employee) {
       render([
           $employee->calculateExpectedSalary(),
           $employee->getExperience(),
           $employee->getGithubLink()
       ]);
   }
}

原文地址

clean-code-php

文章首發地址:個人博客

相關文章
相關標籤/搜索