php代碼整潔之道

前言

文章出處 https://github.com/php-cpm/clean-code-php

Clean Code PHP

目錄

  1. 介紹
  2. 變量
  3. 表達式
  4. 函數(shù)
  5. 對象和數(shù)據(jù)結(jié)構(gòu) Objects and Data Structures
  6. 類的SOLID原則 SOLID
  7. 別寫重復代碼 (DRY)
  8. 翻譯

介紹

本文參考自 Robert C. Martin的Clean Code 書中的軟件工程師的原則
,適用于PHP蚓耽。 這不是風格指南菱父。 這是一個關(guān)于開發(fā)可讀仪搔、可復用并且可重構(gòu)的PHP軟件指南盔憨。

并不是這里所有的原則都得遵循洗搂,甚至很少的能被普遍接受热康。 這些雖然只是指導沛申,但是都是Clean Code作者多年總結(jié)出來的。

本文受到 clean-code-javascript 的啟發(fā)

雖然很多開發(fā)者還在使用PHP5姐军,但是本文中的大部分示例的運行環(huán)境需要PHP 7.1+铁材。

翻譯說明

翻譯完成度100%尖淘,最后更新時間2017-12-25。本文由 php-cpm 基于 yangweijie版本clean-code-php翻譯并同步大量原文內(nèi)容著觉。

原文更新頻率較高村生,我的翻譯方法是直接用文本比較工具逐行對比。優(yōu)先保證文字內(nèi)容是最新的饼丘,再逐步提升翻譯質(zhì)量趁桃。

閱讀過程中如果遇到各種鏈接失效、內(nèi)容老舊肄鸽、術(shù)語使用錯誤和其他翻譯錯誤等問題镇辉,歡迎大家積極提交PR。

變量

使用見字知意的變量名

壞:

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

好:

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

? 返回頂部

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

壞:

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

好:

getUser();

? 返回頂部

使用便于搜索的名稱 (part 1)

寫代碼是用來讀的贴捡。所以寫出可讀性高忽肛、便于搜索的代碼至關(guān)重要。
命名變量時如果沒有有意義烂斋、不好理解屹逛,那就是在傷害讀者。
請讓你的代碼便于搜索汛骂。

壞:

// What the heck is 448 for?
$result = $serializer->serialize($data, 448);

好:

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

使用便于搜索的名稱 (part 2)

壞:

// What the heck is 4 for?
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);

好:

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

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

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

? 返回頂部

避免深層嵌套帘瞭,盡早返回 (part 1)

太多的if else語句通常會導致你的代碼難以閱讀淑掌,直白優(yōu)于隱晦

糟糕:

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

? 返回頂部

避免深層嵌套,盡早返回 (part 2)

糟糕的:

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

? 返回頂部

不要添加不必要上下文

如果從你的類名、對象名已經(jīng)可以得知一些信息媒殉,就別再在變量名里重復担敌。

壞:

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

    //...
}

好:

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

    //...
}

? 返回頂部

合理使用參數(shù)默認值,沒必要在方法里再做默認值檢測

不好:

不好廷蓉,$breweryName 可能為 NULL.

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

還行:

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

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

好:

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

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

? 返回頂部

表達式

使用恒等式

不好:

簡易對比會將字符串轉(zhuǎn)為整形

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

if( $a != $b ) {
   //這里始終執(zhí)行不到
}

對比 a !=b 返回了 FALSE 但應(yīng)該返回 TRUE !
字符串 '42' 跟整數(shù) 42 不相等

好:

使用恒等判斷檢查類型和數(shù)據(jù)

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

if ($a !== $b) {
    // The expression is verified
}

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

? 返回頂部

函數(shù)

函數(shù)參數(shù)(最好少于2個)

限制函數(shù)參數(shù)個數(shù)極其重要,這樣測試你的函數(shù)容易點桃犬。有超過3個可選參數(shù)參數(shù)導致一個爆炸式組合增長刹悴,你會有成噸獨立參數(shù)情形要測試。

