PHP 代碼簡潔之道

一、

PHP 簡潔代碼之道 clean-code-php, 是基於 Clean Code: A Handbook of Agile Software Craftmanship(Clean Code: 敏捷軟件開發工藝手冊) 這本書做的指南,該書是 Bob Martin 叔叔寫的關於如何編寫可維護代碼的經典書籍。

clean-code-php 指南的靈感來源於 Javascript 版本的 clean-code-javascript ,在其基礎加上了 PHP 的特點。

以下是我最喜歡的 clean-code-php 倉庫中的一些點:

不添加不必要的上下文

不好的:

<?php

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

    //...
}

好的:

<?php

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

    //...
}

函數參數(不要超過2個)

不好的:

<?php

function createMenu($title, $body, $buttonText, $cancellable) {
    // ...
}

好的:

<?php

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) {
    // ...
}

一個函數只做一件事情

不好的:

<?php

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

好的:

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

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

function isClientActive($client) {
    $clientRecord = $db->find($client);
    return $clientRecord->isActive();
}

觀點

項目作者概述了本指南的目的如下:

這裏的原則並不是所有的都必須遵守,而且幾乎只有少部分是被廣泛認同的。它們只是一些指導方針,而不是其他,但是它們都是 Clean Code的作者集多年編纂而成的。

在一門動態語言(像 PHP或其他任何語言)領域中,某些開發者可能會不同意其中的某些(或者很多)概念和觀點,但是我需要指出的是即使你對其中一些觀點不認同,也不要將它們全盤否定掉。

二、變量部分

使用有意義的並且可以讀出來的變量名稱

不好的:

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

使用易查找的名稱(第二部分)

不好的:

// 4 是什麼鬼?
if ($user->access & 4) {
    // ...
}

好的:

class User
{
    const ACCESS_READ = 1;
    const ACCESS_CREATE = 2;
    const ACCESS_UPDATE = 4;
    const ACCESS_DELETE = 8;
}

if ($user->access & User::ACCESS_UPDATE) {
    // do edit ...
}

使用可以解釋的變量

不好的:

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

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

好一點的:

這個好了一點,但是我們還是非常依賴正則

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

list(, $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']);

不要讓讀者猜

不要強迫你代碼的讀者去翻譯變量的含義,顯式比隱式要好

不好的:

$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.')
{
    // ...
}

好一點的:
這個比上一個版本好一點,因爲可以保證 $breweryName 不爲 Null

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

好的:

如果你只需要支持 PHP 7以上的版本, 你可以使用使用類型提示來保證 $breweryName 不爲 NULL

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

三、函數部分(一)

1. 函數參數(不要超過兩個)

限制函數的參數數量是非常重要的,因爲它使你的函數更容易測試。超過三個參數會導致參數之間的組合過多,你必須對每個單獨的參數測試大量不同的情況。

沒有參數最理想的情況,一個或兩個參數是可以接受的,三個以上是應該避免的。這是很重要的。通常,如果你有兩個以上的參數,那麼你的函數可能試圖做的太多,如果不是,你可能需要將一個高級別的對象傳當做參數傳進去。

不好的

function createMenu($title, $body, $buttonText, $cancellable)
{
    // ...
}

好的:

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)
{
    // ...
}

2. 一個函數只做一件事

這是軟件工程中最重要的原則。
不好的:

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

不好的

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

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

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

    return $clientRecord->isActive();
}

3. 函數名要能說明它是做什麼的

不好的:

