组合(Composite)模式跟我们之前讲的面向对象设计中的“组合关系(通过组合来组装两个类)”,完全是两码事。这里讲的“组合模式”,主要是用来处理树形结构数据。这里的“数据”,你可以简单理解为一组对象集合。正因为其应用场景的特殊性,数据必须能表示成树形结构,这也导致了这种模式在实际的项目开发中并不那么常用。但是,一旦数据满足树形结构,应用这种模式就能发挥很大的作用,能让代码变得非常简洁。

组合模式在 GoF 的《设计模式》一书中,是这样定义的:

Compose objects into tree structure to represent part-whole hierarchies.Composite lets client treat individual objects and compositions of objects uniformly.

翻译成中文就是:将一组对象组织(Compose)成树形结构,以表示一种“部分 - 整体”的层次结构。组合让客户端(在很多设计模式书籍中,“客户端”代指代码的使用者。)可以统一单个对象和组合对象的处理逻辑。

组合模式的结构与实现

结构

  1. 组件 (Component) 接口描述了树中简单项目和复杂项目所共有的操作。
  2. 叶节点 (Leaf) 是树的基本结构, 它不包含子项目。一般情况下, 叶节点最终会完成大部分的实际工作, 因为它们无法将工作指派给其他部分。
  3. 容器 (Container)——又名 “组合 (Composite”——是包含叶节点或其他容器等子项目的单位。 容器不知道其子项目所属的具体类, 它只通过通用的组件接口与其子项目交互。容器接收到请求后会将工作分配给自己的子项目, 处理中间结果, 然后将最终结果返回给客户端。
  4. 客户端 (Client) 通过组件接口与所有项目交互。 因此, 客户端能以相同方式与树状结构中的简单或复杂项目交互。

代码实现

Component.php

<?php
namespace DesignPatterns\Composite\example;
/**
 * Component为组合中对象声明的接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口
 * 用于访问和管理component的子部件。
 * Class Component
 * @package DesignPatterns\Composite\example
 */
abstract class Component
{
    protected $name;
    public function __construct($name)
    {
        $this->name = $name;
    }
    //通常用add和remove方法来提供增加或移除树枝货树叶的功能
    abstract public function Add(Component $component);
    abstract public function Remove(Component $component);
    abstract public function Display(int $depth);
}

Leaf.php

<?php
namespace DesignPatterns\Composite\example;
/**
 * leaf在组合中表示叶节点对象,叶节点对象没有子节点。
 * Class Leaf
 * @package DesignPatterns\Composite\example
 */
class Leaf extends Component
{
    // 由于叶子没有再增加分枝和树叶,所以add和remove方法实现它没有意义,
    // 但这样做可以消除叶节点和枝节点对象在抽象层次的区别,它们具有完全一致的接口
    public function Add(Component $component)
    {
        echo "can not add to a leaf<br>";
    }

    public function Remove(Component $component)
    {
        echo "can not remove to a leaf<br>";
    }

    public function Display(int $depth)
    {
        echo str_repeat('-', $depth) . $this->name . "<br>";
    }
}

Composite.php

<?php
namespace DesignPatterns\Composite\example;
/**
 * composite定义有枝节点行为,用来存储子部件,
 * 在Component接口中实现与子部件有关的操作,比如增加add和删除remove.
 * Class Composite
 * @package DesignPatterns\Composite\example
 */
class Composite extends Component
{
    private $childern = [];//一个子对象集合用来存储其下属的枝节点和叶节点。
    public function Add(Component $component)
    {
        array_push($this->childern, $component);
    }

    public function Remove(Component $component)
    {
        foreach ($this->childern as $key => $value) {
            if ($component === $value) {
                unset($this->childern[$key]);
            }
        }
    }
    // 显示其枝节点名称,并对其下级进行遍历
    public function Display(int $depth)
    {
        echo str_repeat('-', $depth).$this->name."<br>";
        foreach ($this->childern as $component) {
            $component->display($depth + 2);
        }
    }
}

client.php

<?php
namespace DesignPatterns\Composite\example;
require dirname(__DIR__).'/../vendor/autoload.php';

/**
 * 客户端代码可以通过Component接口操作组合部件(Composite)对象
 * Class Client
 * @package DesignPatterns\Composite\example
 */
class Client
{
    public function run()
    {
        //生成树根root
        //根上长出两页 Leaf A 和 Leaf B
        $root = new Composite('root');
        $root->add(new Leaf("Leaf A"));
        $root->add(new Leaf("Leaf B"));

        //根上长出分支Composite X
        //分枝上也有两叶 Leaf XA 和 LeafX B
        $comp = new Composite("Composite X");
        $comp->add(new Leaf("Leaf XA"));
        $comp->add(new Leaf("Leaf XB"));
        $root->add($comp);
        //在Composite 在长出 CompositeXY分枝 分枝上也有两叶 Leaf XYA 和 LeafXYB
        $comp2 = new Composite("Composite XY");
        $comp2->add(new Leaf("Leaf XYA"));
        $comp2->add(new Leaf("Leaf XYB"));
        $comp->add($comp2);

        $root->add(new  Leaf("Leaf C"));//根部分枝Leaf C
        $leaf = new Leaf("Leaf D");
        $root->add($leaf);//根部分枝Leaf D
        $root->remove($leaf);//移除根部分枝LeafD
        $root->display(1);//显示
    }
}
$worker = new Client();
$worker->run();

运行结果:

透明方式与安全方式

透明方式就是说在Component中声明的用来管理子对象的方法,其中包括Add、Remove等。继承或实现Component的子类都具备Add和Remove方法,这样做的好处就是叶节点和枝节点对于外界没有区别。它们具备完全一致的行为接口。但问题也很明显,因为Leaf类并不具备Add,Remove方法的功能所以实现它们并没有意义。
安全方式就是在Component接口中不在声明Add和Remove方法,那么子类也不需要去实现它,而在Composite中声明所有用来管理子对象的方法。这样就不会出现透明方式的问题了,不过由于不够透明,所有树枝和树叶将不具有相同的接口。客户端调用就需要做相应的判断,带来了不便。

组合模式示例

假如现在你要为一个家在全国许多城市都有分销机构的大公司开发一款办公管理系统。总部有人力资源,财务和运营等部门。这是很常见的OA系统,很快一段时间后产品就正式运行了,过了一段时间后,客户希望在他们全部分公司推广使用。他们在北京有总部,在全国其他城市设有分公司,比如上海设有华东区分部,然后在一些省会还会设有办事处,比如南京办事处,杭州办事处。现在有一个问题是,客户希望总公司的人力资源部,财务部等办公管理功能可以在所有的分公司或者办事处都可以使用。如下所示:

总部,分部和办事处呈树状结构,也就是有组织结构,所以我们就不能进行简单的平行管理,因为实际开发中,我们就需要一个一个去判断他是总公司的财务还是分公司的财务,然后去执行不同的方法。

我们其实可以发现类似这种部分与整体的情况很多,例如,买电脑的商家。可以卖单独的配件,也可以卖整装的主机。又如复制文件,你可以一个一个复制,还可以整个文件夹进行复制。再比如文本编辑,你可以给单个字加粗、变色、改字体等,当然也可以给整段文字做相同的操作。其本质都是相同的问题。
其实我们的分公司或办事处与总公司的关系就是部分与整体的关系。我们希望公司的组织架构,比如人力资源部,财务部的管理功能可以复用于分公司。这其实就是部分与整体可以被一致对待的问题。
回到这个项目,如果我们把北京总公司比做数的根部,而它下属的分公司就是树的分枝,至于办事处就是更小的分枝,而它们相关的职能部门由于没有分枝了,所以可以理解为树叶。尽管天下没有两片相同的树叶,但同一颗树长出的树叶也不会差到哪去,也就是说,我们希望总部的财务功能复制用到子公司,最好的方法就是我们在处理总公司的财务管理功能和子公司的财务功能的方法是一样的。而本章所讲的组合模式恰好就能够处理这种需求。下面是代码实现
Company.php

<?php
namespace DesignPatterns\Composite\OA;
/**
 * 公司类 抽象类或者接口
 * Class Company
 * @package DesignPatterns\Composite\OA
 */
abstract class Company
{
    protected $name;
    public function __construct($name)
    {
        $this->name = $name;
    }
    abstract public function Add(Company $company);//增加
    abstract public function Remove(Company $company);//移除
    abstract public function Display(int $depth);//显示
    abstract public function LineOfDuty();//增加履行责任的方法,不同的部门需要履行不同的责任

}

ConcreteCompany.php

<?php
namespace DesignPatterns\Composite\OA;

/**
 * 具体公司类 实现接口 树枝节点
 * Class ConcreteCompany
 * @package DesignPatterns\Composite\OA
 */
class ConcreteCompany extends Company
{

    private $childern = [];//一个子对象集合用来存储其下属的枝节点和叶节点。
    public function Add(Company $company)
    {
        array_push($this->childern, $company);
    }

    public function Remove(Company $company)
    {
        foreach ($this->childern as $key => $value) {
            if ($company === $value) {
                unset($this->childern[$key]);
            }
        }
    }
    // 显示其枝节点名称,并对其下级进行遍历
    public function Display(int $depth)
    {
        echo str_repeat('-', $depth).$this->name."<br>";
        foreach ($this->childern as $company) {
            $company->display($depth + 2);
        }
    }

    public function LineOfDuty()
    {
        foreach ($this->childern as $company) {
            $company->LineOfDuty();
        }
    }
}

HRDepartment.php

<?php
namespace DesignPatterns\Composite\OA;
/**
 * 人力资源部 树叶节点
 * Class HRDepartment
 * @package DesignPatterns\Composite\OA
 */
class HRDepartment extends Company
{

    public function Add(Company $company){}

    public function Remove(Company $company){}

    public function Display(int $depth)
    {
        echo str_repeat('-', $depth) . $this->name . "<br>";
    }

    public function LineOfDuty()
    {
        echo  $this->name." 员工招聘培训管理<br>";
    }
}

FinanceDepartment.php

<?php

namespace DesignPatterns\Composite\OA;
/**
 * 财务部 树叶节点
 * Class HRDepartment
 * @package DesignPatterns\Composite\OA
 */
class FinanceDepartment extends Company
{

    public function Add(Company $company){}

    public function Remove(Company $company){}

    public function Display(int $depth)
    {
        echo str_repeat('-', $depth) . $this->name . "<br>";
    }

    public function LineOfDuty()
    {
        echo $this->name . " 公司财务收支管理<br>";
    }
}

运行Client.php

<?php

namespace DesignPatterns\Composite\OA;
require dirname(__DIR__) . '/../vendor/autoload.php';

/**
 * 客户端
 * Class Client
 * @package DesignPatterns\Composite\OA
 */
class Client
{
    public function run()
    {
        $root = new ConcreteCompany("北京总公司");
        $root->Add(new HRDepartment("总公司人力资源部"));
        $root->Add(new FinanceDepartment("总公司财务部"));

        $comp = new ConcreteCompany("上海华东分公司");
        $comp->Add(new HRDepartment("上海华东分公司人力资源部"));
        $comp->Add(new FinanceDepartment("上海华东分公司财务部"));
        $root->add($comp);

        $comp1 = new ConcreteCompany("南京办事处");
        $comp1->Add(new HRDepartment("南京办事处人力资源部"));
        $comp1->Add(new FinanceDepartment("南京办事处财务部"));
        $root->add($comp1);

        $comp2 = new ConcreteCompany("杭州办事处");
        $comp2->Add(new HRDepartment("杭州办事处人力资源部"));
        $comp2->Add(new FinanceDepartment("杭州办事处财务部"));
        $root->add($comp2);

        echo "结构图<br>";
        echo $root->Display(1);
        echo "职责<br>";
        echo $root->LineOfDuty();


    }
}

$worker = new Client();
$worker->run();

运行结果:

示例中,组合模式定义了,包含人力资源部和财务部这些基本对象和分公司、办事处等组合对象的类层次结构,基本对象可以被组合成更复杂的组合对象,而这个组合对象,又可以被组合,这样不断递归下去,客户代码中,任何用到基本对象的对象都可以使用组合对象。用户不在关心到底是处理一个叶节点还是一个组合组件,也就不用为定义组合而写一些判断语句。简单的说就是组合模式让客户可以一致的使用组合结构和单个对象。

总结

组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。组合模式,将一组对象组织成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用组合模式的前提在于,你的业务场景必须能够表示成树形结构。所以,组合模式的应用场景也比较局限,它并不是一种很常用的设计模式

github示例:https://github.com/yangpanyao/design-patterns/tree/master/Composite
参考资料:组合模式:如何设计实现支持递归遍历的文件系统目录树结构?
参考书籍:大话设计模式

Last modification:June 26th, 2020 at 07:10 pm