無參數(shù)是理想情況攒暇。1個或2個都可以土匀,最好避免3個。再多就需要加固了扯饶。通常如果你的函數(shù)有超過兩個參數(shù)恒削,說明他要處理的事太多了池颈。 如果必須要傳入很多數(shù)據(jù),建議封裝一個高級別對象作為參數(shù)钓丰。

壞:

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

? 返回頂部

函數(shù)應(yīng)該只做一件事

這是迄今為止軟件工程里最重要的一個規(guī)則躯砰。當一個函數(shù)做超過一件事的時候,他們就難于實現(xiàn)携丁、測試和理解琢歇。當你把一個函數(shù)拆分到只剩一個功能時,他們就容易被重構(gòu)梦鉴,然后你的代碼讀起來就更清晰李茫。如果你光遵循這條規(guī)則,你就領(lǐng)先于大多數(shù)開發(fā)者了肥橙。

壞:

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

? 返回頂部

函數(shù)名應(yīng)體現(xiàn)他做了什么事

壞:

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

? 返回頂部

函數(shù)里應(yīng)當只有一層抽象abstraction

當你抽象層次過多時時宠互,函數(shù)處理的事情太多了。需要拆分功能來提高可重用性和易用性椭坚,以便簡化測試予跌。
(譯者注:這里從示例代碼看應(yīng)該是指嵌套過多)

壞:

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...
    }
}

壞:

我們把一些方法從循環(huán)中提取出來,但是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) {
        // 解析邏輯...
    }
}

好:

最好的解決方案是把 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) {
            // 解析邏輯...
        }
    }
}

這樣我們可以對依賴做mock垂涯,并測試BetterJSAlternative::parse()運行是否符合預(yù)期烁焙。

? 返回頂部

不要用flag作為函數(shù)的參數(shù)

flag就是在告訴大家,這個方法里處理很多事集币。前面剛說過考阱,一個函數(shù)應(yīng)當只做一件事。 把不同flag的代碼拆分到多個函數(shù)里鞠苟。

壞:

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

? 返回頂部

避免副作用

一個函數(shù)做了比獲取一個值然后返回另外一個值或值們會產(chǎn)生副作用如果。副作用可能是寫入一個文件秽之,修改某些全局變量或者偶然的把你全部的錢給了陌生人当娱。

現(xiàn)在,你的確需要在一個程序或者場合里要有副作用考榨,像之前的例子跨细,你也許需要寫一個文件。你想要做的是把你做這些的地方集中起來河质。不要用幾個函數(shù)和類來寫入一個特定的文件冀惭。用一個服務(wù)來做它震叙,一個只有一個。

重點是避免常見陷阱比如對象間共享無結(jié)構(gòu)的數(shù)據(jù)散休,使用可以寫入任何的可變數(shù)據(jù)類型媒楼,不集中處理副作用發(fā)生的地方。如果你做了這些你就會比大多數(shù)程序員快樂戚丸。

壞:

// 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(): 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'];

? 返回頂部

不要寫全局函數(shù)

在大多數(shù)語言中污染全局變量是一個壞的實踐划址,因為你可能和其他類庫沖突
并且調(diào)用你api的人直到他們捕獲異常才知道踩坑了。讓我們思考一種場景:
如果你想配置一個數(shù)組限府,你可能會寫一個全局函數(shù)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;
    }
}

加載配置并創(chuàng)建 Configuration 類的實例

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

現(xiàn)在你必須在程序中用 Configuration 的實例了

? 返回頂部

不要使用單例模式

單例是一種 反模式. 以下是解釋:Paraphrased from Brian Button:

  1. 總是被用成全局實例胁勺。They are generally used as a global instance, why is that so bad? Because you hide the dependencies of your application in your code, instead of exposing them through the interfaces. Making something global to avoid passing it around is a code smell.
  2. 違反了單一響應(yīng)原則They violate the single responsibility principle: by virtue of the fact that they control their own creation and lifecycle.
  3. 導致代碼強耦合They inherently cause code to be tightly coupled. This makes faking them out under test rather difficult in many cases.
  4. 在整個程序的生命周期中始終攜帶狀態(tài)世澜。They carry state around for the lifetime of the application. Another hit to testing since you can end up with a situation where tests need to be ordered which is a big no for unit tests. Why? Because each unit test should be independent from the other.

