共通性與可變性

共通性分析與可變性分析

共通性分析: 
尋找的是不可能隨時間而改變的結構,為架構提供更長效的要素。
可變性分析: 
找到可能變化的結構,促進架構適應實際使用所需。
(可變性分析只在相關聯的共通性分析定義上下文才有意義。)

共通性與可變性分析設計方法

共通性 (抽象類別): 
需要用什麼介面來處理類別的所有責任。
可變性 (子類別): 
對於特定的實做,要怎樣根據指定的規約來實做它。

共通性和可變性分析和、三種視角、抽象類別之間的關係

上述圖與抽象類別的對應關係

抽像類別與核心概念: 
抽象類別代表所有子類別關聯起來的核心概念。正是這個概念定義了子類別的共通性。
共通性與要使用的抽象類別
共通性定義需要使用的抽象類別。
可變性與抽象類別的子類別
共通性中辨別出的可變性將成為抽象類別的子類別。
規約與抽象類別的介面 
這些類別的介面對應於規約層次。

可測試性與架構關聯

內聚: 
程式碼更容易測試,因為程式碼只負責一項責任。
冗餘程式碼
如果冗餘度增加,會降低整個系統的可測試性。
可讀性
程式碼更好測試,因程式碼方法名稱和參數都能更精確地說明各自意圖。
封裝性
因耦合性很鬆更易測試,整體相互依賴性較少。

TDD 模式可以讓開發者思考,要如何做出易測試的架構,變相思考上述幾點,也和設計模式的理念相似。

設計模式三大類

Creational Pattern 創建型模式: 
用於描述如何建立物件,主要核心為把物件的建立和使用分開。
Structural Pattern 結構型模式
描述了在保持原結構的彈性和效能下,如何將類別或物件透過複合(composition)、繼承(inheritance)的方式,建構出更大更複雜的架構。
Behavioral Pattern 行為型模式
描述物或類別之間如何溝通協作。

單一職責原則 (Single Responsibility Principle)

定義:
一個類別應該只有一個職責。

範例:
Interface AuthInterface {
//驗證用戶名和密碼,並登錄用戶
public function login(string $username, string $password): void;

//登出用戶
public function logout(): void;

//發送通知
public function notify(): void;
}

class Auth implements AuthInterface{
public function login(string $username, string $password): void
{
//驗證用戶名和密碼,並登錄用戶
}

public function logout(): void
{
//登出用戶
}

public function notify(): void
{
//發送通知
}
}
上述範例 AuthInterface 類別負責登入驗證、登出和發送通知多個功能。假設登入驗證、登出和發送通知為不同職責,則此類別有多個職責,違反了 SRP 原則。若之後登入需其他的驗證方式,譬如 Google 第三方登入,會需要多新增一類別去實做,但是通知程式碼卻是一樣的,造成程式碼重複問題。
Interface AuthenticationInterface {
// 驗證用戶名和密碼,並登錄用戶
public function login(string $username, string $password): void;

// 登出用戶
public function logout(): void;
}
class Authentication implements AuthenticationInterface {
public function login(string $username, string $password): void
{
//驗證用戶名和密碼,並登錄用戶
}

public function logout(): void
{
//登出用戶
}
}
interface NotifyInterface {
// 發送通知
public function notify(): void;
}
class Notify implements NotifyInterface {
public function notify(): void
{
//
發送通知
}
}
class Auth {
private $authentication;
private $notify;

public function __construct(AuthenticationInterface $authentication, NotifyInterface $notify)
{
$this->authentication = $authentication;
$this->notify = $notify;
}

public function login(string $username, string $password): void
{
//驗證用戶名和密碼,並登錄用戶
$this->authentication->login($username, $password);
}

public function logout(): void
{
// 登出用戶
$this->authentication->logout();
}

public function notify(): void
{
// 修改用戶密碼
$this->notify->notify();
}
}
調整範例將登入驗證和登出職責集中到 AuthenticationInterface 類別,而發送通知的職責集中到 NotifyInterface 類別, Auth 類別負責實做具體方法。每個類別都只有一個職責,符合 SRP 原則,此原則使代碼更容易維護和擴展。假如現在要新增 Google 第三方登入驗證,只要多新增 GoogleAuthentication 的類別並實做相關方法即可。

