天天看點

代碼簡潔之道( PHP Clean Code)

介紹

Robert C.Martin’s 的 軟體工程師準則 Clean Code 同樣适用于 PHP。它并不是一個編碼風格指南,它指導我們用 PHP 寫出具有可讀性,可複用性且可分解的代碼。

并非所有的準則都必須嚴格遵守,甚至一些已經成為普遍的約定。這僅僅作為指導方針,其中許多都是 Clean Code 作者們多年來的經驗。

盡管許多開發者依舊使用 PHP 5 版本,但是這篇文章中絕大多數例子都是隻能在 PHP 7.1 + 版本下運作。

文章目錄

    • **介紹**
        • **變量**
          • **使用有意義的且可讀的變量名**
          • **對同類型的變量使用相同的詞彙**
          • **使用可搜尋的名稱(第一部分)**
          • **使用可搜尋的名稱(第二部分)**
          • **使用解釋性變量**
          • **避免嵌套太深和提前傳回 (第二部分)**
          • **避免心理映射**
          • **不要增加不需要的上下文**
          • **使用預設參數而不是使用短路運算或者是條件判斷**
        • **對比**
          • **使用 相等運算符**
        • **函數**
          • **函數參數(2 個或更少)**
          • **函數應該隻做一件事情**
          • **函數的名稱要說清楚它做什麼**
          • **函數隻能是一個抽象級别**
          • **不要用标示作為函數的參數**
          • **避免副作用**
          • **不要定義全局函數**
          • **不要使用單例模式**
          • **封裝條件語句**
          • **避免用反義條件判斷**
          • **避免使用條件語句**
          • **避免類型檢測 (第 1 部分)**
          • **避免類型檢查(第 2 部分)**
          • **移除無用代碼**
        • **對象和資料結構**
          • **使用對象封裝**
          • **讓對象擁有 private/protected 屬性的成員**
        • **類**
          • **組合優于繼承**
          • **避免流式接口**
        • **SOLID**
          • **職責單一原則 Single Responsibility Principle (SRP)**
          • **開閉原則 (OCP)**
          • **裡氏代換原則 (LSP)**
          • **接口隔離原則 (ISP)**
          • **依賴反轉原則 (DIP)**
          • **别寫重複代碼 (DRY)**

變量

使用有意義的且可讀的變量名

不友好的:

友好的:

對同類型的變量使用相同的詞彙

不友好的:

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

友好的:

使用可搜尋的名稱(第一部分)

我們閱讀的代碼超過我們寫的代碼。是以我們寫出的代碼需要具備可讀性、可搜尋性,這一點非常重要。要我們去了解程式中沒有名字的變量是非常頭疼的。讓你的變量可搜尋吧!

不具備可讀性的代碼:

//  見鬼的 448 是什麼意思?
$result = $serializer->serialize($data, 448);
           

具備可讀性的:

使用可搜尋的名稱(第二部分)

不好的:

// 見鬼的 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);

[, $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 表達式會導緻代碼難以了解。

明确優于隐式。

不好:

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

}
           
避免嵌套太深和提前傳回 (第二部分)

不好:

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();
    // ...
    // ...
    // ...
    // Wait, what is `$li` for again?
    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.';
    // ...
}
           

好的做法:

你可以使用 類型提示 而且可以保證 $breweryName 不會為空 NULL.

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

對比

使用 相等運算符

不好的做法:

$a = '42';
$b = 42;
           

使用簡單的相等運算符會把字元串類型轉換成數字類型

if( $a != $b ) {
   //這個條件表達式總是會通過
}
           

表達式 $a != $b 會傳回 false 但實際上它應該是 true !

字元串類型 ‘42’ 是不同于數字類型的 42

好的做法:

使用全等運算符會對比類型和值

if( $a !== $b ) {
    //這個條件是通過的
}
           

表達式 $a !== $b 會傳回 true。

函數

函數參數(2 個或更少)

限制函數參數個數極其重要

這樣測試你的函數容易點。有超過 3 個可選參數會導緻一個爆炸式組合增長,你會有成噸獨立參數情形要測試。

無參數是理想情況。1 個或 2 個都可以,最好避免 3 個。

再多就需要加強了。通常如果你的函數有超過兩個參數,說明他要處理的事太多了。 如果必須要傳入很多資料,建議封裝一個進階别對象作為參數。

不友好的:

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(...);
// What is this? A handle for the message? Are we writing to a file now?
$message->handle();
           

很好的例子:

class Email 
{
    //...

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

}
$message = new Email(...);
// Clear and obvious
$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...
        }
    }

}
           
不要用标示作為函數的參數

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

不友好的:

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 = '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',
]);
           