這里有一篇非常好的討論單例模式的[根本問題((http://misko.hevery.com/2008/08/25/root-cause-of-singletons/)的文章,是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)
    {
        // ...
    }

     // ...
}

創(chuàng)建 DBConnection 類的實例并通過 DSN 配置.

$connection = new DBConnection($dsn);

現(xiàn)在你必須在程序中 使用 DBConnection 的實例了

? 返回頂部

封裝條件語句

壞:

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

好:

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

? 返回頂部

避免用反義條件判斷

壞:

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

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

好:

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

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

? 返回頂部

避免條件判斷

這看起來像一個不可能任務(wù)寥裂。當人們第一次聽到這句話是都會這么說。
"沒有if語句我還能做啥蛇捌?" 答案是你可以使用多態(tài)來實現(xiàn)多種場景
的相同任務(wù)抚恒。第二個問題很常見, “這么做可以络拌,但為什么我要這么做俭驮?”
答案是前面我們學過的一個Clean Code原則:一個函數(shù)應(yīng)當只做一件事。
當你有很多含有if語句的類和函數(shù)時,你的函數(shù)做了不止一件事春贸。
記住混萝,只做一件事。

壞:

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是弱類型的,這意味著你的函數(shù)可以接收任何類型的參數(shù)萍恕。
有時候你為這自由所痛苦并且在你的函數(shù)漸漸嘗試類型檢查逸嘀。
有很多方法去避免這么做。第一種是統(tǒng)一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)

如果你正使用基本原始值比如字符串崭倘、整形和數(shù)組,要求版本是PHP 7+类垫,不用多態(tài)司光,需要類型檢測,
那你應(yīng)當考慮類型聲明或者嚴格模式悉患。
提供了基于標準PHP語法的靜態(tài)類型残家。 手動檢查類型的問題是做好了需要好多的廢話,好像為了安全就可以不顧損失可讀性售躁。
保持你的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;
}

? 返回頂部

移除僵尸代碼

僵尸代碼和重復代碼一樣壞诺擅。沒有理由保留在你的代碼庫中。如果從來沒被調(diào)用過毫玖,就刪掉掀虎!
因為還在代碼版本庫里,因此很安全付枫。

壞:

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

? 返回頂部

對象和數(shù)據(jù)結(jié)構(gòu)

使用 getters 和 setters

在PHP中你可以對方法使用public, protected, private 來控制對象屬性的變更烹玉。

  • 當你想對對象屬性做獲取之外的操作時,你不需要在代碼中去尋找并修改每一個該屬性訪問方法
  • 當有set對應(yīng)的屬性方法時阐滩,易于增加參數(shù)的驗證
  • 封裝內(nèi)部的表示
  • 使用set和get時二打,易于增加日志和錯誤控制
  • 繼承當前類時,可以復寫默認的方法功能
  • 當對象屬性是從遠端服務(wù)器獲取時掂榔,get继效,set易于使用延遲加載

此外,這樣的方式也符合OOP開發(fā)中的開閉原則

壞:

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

? 返回頂部

