设计模式:灵活编程(组合模式)

组合可比继承提供更多的灵活性。composition provides greater flexibility than inheritance. -- 《深入PHP 面向对象、模式与实践》

介绍

组合模式可以很好地聚合和管理许多相似的对象,因而对客户端代码来说,一个独立对象和一个对象集合是没有差别的(部分-整体)。组合模式定义了一个单根继承体系,使具有截然不同职责的集合可以并肩工作。

简单来说,组合模式是运用面向对象的方式来有效的处理树形结构,如下图:

  •  Component :抽象构件,树形结构的根节点,为组合对象声明接口,也可以为共有接口实现缺省行为。
  •  Leaf :树形结构的叶节点,该节点没有子节点,实现抽象构件所声明的接口。
  •  Compsite :树形结构的树枝节点,该节点有子节点,实现抽象构件所声明的接口,存储子部件。

下图为UML类图:

问题

先构建一个虚拟场景:统计公司的职工数量。先定义几个模型:

abstract class Department
{
    abstract function employees();
}

class SalesDepartment extends Department
{
    public function employees()
    {
        return 200;
    }
}

class TechnologyDepartment extends Department
{
    public function employees()
    {
        return 123;
    }
}

  Department 类定义了一个抽象方法 employees ,用于返回当前部门的职工数,然后再两个部门类 SalesDepartment 、 TechnologyDepartment 实现了 employees 方法。现在我们可以定义一个单独的类来合并职工数:

class Company {
    private $_departments = array();

    public function addDepartment(Department $department)
    {
        array_push($this->_departments, $department);
    }

    public function employees()
    {
        $tem = 0;
        foreach ($this->_departments as $department) {
            $tem += $department->employees();
        }
        return $tem;
    }
}

 Company 有两 个方法 addDepartment 、 employees 。 addDepartment 方法用于接受 Department 对象并保存到 $_departments 中。 employees 方法通过一个简单的迭代遍历所聚合的 Department 对象并调用 employees 方法,计算出总的职工数。

当前模型对现有需求还是可以很好的满足的,但是如果这个公司又成立了分公司,需要将分公司的职工数也统计进来,并且还能够拆离出来又怎样呢?

我们修改 Company 类,使之可以像添加 Department 对象一样添加 Company 对象:

class Company {
    private $_company = array();
    private $_departments = array();

    public function addCompany(Company $company)
    {
        array_push($this->_company, $company);
    }

    public function addDepartment(Department $department)
    {
        array_push($this->_departments, $department);
    }

    public function employees()
    {
        $tem = 0;
        foreach ($this->_company as $company) {
            $tem += $company->employees();
        }
        foreach ($this->_departments as $department) {
            $tem += $department->employees();
        }
        return $tem;
    }
}

现在来看这个类还算不太复杂,但是随着需求的增加,这个类所提供的功能也会越来越多。我们回过头来看上面的这几个类,都需要有 employees 方法,无论是 SalesDepartment 、 TechnologyDepartment 还是 Company 它们所提供的功能是相同的,统计职工数。这些相似性给我们带来一个必然的结论:因为容器对象与它们包含的对象共享同一个接口,所以它们应该共享同一个类型家族。

实现

abstract class Unit
{
    abstract function addUnit(Unit $unit);
    abstract function removeUnit(Unit $unit);
    abstract function employees();
}
class Company extends Unit{
    private $_units = array();

    public function addUnit(Unit $unit)
    {
        array_push($this->_units, $unit);
    }

    public function removeUnit(Unit $unit)
    {
        $this->_units = array_udiff($this->_units, array($unit), function ($a, $b){
            return ($a === $b) ? 0 : 1;
        });
    }

    public function employees()
    {
        $tem = 0;
        foreach ($this->_units as $unit) {
            $tem += $unit->employees();
        }
        return $tem;
    }
}

 Company 可以保存任何类型的 Unit 对象,包括它自己本身。这样保证了每个对象都支持 Unit 定义的方法。先看下调用:

