php代碼整潔之道整理

th?id=OHR.SardiniaHawkMoth_ZH-CN3672906054_1920x1080.jpg&rf=LaDigue_1920x1080.jpg&pid=hp.jpg

Clean Code PHP

目錄

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

介紹

本文參考自 Robert C. Martin的Clean Code 書中的軟件工程師的原則
,適用于PHP古涧。 這不是風(fēng)格指南透罢。 這是一個關(guān)于開發(fā)可讀刨啸、可復(fù)用并且可重構(gòu)的PHP軟件指南获茬。

并不是這里所有的原則都得遵循履恩,甚至很少的能被普遍接受锰茉。 這些雖然只是指導(dǎo),但是都是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語句通常會導(dǎo)致你的代碼難以閱讀,直白優(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)可以得知一些信息暖眼,就別再在變量名里重復(fù)。

壞:

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ù)導(dǎo)致一個爆炸式組合增長挤安,你會有成噸獨立參數(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ī)則置尔。當(dāng)一個函數(shù)做超過一件事的時候杠步,他們就難于實現(xiàn)、測試和理解榜轿。當(dāng)你把一個函數(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)當(dāng)只有一層抽象abstraction

當(dāng)你抽象層次過多時時助析,函數(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()方法還是很復(fù)雜,而且不利于測試雪隧。

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)當(dā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. 導(dǎo)致代碼強耦合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ù)件甥。當(dāng)人們第一次聽到這句話是都會這么說。
"沒有if語句我還能做啥哼拔?" 答案是你可以使用多態(tài)來實現(xiàn)多種場景
的相同任務(wù)引有。第二個問題很常見, “這么做可以倦逐,但為什么我要這么做譬正?”
答案是前面我們學(xué)過的一個Clean Code原則:一個函數(shù)應(yīng)當(dāng)只做一件事。
當(dā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)當(dāng)考慮類型聲明或者嚴格模式筑辨。
提供了基于標(biāo)準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;
}

? 返回頂部

移除僵尸代碼

僵尸代碼和重復(fù)代碼一樣壞。沒有理由保留在你的代碼庫中抚太。如果從來沒被調(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 來控制對象屬性的變更。

  • 當(dāng)你想對對象屬性做獲取之外的操作時庆亡,你不需要在代碼中去尋找并修改每一個該屬性訪問方法
  • 當(dāng)有set對應(yīng)的屬性方法時匾乓,易于增加參數(shù)的驗證
  • 封裝內(nèi)部的表示
  • 使用set和get時,易于增加日志和錯誤控制
  • 繼承當(dāng)前類時又谋,可以復(fù)寫默認的方法功能
  • 當(dāng)對象屬性是從遠端服務(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的修改保證了這部分代碼只會影響當(dāng)前類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).

所以锦积,當(dāng)你需要控制類里的代碼可以被訪問時才用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)先選擇組合而不是繼承的方式。使用繼承和組合都有很多好處哮幢。
這個準則的主要意義在于當(dāng)你本能的使用繼承時带膀,試著思考一下組合是否能更好對你的需求建模。
在一些情況下橙垢,是這樣的垛叨。

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

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

糟糕的:

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

有上下文的地方可以降低代碼復(fù)雜度,例如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. 導(dǎo)致提交的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. 鼓勵 單一職責(zé)模式.
  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è)計原則

單一職責(zé)原則

Single Responsibility Principle (SRP)

正如在Clean Code所述谐檀,"修改一個類應(yīng)該只為一個理由"抡谐。
人們總是易于用一堆方法塞滿一個類,如同我們只能在飛機上
只能攜帶一個行李箱(把所有的東西都塞到箱子里)稚补。這樣做
的問題是:從概念上這樣的類不是高內(nèi)聚的童叠,并且留下了很多
理由去修改它。將你需要修改類的次數(shù)降低到最小很重要课幕。
這是因為厦坛,當(dāng)有很多方法在類中時,修改其中一處乍惊,你很難知
曉在代碼庫中哪些依賴的模塊會被影響到杜秸。

壞:

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ù)學(xué)
上講,正方形是一種長方形伯病,但是當(dāng)你的模型通過繼承使用了"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)該被強制依賴于他不需要的接口"