給對象使用私有或受保護的成員變量

  • public方法和屬性進行修改非常危險装获,因為外部代碼容易依賴他瑞信,而你沒辦法控制。對之修改影響所有這個類的使用者穴豫。 public methods and properties are most dangerous for changes, because some outside code may easily rely on them and you can't control what code relies on them. Modifications in class are dangerous for all users of class.
  • protected的修改跟對public修改差不多危險凡简,因為他們對子類可用,他倆的唯一區(qū)別就是可調(diào)用的位置不一樣精肃,對之修改影響所有集成這個類的地方秤涩。 protected modifier are as dangerous as public, because they are available in scope of any child class. This effectively means that difference between public and protected is only in access mechanism, but encapsulation guarantee remains the same. Modifications in class are dangerous for all descendant classes.
  • private的修改保證了這部分代碼只會影響當前類private modifier guarantees that code is dangerous to modify only in boundaries of single class (you are safe for modifications and you won't have Jenga effect).

所以,當你需要控制類里的代碼可以被訪問時才用public/protected司抱,其他時候都用private筐眷。

可以讀一讀這篇 博客文章Fabien Potencier寫的.

壞:

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 所著的設(shè)計模式之前所說习柠,
我們應(yīng)該盡量優(yōu)先選擇組合而不是繼承的方式匀谣。使用繼承和組合都有很多好處。
這個準則的主要意義在于當你本能的使用繼承時资溃,試著思考一下組合是否能更好對你的需求建模振定。
在一些情況下,是這樣的肉拓。

接下來你或許會想,“那我應(yīng)該在什么時候使用繼承梳庆?”
答案依賴于你的問題暖途,當然下面有一些何時繼承比組合更好的說明:

  1. 你的繼承表達了“是一個”而不是“有一個”的關(guān)系(人類-》動物卑惜,用戶-》用戶詳情)
  2. 你可以復用基類的代碼(人類可以像動物一樣移動)
  3. 你想通過修改基類對所有派生類做全局的修改(當動物移動時,修改她們的能量消耗)

糟糕的:

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

    // ...
}

? 返回頂部

避免連貫接口

連貫接口Fluent interface是一種
旨在提高面向?qū)ο缶幊虝r代碼可讀性的API設(shè)計模式露久,他基于方法鏈Method chaining

有上下文的地方可以降低代碼復雜度,例如PHPUnit Mock Builder
Doctrine Query Builder
欺栗,更多的情況會帶來較大代價:

While there can be some contexts, frequently builder objects, where this
pattern reduces the verbosity of the code (for example the PHPUnit Mock Builder
or the Doctrine Query Builder),
more often it comes at some costs:

  1. 破壞了 對象封裝
  2. 破壞了 裝飾器模式
  3. 在測試組件中不好做mock
  4. 導致提交的diff不好閱讀

了解更多請閱讀 連貫接口為什么不好
毫痕,作者 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();

? 返回頂部

推薦使用 final 類

能用時盡量使用 final 關(guān)鍵字:

  1. 阻止不受控的繼承鏈
  2. 鼓勵 組合.
  3. 鼓勵 單一職責模式.
  4. 鼓勵開發(fā)者用你的公開方法而非通過繼承類獲取受保護方法的訪問權(quán)限.
  5. 使得在不破壞使用你的類的應(yīng)用的情況下修改代碼成為可能.

The only condition is that your class should implement an interface and no other public methods are defined.

For more informations you can read the blog post on this topic written by Marco Pivetta (Ocramius).

Bad:

final class Car
{
    private $color;
    
    public function __construct($color)
    {
        $this->color = $color;
    }
    
    /**
     * @return string The color of the vehicle
     */
    public function getColor() 
    {
        return $this->color;
    }
}

Good:

interface Vehicle
{
    /**
     * @return string The color of the vehicle
     */
    public function getColor();
}

final class Car implements Vehicle
{
    private $color;
    