目的:
提高內聚性,將與此類別無相關部份移除,使其內部成員都是相互關聯,並有著相似的目的和責任,使代碼更容易理解。

優點:
  • 易擴展和維護
  • 降低變更程式碼產生的風險
  • 減少代碼重複性
缺點:
  • 增加設計複雜度:需依規格來思考各類別職責的歸類,並且要創建更多的物件來實現相同功能。

開閉原則 (Open-Closed Principle)

定義:
軟體中的對象應該對擴展開放,對修改關閉。例如:類別、模組、函式等。

範例:
class ShopCart
{
private $items = [];
private $memberLevel = 0;

public function addItem($item)
{
$this->items[] = $item;
}

public function setMemberLevel($level)
{
$this->memberLevel = $level;
}

public function getTotal()
{
$total = 0;

foreach ($this->items as $item) {
$total += $item['price'] * $item['quantity'];
}

//如果之後有 Level3 Level4 Level5,或是不同 level 有不同的作法,持續擴充的過程中可能把原本的方法改錯了,造成系統出問題
if ($this->memberLevel === 1) {
$total *= 0.9;
} elseif ($this->memberLevel == 2) {
$total *= 0.88;
}

return $total;
}
}
上述範例中,購物車商品最終價格會因會員等級不同而有不同的折扣,現在只有 level 1/2 的會員等級,如果之後有 Level 3/4/5,或是不同 level 有各自的作法,再持續擴充的過程中可能把原本的方法改錯了,造成系統出問題。
interface LevelStrategy {
public function getPrice(int $price, int $quantity): int;
}
class LevelOne implements LevelStrategy
{
const DISCOUNT = 0.9;

public function getPrice(int $price, int $quantity): int
{
return $price * $quantity * self::DISCOUNT;
}
}
class LevelTwo implements LevelStrategy
{
const DISCOUNT = 0.88;

public function getPrice(int $price, int $quantity): int
{
return $price * $quantity * self::DISCOUNT;
}
}
class ShopCart
{
private $items = [];
private $priceStrategy;

//透過不同的 $level 設定對應的 priceStrategy,以此取得正確的折扣。
//後續有新的 level 規則,只需要實做新 level 的類別,不需更改到其他程式。
public function setStrategy(string $level)
{
$this->priceStrategy = app()->make("App\Principles\OCP\CorrectExample\Level$level");
}

public function addItem($item)
{
$this->items[] = $item;
}

public function setMemberLevel($level)
{
$this->memberLevel = $level;
}

public function getTotal()
{
$total = 0;

foreach ($this->items as $item) {
$total += $this->priceStrategy->getPrice($item['price'], $item['quantity']);
}

return $total;
}
}
將計算價格的方法拉出來做成 LevelStrategy 介面,並建立 level 1/2 類別並實做 LevelStrategy 介面方法,不同的會員等各自實做計算價格的具體方法。透過不同的會員等級設定對應的計算價格策略,以此取得正確的折扣。如此就算後續有新的會員等級價格規則,只需要實做新的類別並實做 LevelStrategy 介面方法,不需更改到其他程式,符合 OCP 原則。

目的:
減少因修改現有程式碼的同時不會對現有程式碼造成影響,使系統更容易擴展和維護。

優點:
  • 減少耦合性
  • 減少因修改現有程式而引起的錯誤和風險
缺點:
  • 設計得不當,會導致系統的過度抽象和複雜化。

里氏替換原則 (Liskov Substitution Principle)

定義:
子類別必須能夠完全替代父類別的位置,而不影響程式的正確性。

