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类别图说明。)


ʕ •ᴥ•ʔ:动物界-脊索动物门-哺乳纲-灵长目-人科-人属-人。


<<:  Gulp npm install 中的 --save 与 --save-dev 差异 DAY94

>>:  资产剥离(divestiture)

Day25:今天来聊一下Hacking Wireless Networks

Wireless Networking技术我们每天都在使用,但其便利性也存在许多安全问题。 基本上,...

[Day 11] 让tinyML听见你的呼唤

在先前[Day 09] tinyML开胃菜Arduino IDE上桌(下)已经简单介绍过Arduin...

Javascript 运算子、型别与文法 - 陈述式与表达式

陈述式与表达式的差异 陈述式:不会回传结果,而是执行特定的程序码,如使用 if...else、swi...

[Day19] Flutter - Const: Shared(part3)

前言 Hi, 我是鱼板伯爵今天要介绍Const,利用它来宣告一些重复使用且不变的值,教学内容只会撷取...

Re: 新手让网页 act 起来: Day06 - PropTypes

昨天我们介绍完如何建立一个元件,今天就来介绍 PropTypes,让建立的元件更加的完整吧! Pro...