iT邦幫忙

第 12 屆 iThome 鐵人賽

1
Software Development

你終究都要學設計模式的,那為什麼不一開始就學呢?系列 第 48

Day48. 範例:生物分類學(組合模式)

本文同步更新於blog

情境:原本的生物分類學(界門綱目科屬種)

<?php

namespace App\CompositePattern\Taxonomy;

class Program
{
    public function getTaxonomy()
    {
        echo '動物界
-- 脊索動物門
---- 哺乳綱
------ 雙門齒目
-------- 無尾熊科
---------- 無尾熊屬
------------ 無尾熊

------ 食肉目
-------- 熊科
---------- 大貓熊屬
------------ 大貓熊

';
    }
}

(註:排版是因為測試時不能有空格,也間接說明了這是個脆弱測試)

我們利用「-」來做出層級的分類概念。

經由分類發現,無尾熊與大貓熊同屬動物界-脊索動物門-哺乳綱。

讓我們透過組合模式,將其改寫成樹形架構!


需求一:運用組合模式

  • 首先定義組合介面 (Component),採取透明模式 (uniformity)
<?php

namespace App\CompositePattern\Taxonomy\Contracts;

interface Component
{
    public function add(Component $component);

    public function remove(Component $component);

    public function displayClassifiaction(int $depth);
}


  • 定義DashHelper (重構時發現枝節點與葉節點可共用的方法)
<?php

namespace App\CompositePattern\Taxonomy\Traits;

trait DashHelper
{
    /**
     * @param integer $count
     * @return string
     */
    public function getDashes(int $count)
    {
        $dash = '';
        for ($i = 0; $i < $count; $i++) {
            $dash = $dash . '-';
        }

        return $dash;
    }
}

DashHelper目的是做出不同層的分類。


  • 定義枝節點類別 (Composite)
<?php

namespace App\CompositePattern\Taxonomy;

use App\CompositePattern\Taxonomy\Contracts\Component;
use App\CompositePattern\Taxonomy\Traits\DashHelper;

class Composite implements Component
{
    use DashHelper;

    /**
     * @var string
     */
    public $name;

    /**
     * @var Component[]
     */
    protected $children = [];

    /**
     * @param string $name
     */
    public function __construct(string $name)
    {
        $this->name = $name;
    }

    /**
     * @param Component $component
     * @return void
     */
    public function add(Component $component)
    {
        $this->children[$component->name] = $component;
    }

    /**
     * @param Component $component
     * @return void
     */
    public function remove(Component $component)
    {
        unset($this->children[$component->name]);
    }

    /**
     * @param integer $depth
     * @return void
     */
    public function displayClassifiaction(int $depth)
    {
        $this->displaySelfClassification($depth);
        $this->displayChildrenClassification($depth);
    }

    /**
     * @param int $depth
     * @return void
     */
    private function displaySelfClassification(int $depth)
    {
        $dashes = $this->getDashes($depth);

        if (strlen($dashes) == 0) {
            echo "$this->name\n";
            return;
        }

        echo "$dashes $this->name\n";
    }

    /**
     * @param integer $depth
     * @return void
     */
    private function displayChildrenClassification(int $depth)
    {
        foreach ($this->children as $child) {
            $child->displayClassifiaction($depth + 2);
        }
    }
}

枝節點會先印出自己的分類名稱,接著加層數給子物件。


  • 定義葉節點類別 (Leaf)
<?php

namespace App\CompositePattern\Taxonomy;

use App\CompositePattern\Taxonomy\Contracts\Component;
use App\CompositePattern\Taxonomy\Traits\DashHelper;
use Exception;

class Leaf implements Component
{
    use DashHelper;

    /**
     * @var string
     */
    public $name;

    /**
     * @param string $name
     */
    public function __construct(string $name)
    {
        $this->name = $name;
    }

    /**
     * @param Component $component
     * @throws Exception
     */
    public function add(Component $component)
    {
        throw new Exception('Cannot add to a leaf');
    }

    /**
     * @param Component $component
     * @throws Exception
     */
    public function remove(Component $component)
    {
        throw new Exception('Cannot remove from a leaf');
    }

    /**
     * @param integer $depth
     * @return void
     */
    public function displayClassifiaction(int $depth)
    {
        $dashes = $this->getDashes($depth);
        echo "$dashes $this->name\n\n";
    }
}

葉節點只會列出自己的分類名稱,而且不允許對子物件的操作。


  • 最後修改客戶端的程式碼
<?php

namespace App\CompositePattern\Taxonomy;

class Program
{
    public function getTaxonomy()
    {
        $animalia = new Composite('動物界');
        $chordata = new Composite('脊索動物門');
        $mammalia = new Composite('哺乳綱');

        $animalia->add($chordata);
        $chordata->add($mammalia);

        // koala
        $diprotodontia = new Composite('雙門齒目');
        $phascolarctidae = new Composite('無尾熊科');
        $phascolarctos = new Composite('無尾熊屬');
        $phascolarctosCinereus = new Leaf('無尾熊');

        $diprotodontia->add($phascolarctidae);
        $phascolarctidae->add($phascolarctos);
        $phascolarctos->add($phascolarctosCinereus);

        $mammalia->add($diprotodontia);

        // panda
        $carnivora = new Composite('食肉目');
        $ursidae = new Composite('熊科');
        $ailuropoda = new Composite('大貓熊屬');
        $ailuropodaMelanoleuca = new Leaf('大貓熊');

        $carnivora->add($ursidae);
        $ursidae->add($ailuropoda);
        $ailuropoda->add($ailuropodaMelanoleuca);

        $mammalia->add($carnivora);

        $animalia->displayClassifiaction(0);
    }
}

(註:此處的變數命名參考學名)


[單一職責原則]
透過找出可以繼續遞迴的部分,分出枝節點與葉節點。

[開放封閉原則]
可以於組合中新增/修改某節點,不去影響其他節點的行為。

[依賴反轉原則]
客戶依賴於抽象的組合介面 (Component)
枝節點與葉節點實現抽象的組合介面 (Component)

最後附上類別圖:
https://ithelp.ithome.com.tw/upload/images/20201216/201116304aK3tSUXnK.png
(註:若不熟悉 UML 類別圖,可參考UML類別圖說明。)


ʕ •ᴥ•ʔ:動物界-脊索動物門-哺乳綱-靈長目-人科-人屬-人。


上一篇
Day47. 組合模式
下一篇
Day49. 橋接模式
系列文
你終究都要學設計模式的,那為什麼不一開始就學呢?57
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言