乐趣区

关于设计模式:12-设计模式组合模式

定义

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

示例

如下图所示,就是日常工作中一个很常见的树形构造的例子:

对于这种数据,咱们通常会以相似如下二维关系表的模式存储在数据库中,他们之间的树形构造关系由主外键放弃:

Id Name ParentId
1 音乐 0
2 常识 0
3 生存 0
4 迷信科普 2
5 社科人文 2

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

UML 类图

咱们先看看组合模式的类图:

  • 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 等。

优缺点

长处

  • 客户端调用简略,能够像解决简略元素一样来解决简单元素, 从而使得客户程序与简单元素的内部结构解耦。
  • 能够不便的在构造中减少或者移除对象。

毛病

客户端须要花更多工夫理清类之间的档次关系。这个通过下面客户端的调用代码也能够看得出来,然而,任何设计都是在各种利弊之间做出衡量,例如,咱们都晓得通过二叉树的二分查找能够放慢查问速度,然而,它的前提是必须先构建二叉树并且排好序。这里也是一样的,为了前期使用方便,后期结构的麻烦也是在劫难逃的。

总结

组合模式实用于解决树形构造关系的场景,因而很好辨认,然而并非所有树形构造呈现的场合都能够应用组合模式,例如,咱们在写业务接口的时候就大量存在树形构造的关系,但我置信简直不会有人应用组合模式来组织这种关系而后再返回给客户端,而是间接采纳主外键的形式组织,这是因为这种场合组合模式就曾经不实用了。组合模式通常还是更实用于人机交互的场景,例如页面布局控件中。

源码链接
更多内容,欢送关注公众号:

退出移动版