現在,在你的應用中必須使用 Configuration 的執行個體了。

不要使用單例模式

單例模式是個 反模式。 以下轉述 Brian Button 的觀點:

單例模式常用于 全局執行個體, 這麼做為什麼不好呢? 因為在你的代碼裡 你隐藏了應用的依賴關系,而沒有通過接口公開依賴關系 。避免全局的東西擴散使用是一種 代碼味道.

單例模式違反了 單一責任原則: 依據的事實就是 單例模式自己控制自身的建立和生命周期.

單例模式天生就導緻代碼緊 耦合。這使得在許多情況下用僞造的資料 難于測試。

單例模式的狀态會留存于應用的整個生命周期。 這會對測試産生第二次打擊,你隻能讓被嚴令需要測試的代碼運作不了收場,根本不能進行單元測試。為何?因為每一個單元測試應該彼此獨立。

還有些來自 Misko Hevery 的深入思考,關于單例模式的 問題根源。

不好的示範:

class DBConnection
{
    private static $instance;

    private function __construct(string $dsn)
    {
        // ...
    }
    
    public static function getInstance(): DBConnection
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
    
        return self::$instance;
    }
    
    // ...

}

$singleton = DBConnection::getInstance();
           

好的示範:

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

     // ...

}
           

用 DSN 進行配置建立的 DBConnection 類執行個體。

現在就必須在你的應用中使用 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();
    }

}
           
避免類型檢測 (第 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'));
}
           
避免類型檢查(第 2 部分)

如果你正在使用像 字元串、數值、或數組這樣的基礎類型,你使用的是 PHP 版本是 PHP 7+,并且你不能使用多态,但仍然覺得需要使用類型檢測,這時,你應該考慮 類型定義 或 嚴格模式。它為您提供了标準 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;
}
           
移除無用代碼

無用代碼和重複代碼一樣糟糕。 如果沒有被調用,就應該把它删除掉,沒必要将它保留在你的代碼庫中!當你需要它的時候,可以在你的曆史版本中找到它。

Bad:

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

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

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

           

Good:

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

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

對象和資料結構

使用對象封裝

在 PHP 中,你可以在方法中使用關鍵字,如 public, protected and private。

使用它們,你可以任意的控制、修改對象的屬性。

當你除擷取對象屬性外還想做更多的操作時,你不需要修改你的代碼

當 set 屬性時,易于增加參數驗證。

封裝的内部表示。

容易在擷取和設定屬性時添加日志和錯誤處理。

繼承這個類,你可以重寫預設資訊。

你可以延遲加載對象的屬性,比如從伺服器擷取資料。

此外,這樣的方式也符合 OOP 開發中的 [開閉原則](# 開閉原則 (OCP))

不好的:

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 公有的同樣危險,因為他們在子類範圍内是可用的。也就是說 public 和 protected 之間的差別僅僅在于通路機制,隻有封裝才能保證屬性是一緻的。任何在類内的變化對于所有繼承子類來說都是危險的 。

private 私有屬性的變化可以保證代碼 隻對單個類範圍内的危險 (對于修改你是安全的,并且你不會有其他類似堆積木的影響 Jenga effect).

是以,請預設使用 private 屬性,隻有當需要對外部類提供通路屬性的時候才采用 public/protected 屬性。

更多的資訊可以參考 Fabien Potencier 寫的針對這個專欄的文章 blog post .

Bad:

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
           

Good:

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 所著的 設計模式 中所說,

我們應該盡量優先選擇組合而不是繼承的方式。使用繼承群組合都有很多好處。

這個準則的主要意義在于當你本能的使用繼承時,試着思考一下組合是否能更好對你的需求模組化。

在一些情況下,是這樣的。

接下來你或許會想,“那我應該在什麼時候使用繼承?”

答案依賴于你的問題,當然下面有一些何時繼承比組合更好的說明:

你的繼承表達了 “是一個” 而不是 “有一個” 的關系(例如人類 “是” 動物,而使用者 “有” 使用者詳情)。

你可以複用基類的代碼(人類可以像動物一樣移動)。

你想通過修改基類對所有派生類做全局的修改(當動物移動時,修改它們的能量消耗)。

糟糕的:

class Employee 
{
    private $name;
    private $email;

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

}
// 不好,因為Employees "有" taxdata
// 而EmployeeTaxData不是Employee類型的

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

    public function __construct(string $name, string $email, string $ssn, string $salary)
    {
        parent::__construct($name, $email);
    
        $this->ssn = $ssn;
        $this->salary = $salary;
    }
    
    // ...

}
           

棒棒哒:

