定义

将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得对单个对象和组合对象的使用具有一致性。

示例

如下图所示,就是日常工作中一个很常见的树形结构的例子:
设计模式-组合模式-LMLPHP

对于这种数据,我们通常会以类似如下二维关系表的形式存储在数据库中,他们之间的树形结构关系由主外键保持:

但是在界面渲染的时候,这种自依赖的二维表结构就显得不那么人性化了,而组合模式主要就是为了将这种数据以树形结构展示给客户端,并且使得客户端对每一个节点的操作都是一样的简单。

UML类图

我们先看看组合模式的类图:
设计模式-组合模式-LMLPHP

  • Component:组合中的对象声明接口,并实现所有类共有接口的默认行为。
  • Leaf:叶子结点,没有子结点。
  • Composite:枝干节点,用来存储管理子节点,如增加和删除等。

从类图上可以看出,它其实就是一个普通的树的数据结构,封装的是对树节点的增删改查操作,因此,组合模式也是一种数据结构模式。

代码实现

组合模式理解起来比较简单,我们直接看看代码如何实现。

透明模式

public abstract class Component
{
    public string Name { get; set; }

    public Component(string name)
    {
        this.Name = name;
    }

    public abstract int SumArticleCount();

    public abstract void Add(Component component);
    public abstract void Remove(Component component);

    public abstract void Display(int depth);
}

public class Composite : Component
{
    private List<Component> _components = new List<Component>();

    public Composite(string name):base(name)
    {

    }
    public override void Add(Component component)
    {
        _components.Add(component);
    }

    public override void Remove(Component component)
    {
        _components.Remove(component);
    }

    public override void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + Name);
        foreach (Component component in _components)
        {
            component.Display(depth + 1);
        }
    }

    public override int SumArticleCount()
    {
        int count = 0;
        foreach (var item in _components)
        {
            count += item.SumArticleCount();
        }
        return count;
    }
}

public class Leaf : Component
{
    public Leaf(string name) : base(name)
    {

    }

    public override void Add(Component component)
    {
        throw new InvalidOperationException("叶子节点不能添加元素");
    }

    public override void Remove(Component component)
    {
        throw new InvalidOperationException("叶子节点不能删除元素");
    }

    public override int SumArticleCount()
    {
        return 1;
    }

    public override void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + Name);
    }
}

值得注意的是,由于Leaf也继承了Component,因此必须实现父类中的所有抽象方法,包括Add()Remove(),但是我们知道,叶子节点是不应该有这两个方法的,因此,只能给出一个空实现,或者抛出一个非法操作的异常(建议抛出异常,这样可以明确的告诉调用者不能使用,空实现会对调用者造成困扰)。对于其他业务方法,叶子节点直接返回当前叶子的信息,而枝干节点采用递归的方式管理所有节点(其实组合模式的核心思想就是树形结构+递归)。由于叶子节点和枝干节点是继承了父类完全相同的结构,因此,客户端对整个树形结构的所有节点具有一致的操作,不用关心具体操作的是叶子节点还是枝干节点,因此,这种模式被叫做透明模式。客户端调用代码如下:

static void Main(string[] args)
{
    Component root = new Composite("目录");

    Component music = new Composite("音乐");
    Component knowledge = new Composite("知识");
    Component life = new Composite("生活");
    root.Add(music);
    root.Add(knowledge);
    root.Add(life);

    Component science = new Composite("科学科普");
    Component tech = new Composite("野生技术协会");
    knowledge.Add(science);
    knowledge.Add(tech);

    Component scienceArticle1 = new Leaf("科学科普文章1");
    Component scienceArticle2 = new Leaf("科学科普文章2");
    science.Add(scienceArticle1);
    science.Add(scienceArticle2);

    Component shoot = new Composite("摄影");
    Component program = new Composite("编程");
    Component english = new Composite("英语");
    tech.Add(shoot);
    tech.Add(program);
    tech.Add(english);

    Component shootArticle1 = new Leaf("摄影文章1");
    Component lifeArticle1 = new Leaf("生活文章1");
    Component lifeArticle2 = new Leaf("生活文章2");
    shoot.Add(shootArticle1);
    life.Add(lifeArticle1);
    life.Add(lifeArticle2);

    tech.Remove(program);
    knowledge.Display(0);
    Console.WriteLine("文章数:"+ knowledge.SumArticleCount());
}