class Email
{
    //...

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

$message = new Email(...);
// 這是什麼?一條消息的句柄? 你是要寫一個文件麼?(讀者的疑問)
$message->handle();

好的

class Email 
{
    //...

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

$message = new Email(...);
//  一目瞭然
$message->send();

4. 函數應該只做一層抽象

當你有多個層次的抽象時,你的函數通常都在試圖做的太多。拆分這些函數,可以讓代碼可重用性更高且更易測試。

不好的:

function parseBetterJSAlternative($code)
{
    $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($code)
{
    $regexes = [
        // ...
    ];

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

    return $tokens;
}

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

    return $ast;
}

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

好的:
最好的解決方案是移除 parseBetterJSAlternative 函數的依賴

class Tokenizer
{
    public function tokenize($code)
    {
        $regexes = [
            // ...
        ];

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

        return $tokens;
    }
}

class Lexer
{
    public function lexify($tokens)
    {
        $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($code)
    {
        $tokens = $this->tokenizer->tokenize($code);
        $ast = $this->lexer->lexify($tokens);
        foreach ($ast as $node) {
            // parse...
        }
    }
}

5.不要使用標誌做函數參數

標誌相當於告訴使用者,這個函數不止做一件事。函數應該只做一件事,如果函數根據布爾值執行不同代碼路徑,則你應該將函數拆分。

不好的:

function createFile($name, $temp = false)
{
    if ($temp) {
        touch('./temp/'.$name);
    } else {
        touch($name);
    }
}

好的:

function createFile($name)
{
    touch($name);
}

function createTempFile($name)
{
    touch('./temp/'.$name);
}

6.避免副作用

如果一個函數做了“拿到一個值並返回一個值或者多個值”以外的事情,那麼這個函數就有可能產生副作用,副作用可能是意外的寫入了文件、修改了全局變量、或者打錢給了陌生人。

現在假如你確實要在函數中做一些有可能產生副作用的事情。 比如要寫一個文件,你需要做的是將寫文件的操作集中到一處,而不是在幾個函數或者類裏對同一個文件做操作,實現一個服務(函數或者類)去操作它,有且僅有一個。

關鍵是要能避免常見的陷阱:像是在沒有結構的對象之間共享狀態、使用可能被寫入任何值的可變數據類型、 不集中處理有可能產生副作用的操作。 如果你能做到這些,你會比絕大多數程序員更快樂。

不好的:

// 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()
{
    global $name;

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

splitIntoFirstAndLastName();

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

好的:

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

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

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

7. 不要修改全局變量

在許多編程語言中污染全局是一種糟糕的做法,因爲你的庫可能會與另一個庫衝突,但是你的庫的用戶卻一無所知,直到在生產環境中爆發異常。讓我們來考慮一個例子:如果你想要拿到配置數組怎麼辦?你可以編寫全局函數,如config(),但是它可能與另一個試圖做同樣事情的庫衝突。

不好的:

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

好的

class Configuration
{
    private $configuration = [];

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

    public function get($key)
    {
        return isset($this->configuration[$key]) ? $this->configuration[$key] : null;
    }
}

加載配置並且創建 Configuration 類的實例

$configuration = new Configuration([
    'foo' => 'bar',
]);

現在你應該在你的應用程序中使用 Configuration類 的實例 。

四、函數部分(二)

8. 不要使用單例模式

(譯者注:這一條有些難理解,看不懂就略過吧)

單例模式是一種反模式,Brian Button 的解釋:

  1. 單例通常被用做一個全局的實例,爲什麼不好?因爲你在代碼中隱藏了依賴,而不是通過接口暴露他們。通過將一些東西放到全局來避免傳遞他們是一種“代碼異味(code smell)”

code smell 是指能夠被開發者察覺到的不好的形式

  1. 它違反了 SPR原則(single responsibility principle): 由它自己控制自己的創建和生命週期
  2. 它本身就導致了代碼的緊耦合。大多數情況下這使得通過僞造數據來測試變的相當困難。

不好的

class DBConnection
{
    private static $instance;

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

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

        return self::$instance;
    }

    // ...
}

$singleton = DBConnection::getInstance();

好的:

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

     // ...
}

創建 DBConneciton 的實例,並配置 DSN

這樣你就可以在你的應用程序中使用 DBConnection 的實例了。

9 封裝條件判斷

不好的:

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

好的:

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

避免否定類型的判斷

不好的:

function isDOMNodeNotPresent($node)
{
    // ...
}

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

好的

function isDOMNodePresent($node)
{
    // ...
}

if (isDOMNodePresent($node)) {
    // ...
}

10. 避免條件判斷

這似乎是一個不可能完成的任務。人們會問“如果不用 if 語句我該怎麼做?”,答案是在許多情況下,你可以用多態來實現同樣的效果。你可能還會問“這樣有什麼好處?”,答案是我們之前提到的原則:“一個函數應該只做一件事”, 當你的類或函數中有了 if 語句,相當於告訴別人你的函數做了一件以上的事情。

不好的:

class Airplane
{
    // ...

    public function getCruisingAltitude()
    {
        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();
}

class Boeing777 implements Airplane
{
    // ...

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

class AirForceOne implements Airplane
{
    // ...

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

class Cessna implements Airplane
{
    // ...

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

11. 避免類型檢查(第一部分)

PHP是弱類型語言,意味着你的函數可以接收任何類型的參數。有時你會因爲這點自由而受害,這時你可能會在函數中檢查參數類型,有許多途徑可以避免在函數能做類型檢查,首先要做的是有一致的接口。

不好的

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

好的:

function travelToTexas(Traveler $vehicle)
{
    $vehicle->travelTo(new Location('texas'));
}

避免類型檢查(第二部分)

如果現在要處理的是基礎數據類型,像字符串,整型和數組。現在如果你用的 PHP7以上的版本,而且現在很明顯你也不能用多態。現在你應該考慮使用類型檢查或者嚴格模式,它使你能夠在php語法層面提供靜態類型檢查。手動判斷類型的問題是,你需要添加許多額外的代碼,你獲得的人造的“類型安全”抵不過你代碼可讀性的損失。

不好的

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