class EmployeeTaxData 
{
    private $ssn;
    private $salary;

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

}

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

    public function __construct(string $name, string $email)
    {
        $this->name = $name;
        $this->email = $email;
    }
    
    public function setTaxData(string $ssn, string $salary)
    {
        $this->taxData = new EmployeeTaxData($ssn, $salary);
    }
    
    // ...

}
           
避免流式接口

流式接口 是一種面向對象 API 的方法,旨在通過方法鍊 Method chaining 來提高源代碼的可閱讀性.

流式接口雖然需要一些上下文,需要經常建構對象,但是這種模式減少了代碼的備援度 (例如: PHPUnit Mock Builder

或 Doctrine Query Builder)

但是同樣它也帶來了很多麻煩:

破壞了封裝 Encapsulation

破壞了原型 Decorators

難以模拟測試 mock

使得多次送出的代碼難以了解

更多資訊可以參考 Marco Pivetta 撰寫的關于這個專題的文章 blog post

Bad:

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

Good:

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 是 Michael Feathers 推薦的便于記憶的首字母簡寫,它代表了 Robert Martin 命名的最重要的五個面向對象程式設計設計原則:

S: 職責單一原則 (SRP)

O: 開閉原則 (OCP)

L: 裡氏替換原則 (LSP)

I: 接口隔離原則 (ISP)

D: 依賴反轉原則 (DIP)

職責單一原則 Single Responsibility Principle (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 型對象”

(i.e., S 類型的對象可以替換 T 型對象) 在不改變程式的任何理想屬性的情況下 (正确性,任務完成度,etc.)." 這是一個更可怕的定義.

這個的最佳解釋是,如果你有個父類和一個子類,然後父類和子類可以互換使用而不會得到不正确的結果。這或許依然令人疑惑,是以我們來看下經典的正方形 - 矩形例子。幾何定義,正方形是矩形,但是,如果你通過繼承建立了 “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;
    }

}

/**

 * @param Rectangle[] $rectangles
   */
   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
{
    abstract public function getArea(): int;

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

}

class Rectangle extends Shape
{
    private $width;
    private $height;

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

}

class Square extends Shape
{
    private $length;

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

}

/**

 * @param Rectangle[] $rectangles
   */
   function renderLargeRectangles(array $rectangles): void
   {
   foreach ($rectangles as $rectangle) {
       $area = $rectangle->getArea(); 
       $rectangle->render($area);
   }
   }

$shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
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)

這一原則規定了兩項基本内容:

進階子產品不應依賴于低級子產品。兩者都應該依賴于抽象.

抽象類不應依賴于執行個體。執行個體應該依賴于抽象.

一開始可能很難去了解,但是你如果工作中使用過 php 架構(如 Symfony), 你應該見過以依賴的形式執行這一原則

依賴注入 (DI). 雖然他們不是相同的概念,DIP 可以讓進階子產品不需要了解其低級子產品的詳細資訊而安裝它們.

通過依賴注入可以做到。這樣做的一個巨大好處是減少了子產品之間的耦合。耦合是一種非常糟糕的開發模式,因為它使您的代碼難以重構.

不好的:

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 原則。

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

試想一下,如果你在經營一家餐廳,并且你需要記錄你倉庫的進銷記錄:包括所有的洋芋,洋蔥,大蒜,辣椒,等等。如果你使用多個表格來管理進銷記錄,當你用其中一些洋芋做菜時,你需要更新所有的表格。如果你隻有一個清單的話就隻需要更新一個地方。

通常情況下你複制代碼的原因可能是它們大多數都是一樣的,隻不過有兩個或者多個略微不同的邏輯,但是由于這些差別,最終導緻你寫出了兩個或者多個隔離的但大部分相同的方法,移除重複的代碼意味着用一個 function/module/class 建立一個能處理差異的抽象。

正确的抽象是非常關鍵的,這正是為什麼你必須學習遵守在 Classes 章節展開讨論的的 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()
        ]);
    }
}
           

原文位址:https://github.com/jupeter/clean-code-php

譯文位址:https://learnku.com/laravel/t/7774/the-conciseness-of-the-php-code-php-clean-code

有兩個或者多個略微不同的邏輯,但是由于這些差別,最終導緻你寫出了兩個或者多個隔離的但大部分相同的方法,移除重複的代碼意味着用一個 function/module/class 建立一個能處理差異的抽象。

正确的抽象是非常關鍵的,這正是為什麼你必須學習遵守在 Classes 章節展開讨論的的 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()
        ]);
    }
}
           

原文位址:https://github.com/jupeter/clean-code-php

譯文位址:https://learnku.com/laravel/t/7774/the-conciseness-of-the-php-code-php-clean-code