如果有人想成為更棒的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 開放封閉原則

定義

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

秘訣

  • 只考慮抽象層級的介面互動
  • 把變化委託給其他類別處理
  • 只要異動 metadata 或 config

提醒

  • 不是所有程式都須遵守 OCP
  • 可能一開始無法預想到需要擴充,但可以透過重構完成
  • 不要過度用繼承的方式來擴充

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,降低其他類別對此類別的偶合度。

秘訣

  • 已經成形的操作流程就封裝起來
  • 不必要的公開方法就設為 private 或 protected
  • 外面知道的越少,偶合度就越低

Interface Segregation Principle 介面隔離原則

定義

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

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

秘訣

  • 把 Interface 當成「可以做什麼」, 而不是「是一個什麼」
  • 減少讓每個 Interface 可以做的事
  • 如果發現有空實作時,就表示 Interface 可以再細化

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