有一個清晰的例子來說明示范這條原則酌畜。當(dā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)它。最大的好處
是它使模塊之間解耦咽袜。耦合會導(dǎo)致你難于重構(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();
    }
}

? 返回頂部

別寫重復(fù)代碼 (DRY)

試著去遵循DRY 原則.

盡你最大的努力去避免復(fù)制代碼酬蹋,它是一種非常糟糕的行為及老,復(fù)制代碼
通常意味著當(dāng)你需要變更一些邏輯時,你需要修改不止一處范抓。

試想一下骄恶,如果你在經(jīng)營一家餐廳并且你在記錄你倉庫的進銷記錄:所有
的土豆,洋蔥匕垫,大蒜僧鲁,辣椒等。如果你有多個列表來管理進銷記錄,當(dāng)你
用其中一些土豆做菜時你需要更新所有的列表寞秃。如果你只有一個列表的話
只有一個地方需要更新斟叼。

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

用對抽象非常關(guān)鍵绑改,這正是為什么你必須學(xué)習(xí)遵守在章節(jié)寫
的SOLID原則谢床,不合理的抽象比復(fù)制代碼更糟糕,所以務(wù)必謹慎厘线!說了這么多识腿,
如果你能設(shè)計一個合理的抽象,那就這么干造壮!別寫重復(fù)代碼渡讼,否則你會發(fā)現(xiàn)
任何時候當(dāng)你想修改一個邏輯時你必須修改多個地方。

壞:

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?utm_source=gold_browser_extension#variables

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末耳璧,一起剝皮案震驚了整個濱河市成箫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌旨枯,老刑警劉巖伟众,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異召廷,居然都是意外死亡凳厢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門竞慢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來先紫,“玉大人,你說我怎么就攤上這事筹煮≌诰” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵败潦,是天一觀的道長本冲。 經(jīng)常有香客問我,道長劫扒,這世上最難降的妖魔是什么檬洞? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮沟饥,結(jié)果婚禮上添怔,老公的妹妹穿的比我還像新娘湾戳。我一直安慰自己,他們只是感情好广料,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布砾脑。 她就那樣靜靜地躺著,像睡著了一般艾杏。 火紅的嫁衣襯著肌膚如雪韧衣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天购桑,我揣著相機與錄音汹族,去河邊找鬼。 笑死其兴,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的夸政。 我是一名探鬼主播元旬,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼守问!你這毒婦竟也來了匀归?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤耗帕,失蹤者是張志新(化名)和其女友劉穎穆端,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仿便,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡体啰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了嗽仪。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荒勇。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖闻坚,靈堂內(nèi)的尸體忽然破棺而出沽翔,到底是詐尸還是另有隱情,我是刑警寧澤窿凤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布仅偎,位于F島的核電站,受9級特大地震影響雳殊,放射性物質(zhì)發(fā)生泄漏橘沥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一夯秃、第九天 我趴在偏房一處隱蔽的房頂上張望威恼。 院中可真熱鬧品姓,春花似錦、人聲如沸箫措。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斤蔓。三九已至植酥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弦牡,已是汗流浹背友驮。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留驾锰,地道東北人卸留。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像椭豫,于是被迫代替她去往敵國和親耻瑟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

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

  • 一赏酥、整潔代碼 A.混亂的代價 1.有些團隊在項目初期進展迅速喳整,但有那么一兩年的時間卻慢去蝸行。對代碼的每次修改都影...
    ZyBlog閱讀 2,017評論 0 2
  • Clean Code PHP github地址 目錄 介紹 變量使用見字知意的變量名同一個實體要用相同的變量名使用...
    code_nerd閱讀 461評論 0 3
  • 代碼整潔之道 Clean Code 第一章 整潔代碼 代碼的重要性我們永遠拋不掉代碼裸扶,因為代碼呈現(xiàn)了需求的細節(jié)框都。在...
    Pengzh1閱讀 1,201評論 0 1
  • 整潔代碼 Leblanc : Later equals never.(勒布朗法則:稍后等于永不) 對代碼的每次修改...
    foever_f1eb閱讀 787評論 0 0
  • 目錄及筆記鏈接 序 丹麥諺語:小處誠實非小事。 建筑師路德維希:神在細節(jié)之中呵晨。 日本的 5S 哲學(xué): 整理(整理魏保、...
    小鐳Ra閱讀 1,495評論 3 3