繼承 4 個規範:
  1. 子類別必須實做父類別所有抽象方法
  2. 子類別可以定義自己的方法
  3. 子類別在覆蓋或實做父類別方法時,輸入參數必須與父類別相同或更寬鬆。(參數類別能更寬鬆)
  4. 子類別在覆蓋或實做父類別方法時,輸出結果必須與父類別相同或更嚴謹。(返回類型必須更精確)

範例
  1. 子類別必須實做父類別所有抽象方法
    abstract class Shape {
    abstract public function getArea();
    }
    子類別有實做父類別 getArea() 方法
    class Circle extends Shape
    {
    private $radius;

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

    public function getArea(): float
    {
    return pi() * pow($this->radius, 2);
    }
    }
    子類別未實做父類別 getArea() 方法,發生錯誤
    class Square extends Shape
    {
    private $length;

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

    //未實做 getArea 方法
    }
  2. 子類別可以定義自己的方法
    class Shape {
    protected $color;

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

    public function getColor() {
    return $this->color;
    }
    }
    子類別可以新增自己的方法,但這些方法不能影響父類別的行為,也不能與父類別方法的命名衝突。
    class Square extends Shape {
    protected $sideLength;

    public function __construct($color, $sideLength) {
    parent::__construct($color);
    $this->sideLength = $sideLength;
    }

    public function getArea() {
    return pow($this->sideLength, 2);
    }
    }
  3. 子類別在覆蓋或實做父類別方法時,輸入參數必須與父類別參數類別層級相同或是更高。(參數類別能更寬鬆)
    class Food
    {
    public function getFood(): string
    {
    return "food";
    }
    }
    class DogFeed extends Food
    {
    public function getFood(): string
    {
    return "dog feed";
    }
    }
    子類別參數範圍大於父類別
    class Animal
    {
    public function eat(DogFeed $food): string
    {
    return "Animal is eating " . $food->getFood();
    }
    }

    class Dog extends Animal
    {
    public function eat(Food $food): string
    {
    return "Dog is eating..." . $food->getFood();
    }
    }
    在例子中,若 Dog 要覆寫 Animal 的方法,那傳入的參數範圍就要比 Animal 方法參數 DogFeed 層級相同或大才行。
    $animal = new Animal();
    $dog = new Dog();
    $food = new Food();
    $dogFeed = new DogFeed();

    $animal->eat($dogFeed); // 父類別方法
    $animal->eat($food); // 錯誤,因參數 Food 層級大於 Dog 層級
    $dog->eat($dogFeed); // 子類別方法
    $dog->eat($food); // 子類別方法
    子類別參數範圍小於父類別
    class Animal
    {
    public function eat(Food $food): string
    {
    return "Animal is eating " . $food->getFood();
    }
    }

    class Dog extends Animal
    {
    public function eat(DogFeed $food): string
    {
    return "Dog is eating..." . $food->getFood();
    }
    }
    此例子中,Dog 無法使用 DogFeed 層的參數,因父類別參數為 Food 層級,會發生錯誤,子類別應該可以完全替換調父類別並保持正確性。

  4. 子類別在覆蓋或實做父類別方法時,輸出結果可以被縮小。
    class Animal {
    public function eat() {
    return "I eat something.";
    }
    }

    class Dog extends Animal {
    public function eat(): string {
    return "I eat bones.";
    }
    }
    此處 Dog 覆蓋了 Animal 的 eat 方法,並且輸出是更具体的字符串,在 Animal 中的 eat 方法並未定義輸出具體類別,而 Dog 則是定義了字串類別,如此縮小了輸出類別。
    $dog = new Dog(); // 輸出 I eat bones.

目的:
使子類別在使用父類別的方法时,不產生意料之外的錯誤,並保證系統的擴展性。

優點:
  • 提高擴展性
  • 增加程式碼重用性
缺點:
  • 需考慮子類別行為是否符合父類別方法
  • 增加耦合性,繼承在調整父類別時會連帶影響子類別

