iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 5
1

本文同步更新於blog

Interface Segregation Principle (ISP)

No client should be forced to depend on methods it does not use.

客戶不應該依賴他們不使用的方法。


透過將龐大的介面,拆分成一個個的小介面,
解開耦合,進而容易重構、修改及部署。

前陣子Johnny提出ISP的目的,是為了多型的實作,也是不錯的觀點。
(註:小介面比較易於實作)


Def. 全自動咖啡機:會自己磨粉、沖煮的咖啡機

假使我們實作一個咖啡機介面
內有兩種方法,分別是磨粉沖煮

隨著對咖啡的興趣增加,我們可能會想玩手沖壺。

這時會發現,原本咖啡機介面職責有點多。
手沖壺沒辦法實作咖啡機介面(不會磨粉)。

透過將介面拆分成:磨粉介面沖煮介面
我們可以讓原本的全自動咖啡機實作兩種介面。
而手沖壺實作沖煮介面。

這下原本的煮咖啡程式,就可以寫好不修改了。


以下提供範例程式碼:

情境:目前我們有一台全自動咖啡機

  • 首先定義全自動咖啡機介面
<?php

namespace App\SOLID\ISP\CoffeeMachine\Contracts;

interface AutomaticCoffeeMachineInterface
{
    public function grind($coffeeBeans);

    public function brew($coffeePowder);
}

  • 接著是全自動咖啡機
<?php

namespace App\SOLID\ISP\CoffeeMachine;

use App\SOLID\ISP\CoffeeMachine\Contracts\AutomaticCoffeeMachineInterface;

class AutomaticCoffeeMachine implements AutomaticCoffeeMachineInterface
{
    public function grind($coffeeBeans)
    {
        if ($coffeeBeans == '咖啡豆') {
            return '咖啡粉';
        }
    }

    public function brew($coffeePowder)
    {
        if ($coffeePowder == '咖啡粉') {
            return '咖啡';
        }
    }
}
  • 最後是目前的沖煮方式
<?php

namespace App\SOLID\ISP\CoffeeMachine;

class Program
{
    protected $coffeeGrinder;

    protected $coffeeMaker;

    public function getCoffeeByAutomaticCoffeeMachine($coffeeBeans)
    {
        $automaticCoffeeMachine = new AutomaticCoffeeMachine();
        $this->coffeeGrinder = $automaticCoffeeMachine;
        $this->coffeeMaker = $automaticCoffeeMachine;

        return $this->getCoffee($coffeeBeans);
    }

    private function grind($coffeeBeans)
    {
        return $this->coffeeGrinder->grind($coffeeBeans);
    }

    private function brew($coffeePowder)
    {
        return $this->coffeeMaker->brew($coffeePowder);
    }

    private function getCoffee($coffeeBeans)
    {
        $coffeePowder = $this->grind($coffeeBeans);
        $coffee = $this->brew($coffeePowder);
        return $coffee;
    }
}

然而,隨著對咖啡興趣增加,我們想來玩玩摩卡壺。
卻發現摩卡壺不會磨粉...


需求一:將全自動咖啡機介面職責分離,拆分出磨粉介面與沖煮介面

  • 首先定義磨粉介面
<?php

namespace App\SOLID\ISP\CoffeeMachine\Contracts;

interface CoffeeGrinder
{
    public function grind($coffeeBeans);
}

  • 接著定義沖煮介面
<?php

namespace App\SOLID\ISP\CoffeeMachine\Contracts;

interface CoffeeMaker
{
    public function brew($coffeePowder);
}

  • 最後修改原本的全自動咖啡機,讓它實作新的兩個介面
<?php

namespace App\SOLID\ISP\CoffeeMachine;

use App\SOLID\ISP\CoffeeMachine\Contracts\CoffeeGrinder;
use App\SOLID\ISP\CoffeeMachine\Contracts\CoffeeMaker;