$main_army = new Company();
$main_army->addUnit(new SalesDepartment());
$main_army->addUnit(new TechnologyDepartment());

$sub_army = new Company();
$sub_army->addUnit(new SalesDepartment());
$sub_army->addUnit(new SalesDepartment());
$sub_army->addUnit(new SalesDepartment());

$main_army->addUnit($sub_army);

$main_army->employees();

可以看到我们只需简单的操作就能计算出总的职工数了,组合模式将计算复杂性完全隐藏了。并且符合组合模式的原则:局部类和组合类具有相同的接口。

优化

我们发现,由于 SalesDepartment 和 TechnologyDepartment 并不需要 addUnit 和 removeUnit 方法,但是 Unit 将两个方法定义为抽象方法,则子类必须实现,这就导致了代码冗余。所以,需要解决此问题:

定义默认方法

 Unit 中定义默认方法,这样在子类中不必要实现 addUnit 和 removeUnit 方法。

abstract class Unit
{
    abstract function employees();
    public function addUnit(Unit $unit)
    {
        throw new UnitException('error msg');
    }
    public function removeUnit(Unit $unit)
    {
        throw new UnitException('error msg');
    }
}

虽然定义了默认方法,非法调用就会抛出异常,但是这么处理未必就是好的,我们仍然不知道调用 Unit 对象的 addUnit 方法是否是安全的。因此,采用这种处理方式就要权衡下了。

优化模式结构

虽然可以将添加和删除方法放到局部类中定义,但是这么处理就需要在每个局部类中分开定义,没有统一约束。所以,将基类 Unit 分解为 CompositeUnit 子类型,然后由局部类来继承。

abstract class Unit
{
    abstract function employees();
    public function getComposite()
    {
        return null;
    }
}
abstract class CompositeUnit extends Unit
{
    private $_units = array();

    public function addUnit(Unit $unit)
    {
        if (in_array($unit, $this->_units, true)) {
            return;
        }
        $this->_units[] = $unit;
    }

    public function removeUnit(Unit $unit)
    {
        $this->_units = array_udiff($this->_units, array($unit), function ($a, $b){
            return ($a === $b) ? 0 : 1;
        });
    }

    public function getComposite()
    {
        return $this;
    }
}

可以发现新增了个 getComposite 方法, getComposite 方法是用来区分能否调用 addUnit 、 removeUnit 方法的,如果 getComposite 返回不是NULL就说明方法可调用。

这样看起来结构更清晰了,这种结构还可以再优化,这就需要在项目中慢慢调整了。最切合需求的才是最好的!

总结

组合模式能使原本复杂的计算简单化,在适合的业务需求中使用会带来很多益处:

  • 灵活:因为组合模式中的一切类都共享了同一个父类型,所以可以轻松地在设计中添加新的组合对象或者局部对象,而无需大范围地修改代码。
  • 简单:使用组合结构的客户端代码只需设计简单的接口。客户端代码没有必要区分一个对象是组合对象还是局部对象(除了添加新组件时)。
  • 隐式到达:组合模式中的对象通过树型结构组织。每个组合对象中都保存着对子对象的引用。因此对树中某部分的一个小操作可能会产生很大的影响。
  • 显式到达:树型结构可轻松遍历。可以通过迭代树型结构来获取组合对象和局部对象的信息,或对组合对象和局部对象执行批量处理。

但另一方面,组合模式又依赖于其组成部分的简单性。随着我们引入复杂的规则,代码会变得越来越难以维护。

后记

设计模式真是看似简单,其实复杂的东西,理解实现原理很容易,但是想要活用,就要理解模式的思想了。本文部分摘录自《深入PHP 面向对象、模式与实践》,书很好,我看的倒是挺慢的,有兴趣的也可以读读。最后吐槽下用 Visio 画UML图真不太方便,还是用回Astah Professional吧ಠ╭╮ಠ。感谢阅读,再会!!!

  

posted @ 2018-05-16 14:52  BNDong  阅读(1680)  评论(1编辑  收藏  举报