设计模式(8) 组合模式
- 组合模式
- 透明模式与安全模式
- 对组合的筛选遍历
无论是在生活中还是项目中,我们经常会遇到具有“部分-整体”概念的对象,比如员工与团队的关系,这就类似树形结构,可能具有很多的嵌套层次和分支,把这种复杂性直接暴露给调用端是不合适的。
组合模式
借助组合模式,可以将这类具有“部分-整体”的对象组合成树形的层次结构,并使得用户可以对单个对象和组合对象采用相同的使用方式。
GOF对组合模式的描述为:
Compose objects into tree structures to represent part-whole hierarchies.
Compositelets clients treat individual objects and compositions of objects uniformly.
— Design Patterns : Elements of Reusable Object-Oriented Software
UML类图:
组合模式包含三个角色:
- Leaf:叶子节点,代表单个个体,它没有子节点。
- Composite:组合节点,既可以包含叶子节点,也可以包含其他的组合节点,
- Component:抽象构件,定义Leaf和Composite共有的方法和属性,可以定义一些默认的行为或属性。
透明模式与安全模式
在使用组合模式时,根据抽象构件类的定义形式,可将组合模式分为透明模式和安全组合两种形式。
透明模式
透明模式中,抽象构件Component中声明了所有用于管理成员对象的方法,包括add()、remove()以及getChildren()等方法,这样做的好处是确保所有的构件类都有相同的接口。在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以相同地对待所有的对象。透明组合模式也是组合模式的标准形式,前面的类图表示的就是透明模式。
透明模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的。叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供add()、remove()以及getChildren()等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法就会导致异常。
透明模式的实现代码如下:
public abstract class Component
{
protected IList<Component> children;
public virtual string Name { get; set; }
public virtual void Add(Component child)
{
children.Add(child);
}
public virtual void Remove(Component child)
{
children.Remove(child);
}
public virtual Component this[int index]
{
get { return children[index]; }
}
}
public class Leaf : Component
{
public override void Add(Component child)
{
throw new NotSupportedException();
}
public override void Remove(Component child)
{
throw new NotSupportedException();
}
public override Component this[int index] => throw new NotSupportedException();
}
public class Composite : Component
{
public Composite()
{
base.children = new List<Component>();
}
}
安全模式
安全模式则是将管理成员对象的方法从抽象构件Component转移到了Composite,在抽象构件Component中没有声明任何用于管理成员对象的方法,这样可以保证安全,叶子对象中无法调用到那些管理成员对象的方法。
安全模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
对组合的筛选遍历
将对象组合成树形结构后,要使用这些对象,就需要用遍历树形结构的方式来获取这些对象。
比如对于上面代码中的Component,如果需要获取全部结点的Names属性
实现代码可以为:
public List<string> names = new List<string>();
public virtual IEnumerable<string> GetNameList()
{
GetNameList(names);
return names;
}
private virtual void GetNameList(List<string> names)
{
names.Add(this.Name);
if (children != null && children.Count > 0)
{
foreach (Component child in children)
{
child.GetNameList(names);
}
}
}
但有的时候往往会遇到一些定制化的遍历需求,比如只获取Leaf结点(仅列出一个所有员工的名单),只获取Composite结点(仅列出所有部门领导的信息)等等,对于这些需求如果一一实现比较麻烦,且需要频繁变化,可以采用一种更通用的方式,类似Linq中Where筛选那样,调用的同时把筛选条件也传入。
对GetNameList方法的扩展:
public virtual IEnumerable<string> GetNameList(Func<Component, bool> isMatchFunc)
{
GetNameList(names, isMatchFunc);
return names;
}
public virtual void GetNameList(List<string> names, Func<Component, bool> isMatchFunc)
{
if (isMatchFunc == null || isMatchFunc(this))
{
names.Add(this.Name);
}
if (children != null && children.Count > 0)
{
foreach (Component child in children)
{
child.GetNameList(names, isMatchFunc);
}
}
}
参考书籍:
王翔著 《设计模式——基于C#的工程化实现及扩展》