class AutomaticCoffeeMachine implements CoffeeGrinder, CoffeeMaker
{
    public function grind($coffeeBeans)
    {
        if ($coffeeBeans == '咖啡豆') {
            return '咖啡粉';
        }
    }

    public function brew($coffeePowder)
    {
        if ($coffeePowder == '咖啡粉') {
            return '咖啡';
        }
    }
}


需求二:實作磨豆機與摩卡壺

  • 實作磨豆機(因為拆分出沖煮介面,它不需要知道如何沖煮)
<?php

namespace App\SOLID\ISP\CoffeeMachine;

use App\SOLID\ISP\CoffeeMachine\Contracts\CoffeeGrinder;

class NormalCoffeeGrinder implements CoffeeGrinder
{
    public function grind($coffeeBeans)
    {
        if ($coffeeBeans == '咖啡豆') {
            return '咖啡粉';
        }
    }
}

  • 實作摩卡壺(因為拆分出磨豆介面,它不需要知道如何磨豆)
<?php

namespace App\SOLID\ISP\CoffeeMachine;

use App\SOLID\ISP\CoffeeMachine\Contracts\CoffeeMaker;

class Mocalpot implements CoffeeMaker
{
    public function brew($coffeePowder)
    {
        if ($coffeePowder == '咖啡粉') {
            return '咖啡';
        }
    }
}

  • 最後新增煮咖啡的方法,用磨豆機與摩卡壺的組合
<?php

namespace App\SOLID\ISP\CoffeeMachine;

use App\SOLID\ISP\CoffeeMachine\Contracts\CoffeeGrinder;
use App\SOLID\ISP\CoffeeMachine\Contracts\CoffeeMaker;
use App\SOLID\ISP\CoffeeMachine\NormalCoffeeGrinder;
use App\SOLID\ISP\CoffeeMachine\MocalPot;

class Program
{
    /**
     * @var CoffeeGrinder
     */
    protected $coffeeGrinder;

    /**
     * @var CoffeeMaker
     */
    protected $coffeeMaker;

    public function getCoffeeByAutomaticCoffeeMachine($coffeeBeans)
    {
        $automaticCoffeeMachine = new AutomaticCoffeeMachine();
        $this->setCoffeeGrinder($automaticCoffeeMachine);
        $this->setCoffeeMaker($automaticCoffeeMachine);

        return $this->getCoffee($coffeeBeans);
    }

    public function getCoffeeByNormalCoffeeGrinderAndMocalPot($coffeeBeans)
    {
        $normalCoffeeGrinder = new NormalCoffeeGrinder();
        $mocalPot = new MocalPot();
        $this->setCoffeeGrinder($normalCoffeeGrinder);
        $this->setCoffeeMaker($mocalPot);

        return $this->getCoffee($coffeeBeans);
    }

    private function grind($coffeeBeans)
    {
        return $this->coffeeGrinder->grind($coffeeBeans);
    }

    private function brew($coffeePowder)
    {
        return $this->coffeeMaker->brew($coffeePowder);
    }

    private function getCoffee($coffeeBeans)
    {
        $coffeePowder = $this->grind($coffeeBeans);
        $coffee = $this->brew($coffeePowder);
        return $coffee;
    }

    private function setCoffeeGrinder(CoffeeGrinder $coffeeGrinder)
    {
        $this->coffeeGrinder = $coffeeGrinder;
    }

    private function setCoffeeMaker(CoffeeMaker $coffeeMaker)
    {
        $this->coffeeMaker = $coffeeMaker;
    }
}

透過介面隔離原則,拆分職責,我們就可以玩更多花樣的咖啡沖煮方法了。

而煮咖啡方法也不必因為器具更換而修改。
符合開放封閉原則

ʕ •ᴥ•ʔ:ISP不就是介面版本的單一職責原則嗎?(笑)


上一篇
Day4. 裡氏替換原則
下一篇
Day6. 依賴反轉原則
系列文
你終究都要學設計模式的,那為什麼不一開始就學呢?57
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言