物件導向設計原則 SOLID

如果有人想成為更棒的PHP工程師? Laravel 之父 Taylor Otwell: 學習良好的設計模式,尤其是 S.O.L.I.D

PHP Interview With Taylor Otwell – Learn Good Design Patterns


寫物件導向常見問題

萬能類別

全部的功能都寫在一個類別裡面,職責太大,合併、修改或維護都變得很困難。

BUG製造機

需求一直變,為了寫新功能,需要修改程式碼,可是需要修改的地方越多,BUG也會跟著變多。

繼承用太多

PHP不允許使用多重繼承,常常為了方便共用同樣的功能而直接繼承,導致之後要改其中一部分程式碼,而影響後面的子類別。

要知道的太多

類別使用方式寫得太複雜,花費太多時間在看code。

給多用少

一個物件有很多方法可以使用,可是只是需要其中一個方法,浪費了很多效能。

耦合度太高

程式碼之間的關係太深,導致很難修改。


SOLID


Single Responsibility Principle 單一職責原則

定義

應該且僅有一個原因引起類別的變更,讓類別只有一個職責。

秘訣

提醒

Bad:

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

Good:

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 開放封閉原則

定義

軟體中的對象(類別、函數),對於擴展是開放的,對於修改是封閉的。

秘訣

提醒

Bad:

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

Good:

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 里氏替換原則

定義

所有參照基礎類別的地方,必須可以使用衍生類別的物件代替,而不需要任何改變。

子類別應該可以替換掉父類別而不影響程式架構。

子類別應該可以執行父類別想做的事情。

秘訣

提醒

Bad:

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

Good:

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

Least Knowledge Principle 最小知識原則

定義

一個物件應該對其他物件有最少的了解,盡可能減少類別中的 public method,降低其他類別對此類別的偶合度。

秘訣


Interface Segregation Principle 介面隔離原則

定義

用戶端程式碼不應該依賴他用不到的介面,依賴的介面都是有其必要性。

把不同功能的從介面中分離出來。

秘訣

Bad:

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

Good:

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

Dependency Inversion Principle 依賴反轉原則

高階模組不應該依賴低階模組,兩者應該要依賴其抽象,抽象不要依賴細節,細節要依賴抽象。

不要把程式碼寫死某種實作上。

秘訣

提醒

Bad:

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

Good:

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

SOLID 其實講的事同一件事

面對原始碼改變的策略

SRP: 降低單一類別被「改變」所影響的機會 OCP: 讓主類別不會因為新增需求而改變 LSP: 避免繼承時子類別所造成的「行為改變」 LKP: 避免暴露過多資訊造成用戶端因流程調整而改變 ISP: 降低用戶端因為不相關介面而被改變 DIP: 避免高階程式因為低階程式改變而被迫改變


後記

真的很感謝鐵大,從他身上學到很多東西,他真的很厲害。

寫 PHP 幾年以來,認識 SOLID 真的學到很多,遵守這些可以讓程式碼更好維護、更好擴充,但實際上要運用起來還是相當的難。

所以要有 Design Patterns 來做為輔助,這些都是原則,而不是聖旨,應該要學會如何運用,而不是變成阻礙。


References