透明模式是把组合使用的方法放到抽象类中,使得叶子对象和枝干对象具有相同的结构,客户端调用时具备完全一致的行为接口。但因为Leaf类本身不具备Add()Remove()方法的功能,所以实现它是没有意义的,违背了单一职责原则和里氏替换原则。

安全模式

基于上面的问题,我们可以对实现进行改造,代码如下:

public abstract class Component
{
    public string Name { get; set; }

    public Component(string name)
    {
        this.Name = name;
    }

    public abstract int SumArticleCount();

    public abstract void Display(int depth);
}

public class Composite : Component
{
    private List<Component> _components = new List<Component>();

    public Composite(string name):base(name)
    {

    }
    public void Add(Component component)
    {
        _components.Add(component);
    }

    public void Remove(Component component)
    {
        _components.Remove(component);
    }

    public override void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + Name);
        foreach (Component component in _components)
        {
            component.Display(depth + 1);
        }
    }

    public override int SumArticleCount()
    {
        int count = 0;
        foreach (var item in _components)
        {
            count += item.SumArticleCount();
        }
        return count;
    }
}

public class Leaf : Component
{
    public Leaf(string name) : base(name)
    {

    }

    public override int SumArticleCount()
    {
        return 1;
    }

    public override void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + Name);
    }
}

我们去掉了父类中抽象的Add()Remove()方法,让其独立的被Composite控制,这样Leaf中就不需要实现无意义的Add()Remove()方法了,使得对叶子节点的操作更加安全(不存在无意义的方法),因此,这种模式也叫安全模式。

安全模式是把枝干和叶子节点区分开来,枝干单独拥有用来组合的方法,这种方法比较安全。但枝干和叶子节点不具有相同的接口,客户端的调用需要做相应的判断,违背了依赖倒置原则。

由于这两种模式各有优缺点,因此,无法断定哪一种更优,选用哪一种方式还得取决于具体的需求。不过个人还是比较倾向于透明模式,因为这种模式,客户端的调用更容易,况且,在软件开发过程中,叶子也并没有那么容易识别,叶子不一定永远都是叶子,例如,我们以为文章就是叶子,殊不知,当需求发生变化时,文章下面还可能有章节,这时透明模式也不失为一种预留扩展的手段。

应用实例

在实际工作中,这种树形结构也是非常多见的,其中或多或少都体现了组合模式的思想,例如,文件系统中的文件与文件夹、Winform中的简单控件与容器控件、XML中的Node和Element等。

优缺点

优点

  • 客户端调用简单,可以像处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
  • 可以方便的在结构中增加或者移除对象。

缺点

客户端需要花更多时间理清类之间的层次关系。这个通过上面客户端的调用代码也可以看得出来,但是,任何设计都是在各种利弊之间做出权衡,例如,我们都知道通过二叉树的二分查找可以加快查询速度,但是,它的前提是必须先构建二叉树并且排好序。这里也是一样的,为了后期使用方便,前期构造的麻烦也是在所难免的。

总结

组合模式适用于处理树形结构关系的场景,因此很好识别,但是并非所有树形结构出现的场合都可以使用组合模式,例如,我们在写业务接口的时候就大量存在树形结构的关系,但我相信几乎不会有人使用组合模式来组织这种关系然后再返回给客户端,而是直接采用主外键的方式组织,这是因为这种场合组合模式就已经不适用了。组合模式通常还是更适用于人机交互的场景,例如页面布局控件中。

源码链接

09-05 04:33