    public function __construct($color)
    {
        $this->color = $color;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getColor() 
    {
        return $this->color;
    }
}

? 返回頂部

SOLID

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

單一職責原則

Single Responsibility Principle (SRP)

正如在Clean Code所述迟几,"修改一個類應(yīng)該只為一個理由"消请。
人們總是易于用一堆方法塞滿一個類,如同我們只能在飛機上
只能攜帶一個行李箱(把所有的東西都塞到箱子里)类腮。這樣做
的問題是:從概念上這樣的類不是高內(nèi)聚的臊泰,并且留下了很多
理由去修改它。將你需要修改類的次數(shù)降低到最小很重要蚜枢。
這是因為缸逃,當有很多方法在類中時,修改其中一處厂抽,你很難知
曉在代碼庫中哪些依賴的模塊會被影響到需频。

壞:

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

? 返回頂部

開閉原則

Open/Closed Principle (OCP)

正如Bertrand Meyer所述,"軟件的工件( classes, modules, functions 等)
應(yīng)該對擴展開放筷凤,對修改關(guān)閉昭殉。" 然而這句話意味著什么呢?這個原則大體上表示你
應(yīng)該允許在不改變已有代碼的情況下增加新的功能

壞:

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

? 返回頂部

里氏替換原則

Liskov Substitution Principle (LSP)

這是一個簡單的原則嵌施,卻用了一個不好理解的術(shù)語饲化。它的正式定義是
"如果S是T的子類型,那么在不改變程序原有既定屬性(檢查吗伤、執(zhí)行
任務(wù)等)的前提下吃靠,任何T類型的對象都可以使用S類型的對象替代
(例如,使用S的對象可以替代T的對象)" 這個定義更難理解:-)足淆。

對這個概念最好的解釋是:如果你有一個父類和一個子類巢块,在不改變
原有結(jié)果正確性的前提下父類和子類可以互換。這個聽起來依舊讓人
有些迷惑巧号,所以讓我們來看一個經(jīng)典的正方形-長方形的例子族奢。從數(shù)學
上講,正方形是一種長方形丹鸿,但是當你的模型通過繼承使用了"is-a"
的關(guān)系時越走,就不對了。

壞:

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

    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 printArea(Rectangle $rectangle): void
{
    $rectangle->setWidth(4);
    $rectangle->setHeight(5);
 
    // BAD: Will return 25 for Square. Should be 20.
    echo sprintf('%s has area %d.', get_class($rectangle), $rectangle->getArea()).PHP_EOL;
}

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

foreach ($rectangles as $rectangle) {
    printArea($rectangle);
}

好:

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

盡管正方形和長方形看起來很相似铜跑,但他們是不同的。
正方形更接近菱形骡澈,而長方形更接近平行四邊形锅纺。但他們不是子類型。
盡管相似肋殴,正方形囤锉、長方形、菱形护锤、平行四邊形都是有自己屬性的不同形狀官地。

interface Shape
{
    public function getArea(): int;
}

class Rectangle implements Shape
{
    private $width = 0;
    private $height = 0;

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

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

class Square implements Shape
{
    private $length = 0;

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

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

function printArea(Shape $shape): void
{
    echo sprintf('%s has area %d.', get_class($shape), $shape->getArea()).PHP_EOL;
}

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

foreach ($shapes as $shape) {
    printArea($shape);
}

? 返回頂部

接口隔離原則

Interface Segregation Principle (ISP)

接口隔離原則表示:"調(diào)用方不應(yīng)該被強制依賴于他不需要的接口"

有一個清晰的例子來說明示范這條原則。當一個類需要一個大量的設(shè)置項蔽豺,
為了方便不會要求調(diào)用方去設(shè)置大量的選項区丑,因為在通常他們不需要所有的
設(shè)置項。使設(shè)置項可選有助于我們避免產(chǎn)生"胖接口"

壞:

interface Employee
{
    public function work(): void;

    public function eat(): void;
}

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

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

class RobotEmployee 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 HumanEmployee implements Employee
{
    public function work(): void
    {
        // ....working
    }

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

// robot can only work
class RobotEmployee implements Workable
{
    public function work(): void
    {
        // ....working
    }
}

? 返回頂部

依賴倒置原則

Dependency Inversion Principle (DIP)

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