介面隔離原則 (Interface Segregation Principle)

定義:
一個類別不應該強迫使用它不需要的接口,換句話說,一個類別應該只提供它需要的接口。

範例:
interface Worker
{
public function work(): void;
public function rest(): void;
}
class Human implements Worker
{
public function work(): void
{
echo "work by human";
}

public function rest(): void
{
echo "rest by human";
}
}

//機器人不需要休息,會造成 rest 方法空實做
class Robot implements Worker
{
public function work(): void
{
echo "work by human";
}

public function rest(): void
{
//機器人不需要休息,空實做
}
}
上述範例中,worker 定義了 work 和 rest 方法,讓 Human 和 Robot 類別實做,但因 
Robot 不需要休息,故 rest 方法需要空實做,違反了 LSP 原則。
interface Worker
{
public function work(): void;
}

interface Rest
{
public function rest(): void;
}
class Human implements Worker, Rest
{
public function work(): void
{
echo "work by human";
}

public function rest(): void
{
echo "rest by human";
}
}

// work rest 分成兩個介面,只實做需要的介面
class Robot implements Worker
{
public function work(): void
{
echo "work by human";
}
}
此處將 work 和 rest 創建各自的介面,Human 和 Robot 類別可以依需求選擇需要實做的介面,Human 實做 Worker 和 Rest 介面,而 Robot 不休息只要實做 Worker 介面就好。

目的:
更好地管理和設計接口,減少代碼中的冗餘和不必要的功能,減少代碼的耦合度。

優點:
  • 減少耦合性
  • 減少冗餘和不必要的功能

缺點:
  • 增加複雜性並且會限制靈活性。

SRP V.S ISP:
SRP: 
用於設計單一功能的類別,重於單一職責原則,即一個類別只應該有一個職責。
ISP:
 用於設計多個功能的類別,重於接口的分離,即一個類別應該只提供它需要的接口。

依賴反轉原則 (Dependency Inversion Principle)

定義:
  1. 高層模組 (UserService) 不應該依賴於低層模組 (GoogleMail),兩者都應該依賴於抽象接口 (MailInterface)。
  2. 抽象接口 (MailInterface) 不應該依賴於具體實現細節 (GoogleMail),具體實現細節 (GoogleMail) 應該依賴於抽象接口 (MailInterface) 。

範例:
class UserService
{
public function register(string $username, string $email, string $password): void
{
// create user in database
// send welcome email to user
$emailService = new GoogleMail();
$emailService->send($email, 'Welcome', $username . 'Thank you for registering!');
}
}

class GoogleMail
{
public function send(string $to, string $subject, string $body): void
{
// send email using google mail function
}
}
上述範例中,UserService 是高層次模組,GoogleMail 是低層次模組,但是 UserService 卻直接依賴 GoogleMail,違反了DIP 第一條原則。每當再出現一種信箱,就要調整一次現有的程式,不是好的設計。
interface MailInterface {
public function send(string $to, string $subject, string $body): void;
}
class GoogleMail implements MailInterface
{
public function send(string $to, string $subject, string $body): void
{
// send email using google mail function
}
}

class OutlookMail implements MailInterface
{
public function send(string $to, string $subject, string $body): void
{
// send email using outlook mail function
}
}
class UserService {
protected MailInterface $mailInterface;

public function register($username, $email, $password, $type) {
// create user in database
// send welcome email to user
$this->mailInterface = app()->make('App\Principles\DIP\CorrectExample\\' . $type . 'Mail');
$this->mailInterface->send($email, 'Welcome', $username . 'Thank you for registering!');
}
}
此處將 send 行為抽出,封裝成 MailInterface 介面,GoogleMail 和 OutlookMail 去實做此介面。 UserService 只需要知道是用 MailInterface 類別寄信,而不用知道是用實際是用什麼類型的信箱,透過 client 提供的類型,來產生對應的具體類別。如果未來新增一個信箱,只要新增對應的具體類別,不用在異動原有程式。
GoogleMail 和 OutlookMail 寄信的方法 (具體實現細節),依照著 MailInterface 類別實做 (抽象接口),而不是 MailInterface 照著兩者的方向走,符合 DIP 第二點原則。

