[精選]Clean PHP Code(清晰的PHP程式碼思路)

Weiwen發表於2022-07-26

變數

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

不友好的:

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

友好的:

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

不友好的:

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

友好的:

getUser();
  • 使用可以查詢到的變數

我們讀的程式碼量遠比我們寫過的多。因此,寫出可閱讀和便於搜尋的程式碼是及其重要的。在我們的程式中寫出一些難以理解的變數名
到最後甚至會讓自己非常傷腦筋。
因此,讓你的名字便於搜尋吧。

不友好的:

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

友好的:

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

不友好的

$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

本作品採用《CC 協議》,轉載必須註明作者和本文連結
最美的不是下雨天,而是和你一起躲過的屋簷!

相關文章