  1. 高階的模塊不應(yīng)該依賴低階的模塊沧侥,它們都應(yīng)該依賴于抽象
  2. 抽象不應(yīng)該依賴于實現(xiàn),實現(xiàn)應(yīng)該依賴于抽象

這條起初看起來有點晦澀難懂魄鸦,但是如果你使用過 PHP 框架(例如 Symfony)宴杀,你應(yīng)該見過
依賴注入(DI),它是對這個概念的實現(xiàn)拾因。雖然它們不是完全相等的概念旺罢,依賴倒置原則使高階模塊
與低階模塊的實現(xiàn)細節(jié)和創(chuàng)建分離【罴牵可以使用依賴注入(DI)這種方式來實現(xiàn)它扁达。最大的好處
是它使模塊之間解耦。耦合會導致你難于重構(gòu)蠢熄,它是一種非常糟糕的的開發(fā)模式跪解。

壞:

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

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

試想一下饥追,如果你在經(jīng)營一家餐廳并且你在記錄你倉庫的進銷記錄:所有
的土豆图仓,洋蔥,大蒜但绕,辣椒等救崔。如果你有多個列表來管理進銷記錄,當你
用其中一些土豆做菜時你需要更新所有的列表。如果你只有一個列表的話
只有一個地方需要更新帚豪。

通常情況下你復制代碼是應(yīng)該有兩個或者多個略微不同的邏輯碳竟,它們大多數(shù)
都是一樣的,但是由于它們的區(qū)別致使你必須有兩個或者多個隔離的但大部
分相同的方法狸臣,移除重復的代碼意味著用一個function/module/class創(chuàng)
建一個能處理差異的抽象。

用對抽象非常關(guān)鍵昌执,這正是為什么你必須學習遵守在章節(jié)寫
的SOLID原則烛亦,不合理的抽象比復制代碼更糟糕,所以務(wù)必謹慎懂拾!說了這么多煤禽,
如果你能設(shè)計一個合理的抽象,那就這么干岖赋!別寫重復代碼檬果,否則你會發(fā)現(xiàn)
任何時候當你想修改一個邏輯時你必須修改多個地方。

壞:

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

? 返回頂部

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末唐断,一起剝皮案震驚了整個濱河市选脊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌脸甘,老刑警劉巖恳啥,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異丹诀,居然都是意外死亡钝的,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門铆遭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來硝桩,“玉大人,你說我怎么就攤上這事枚荣⊥爰梗” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵棍弄,是天一觀的道長望薄。 經(jīng)常有香客問我,道長呼畸,這世上最難降的妖魔是什么痕支? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮蛮原,結(jié)果婚禮上卧须,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好花嘶,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布笋籽。 她就那樣靜靜地躺著,像睡著了一般椭员。 火紅的嫁衣襯著肌膚如雪车海。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天隘击,我揣著相機與錄音侍芝,去河邊找鬼。 笑死埋同,一個胖子當著我的面吹牛州叠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播凶赁,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼咧栗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了虱肄?” 一聲冷哼從身側(cè)響起致板,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浩峡,沒想到半個月后可岂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡翰灾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年缕粹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纸淮。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡平斩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咽块,到底是詐尸還是另有隱情绘面,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布侈沪,位于F島的核電站揭璃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一歹撒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧情组,春花似錦燥筷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至底瓣,卻和暖如春谢揪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背濒持。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工键耕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人柑营。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像村视,于是被迫代替她去往敵國和親官套。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 原文:https://github.com/jupeter/clean-code-php 譯文:https://g...
    Separes閱讀 1,175評論 0 2
  • 一蚁孔、整潔代碼 A.混亂的代價 1.有些團隊在項目初期進展迅速奶赔,但有那么一兩年的時間卻慢去蝸行。對代碼的每次修改都影...
    ZyBlog閱讀 2,039評論 0 2
  • Clean Code PHP 目錄 介紹 變量使用見字知意的變量名同一個實體要用相同的變量名使用便于搜索的名稱 (...
    于殿國閱讀 147評論 0 0
  • 介紹 本文參考自 Robert C. Martin的Clean Code 書中的軟件工程師的原則,適用于PHP杠氢。...
    零一間閱讀 458評論 0 11
  • Clean Code PHP github地址 目錄 介紹 變量使用見字知意的變量名同一個實體要用相同的變量名使用...
    code_nerd閱讀 464評論 0 3