目的:
將具體實現細節隱藏起來,高層模組只依賴於抽象接口,具體實現細節可以被替換或升級而不影響高層模組的運行。降低耦合度並提高擴展性。

優點:
  • 減少耦合性
  • 提高擴展性
缺點:
  • 較為複雜,需額外定義抽象接口和類型轉換。

Dependency Injection:
依賴注入指將一個元件所需的依賴關係,通過構造函數、屬性或者方法參數等方式從外部傳遞進來,而不是在元件內部自己創建或者管理依賴。依賴注入能讓元件的依賴關係清晰地呈現,並且方便替換或修改。還可以實現 SRP 和 DIP 原則。
實際應用中,依賴注入通常與依賴注入容器(Dependency Injection Container)一起使用。DIC 可以自動創建和管理元件之間的依賴關係,使得應用程序的開發和維護變得更加容易。Laravel 的核心容器就是 DIC,透過 app 的 bind 和 make 來註冊和解析服務。

合成/聚合複用原則 (Composite-Aggregate Reuse Principle)

定義:
合成:
合成為強關聯關係,通常是 A 類別成員變量為 B 類別,而 B 類別是不能獨立存在的,當 A 類別消失時, B 類別也會一起消失。B 類別透過 A 類別內部創建出來。用在創建較複雜的結構,並且需要實現更強的關聯關係。
聚合
聚合為弱關聯關係,通常是 A 類別成員變量為 B 類別,而 B 類別可以獨立存在的,當 A 類別消失時, B 類別還是會存在。B 類別可以通過 A 類別構造函數傳入或者 setter 方法注入。用在創建較簡單的結構,並且需要實現較弱的關聯關係。
範例:
合成:
class Car
{
private $engine;

//汽車類別必須包含一個引擎才能正常工作
public function __construct()
{
$this->engine = new Engine();
}

public function run(): void
{
$this->engine->start();
echo "汽車出發!";
}
}

class Engine
{
//引擎類別必須要在汽車類別才能發揮作用,如果沒汽車類別引擎類別則沒意義
public function start(): void
{
echo "引擎啟動!";
}
}
在上述範例中,Car 類別和 Engine 類別為合成關係,因為 Car 類別必須包含一個 Engine 類別的,而 Engine 類別必須要在汽車類別才能發揮作用,如果沒汽車類別 Engine 類別則沒意義,故不能獨立存在。
class User
{
private $address;

//User 類別可以包含一個 Address 類別,而 Address 類別是獨立存在的
public function __construct(Address $address)
{
$this->address = $address;
}

public function __toSing(): string
{
return '住在' . $this->address;
}
}

class Address
{
private $city;
private $street;

public function __construct($city, $street)
{
$this->city = $city;
$this->street = $street;
}

public function __toString(): string
{
return $this->city . $this->street;
}
}
上述範例中,User 類別和 Address 類別為聚合關係,因為 User  類別可以包含一個 Address 類別,而 Address 類別是可以獨立存在的,透過 User 類別的建構式傳入。
合成/聚合 V.S 繼承:
合成/聚合
  • 合成/聚合時並不知道使用的類別的實作細節,只需要使用類別接口,維持類別的封裝性,又稱黑箱複用。
  • 耦合度較低,新類別只能透過複用類別的接口取得內容。
  • 靈活性較高,可以靈活的組合不同的對象滿足需求,並且能載運行時更改成員對象。
繼承
  • 繼承時子類別會清楚父類別的實做細節,故繼承破壞了類別的封裝。因父類別對子類別是透明的,所以又稱白箱複用。
  • 耦合度高,父類別做任何更動都會影響到子類別。
  • 繼承是一種靜態關係,在編譯時就已經確定,不能隨意更改。