    return $val1 + $val2;
}

好的

function combine(int $val1, int $val2)
{
    return $val1 + $val2;
}

12.移除廢棄的代碼

廢棄的代碼跟重複的代碼一樣不好,沒有理由在你的代碼庫繼續保留他們,如果你想找回他們,到版本歷史中找回就行了。

不好的

function oldRequestModule($url)
{
    // ...
}

function newRequestModule($url)
{
    // ...
}

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

好的

function requestModule($url)
{
    // ...
}

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

五、對象部分

1.使用 setter 和 getter

在 PHP 中,你可以爲方法設置 public, protectedprivate 關鍵字。使用這些關鍵字你可以控制一個對象的屬性修改權限。

  • 如果除了獲取對象屬性你還想做一些別的事,就不用再到代碼庫中去尋找並修改每一個修改對象屬性的地方(屬性入口歸一)。
  • 方便數據驗證
  • 封裝內部實現
  • 獲取和設置時方便添加日誌和錯誤處理
  • 繼承了類,你可以重寫默認的函數
  • 我們可以延遲加載類的屬性,假設它是從服務器獲取的

另外,這是開放/封閉原則的一部分,是面向對象的基本設計原則。

不好的

class BankAccount
{
    public $balance = 1000;
}

$bankAccount = new BankAccount();

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

好的

class BankAccount
{
    private $balance;

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

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

        $this->balance -= $amount;
    }

    public function depositBalance($amount)
    {
        $this->balance += $amount;
    }

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

$bankAccount = new BankAccount();

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

// Get balance
$balance = $bankAccount->getBalance();

2.讓對象有 私有(private)/受保護的(protected) 的成員

不好的

class Employee
{
    public $name;

    public function __construct($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($name)
    {
        $this->name = $name;
    }

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

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

六、類部分

使用組合而不是繼承

Gang of Four”在設計模式裏所聲明的,你應該優先選擇“組合模式”而不是“繼承”

譯者注:Gang of Four 譯成 四人幫,指代 DesignPatternBook 的四位作者

不論是使用“組合模式”還是使用“繼承”都有許多理由。
這個話題的要點是當你本能的要使用繼承時就想一想是否“組合模式”能幫你更好的解決問題。

你可能會問,“我什麼時候應該用繼承?”, 這取決於你手頭的問題。這裏有一個列表說明什麼時候使用繼承會更合適:

  1. 你的繼承表達了一個“is-a(是)” 的關係,不是“has-a(有)”的關係(對比人類 -> 動物 和 用戶-> 用戶詳情 )
  2. 你能從基礎類中複用代碼(人類能夠像所有動物一樣移動)
  3. 你想通過修改全局類來對所有派生類進行修改。(例如改變所有動物移動消耗的熱量)

不好的:

class Employee 
{
    private $name;
    private $email;

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

    // ...
}

// 不好,因爲是僱員有稅收數據
// 稅收數據不是一種僱員,所以使用繼承不適合

class EmployeeTaxData extends Employee 
{
    private $ssn;
    private $salary;

    public function __construct($name, $email, $ssn, $salary)
    {
        parent::__construct($name, $email);

        $this->ssn = $ssn;
        $this->salary = $salary;
    }

    // ...
}

好的:

class EmployeeTaxData 
{
    private $ssn;
    private $salary;

    public function __construct($ssn, $salary)
    {
        $this->ssn = $ssn;
        $this->salary = $salary;
    }

    // ...
}

class Employee 
{
    private $name;
    private $email;
    private $taxData;

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

    public function setTaxData($ssn, $salary)
    {
        $this->taxData = new EmployeeTaxData($ssn, $salary);
    }

    // ...
}

避免使用流式接口(fluent interfaces)

fluent interfaces 是一種面向對象的接口,通過Method chaining(鏈式操作)來提高代碼的可讀性。

然而有一些Context,通常是構建對象,這個設計模式雖然減少了代碼的囉嗦(例如:PHPUnit Mock Builder 或者 Doctrine Query Builder),大部分時候他會產生如下這些消耗:

不好的:

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

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章