1.定义:
  • 将对象组合成树型结构,以表示 ”部分-整体“ 的层次结构

    • 部分:各个叶子结点

    • 整体:中间结点或者根结点

  • 组合模式让客户端可以一致地对待单个对象和对象的组合

  • 实例:

    • 1+(2×3)+(4×(5+6))

    • 可以对单独的叶子结点调用GetValue,1返回1

    • 可以对一个整体调用GetValue,对(2×3)调用GetValue,返回6

    • 客户端可以将 任何一个数字,或整个表达式,都当作一个表达式来处理。如果我们考虑一个实际需要处理数学表达式的系统,那么这种方式会简化行为,简化与数学表达式的交互。你可以直接对任何数学表达式调用GetValue(),而无需关心它是否需要递归的在所有子节点上调用GetValue()。

  • 属于结构型设计模式

    • 客户端可以一致地看待单个对象和组合对象,从客户端的视角来看,两者之间没有区别。

    • 我们讨论的是,如何通过组合关系,将对象彼此结构化,从而简化客户端的整体接口,因此这属于结构型设计模式的范畴。

2.类图

  • Client客户端:使用组合设计模式的代码

  • 组件Component:组件既可以是部分,也可以是整体,它既可以是叶子节点,也可以是根节点,或者只是层次结构中的某个中间节点。它包含客户端需要,并与之交互的某个方法或某个属性

  • 叶子Leaf:

    • 组合可以添加删除组件,而如果你定义了一个叶子,无法想其中添加更多组件

  • 组合Composite:

    • 组合拥有多个组件,它里面有一个组件列表List<Component>

    • 因为组合里面有这个组件列表,所以应该有组件列表对应的添加,删除方法

  • 客户端再编译时,只是使用组件,变调用它所关心的,需要交互的Operation()方法;而在幕后,实际的具体实现,可能是叶子,也可能是组合

    • 叶子和组合的底层实现很可能不同,叶子可能直接返回一个值,而组合则可能需要和它的内部子节点交互,进行计算,最终返回结果

3.代码实现
//组件,抽象基类
public abstract class Component
{
    //具体类将实现这个方法
    public abstract void Operation();
    
    //定义为virtual,提供默认实现,这样叶子类就不必实现这些方法
    public virtual void Add(Component component) { }
    public virtual void Remove(Component component) { }
    public virtual Component? GetChild(int index)
    { 
        return null;
    }
}
//叶子节点
public class Leaf : Component
{
    public override void Operation()
    {
        //叶子结点 操作逻辑
    }
}
//组合节点
public class Composite : Component
{
    //存放组件列表
    private readonly List<Component> _children = [];
    
    public override void Operation()
    {
        //对每个子节点调用Operation,当我们的子节点也是一个组件时,继续递归调用
        foreach (var child in _children)
        { 
            child.Operation();
        }
    }
    
    public override void Add(Component component)
    {
        _children.Add(component);
    }
    public override void Remove(Component component)
    {
        _children.Remove(component);
    }
    public override Component? GetChild(int index)
    {
        //索引有效
        if(index < 0 || index >= _children.Count) return null;
​
        return _children[index];
    }
}
///客户端调用
Component root = new Composite();
Component leafA = new Leaf();
Component childComposite = new Composite();
root.Add(leafA);
root.Add(childComposite);
​
Component leafB = new Leaf();
Component leafC = new Leaf();
childComposite.Add(leafB);
childComposite.Add(leafC);
​
root.Operation();

4.课程购买示例
  • 假设我们在浏览一个课程购物网站,我们需要点击一个课程,得到它的价格和时长;我们也可以点击一个课程捆绑包,获得它的价格和时长。对于用户来说,我点击课程,或者点击课程捆绑包,对用户的感觉是一样的。

代码示例:
/// <summary>
/// 组合模式的抽象基类
/// </summary>
public abstract class LearningResource
{
    public abstract string GetName();
    public abstract decimal GetPrice();
    public abstract TimeSpan GetDuration();
    public virtual void Add(LearningResource learningResource) { }
    public virtual void Remove(LearningResource learningResource) { }
    public virtual LearningResource? GetLearningResource(string name)
    { 
        return null;
    }
}
/// <summary>
/// 叶子结点 单个课程
/// </summary>
public class Course(string name,TimeSpan duration,decimal price) : LearningResource
{
    public override TimeSpan GetDuration()
    {
        return duration;
    }
​
    public override string GetName()
    {
        return name;
    }
​
    public override decimal GetPrice()
    {
        return price;
    }
}
/// <summary>
/// 组合结点 捆绑包
/// </summary>
public class Bundle(string name) : LearningResource
{
    private readonly List<LearningResource> _children = [];
​
    public override string GetName()
    {
        return name;
    }
​
    public override decimal GetPrice()
    {
        return _children.Sum(x => x.GetPrice()) * 0.8m;
    }
​
    public override TimeSpan GetDuration()
    {
        return new(_children.Sum(x => x.GetDuration().Ticks));
    }
​
    public override void Add(LearningResource learningResource)
    {
        _children.Add(learningResource);
    }
    public override void Remove(LearningResource learningResource)
    {
        _children.Remove(learningResource);
    }
    public override LearningResource? GetLearningResource(string name)
    {
        return _children.SingleOrDefault(x => x.GetName() == name);
    }
}
///客户端
LearningResource root = new Bundle(name: "从零到英雄,整洁架构");
​
LearningResource leafA = new Course(
    name: "整洁架构入门",
    duration: TimeSpan.FromHours(3),
    price: 100);
​
LearningResource leafB = new Course(
    name: "深入架构入门",
    duration: TimeSpan.FromHours(4),
    price: 110);
root.Add(leafA);
root.Add(leafB);
Console.WriteLine(root.GetPrice().ToString());
5.优缺点:
  • 优点:

    1. 极大地简化了客户端的工作:客户端拥有一个统一的接口,用于交互,从客户端的视角来看,它可以放心地对任何学习资源调用GetPrice(),GetDruation(),GetName()方法。它知道如何与这个学习资源进行交互,而无需关心幕后是否存在一棵包含大量复杂逻辑的巨大树形结构。

    2. 提供了极大的灵活性:组合对象是可变的,整个结构非常灵活,我们可以构建出任意的树形结构。当我们对树形结构进行添加分支时,客户端仍然可以继续调用相同的交互方法;无需修改任何客户端代码

  • 缺点:

    1. 基类需要由两种不同的具体类来实现,可能会导致接口过度泛化:我们可能会在组件(抽象基类)中,不断添加越来越多的方法,这些方法有时候只适用于某一种组合对象有时候只适用于某一种叶子节点。因为我们有一个统一的接口,供客户端交互,而其背后可能有多种不同类型的实现,这就使得限制组合对象的组成部分变得困难。这让我们为基类找到合适的接口,合适的方法,合适的方法命名,以及找到对组合和叶节点都有意义的方法变得棘手

    2. 可能最终不得不在代码中进行显示类型转换,根据组合对象,或者叶节点的具体类型,来访问所需的方法。在组件的具体实现中,类型检查和类型转换可能变得必不可少,会增加整体的复杂性

    3. 不能阻止同一叶节点添加到多个组合对象中,或者将同一个组合对象添加到多个其他组合对象中。我们对如何将多种组件组合在一起没有任何限制。

组合设计模式在Nodify中的使用

将对象组合成树形结构,以表示 ”整体-部分“ 的层次结构,组合模式可以让客户端可以一致的对待 单个对象 和 对象的组合

  • 组件:IInputHandler接口

    • public interface IInputHandler
      {
          //触发方法
          void HandleEvent(InputEventArgs e);
          //设置为true,表示鼠标一直按下,在拖拽,框选中有用
          bool RequiresInputCapture { get; }
          //设置为ture,无视e.handle,一直触发
          bool ProcessHandledEvents { get; }
      }
  • 组合(复合体):Shared<TElement>

    • Shared<TElement>本身也是一个IInputHandler,它的HandleEvent会递归调用所有存储在组件列表_handlers中的IInputHandler的HandleEvent方法。

      • 组件列表_handlers,用于存放具体的输入行为

      • AddHandler,RemoveHandlers,Clear,对列表的增删操作

    • public partial class InputProcessor
      {
          private readonly List<IInputHandler> _handlers = new List<IInputHandler>();
      ​
          public bool RequiresInputCapture { get; private set; }
      ​
          public void AddHandler(IInputHandler handler)
              => _handlers.Add(handler);
      ​
          public void RemoveHandlers<T>() where T : IInputHandler
              => _handlers.RemoveAll(x => x.GetType() == typeof(T));
      ​
          public void Clear()
              => _handlers.Clear();
      ​
          public void ProcessEvent(InputEventArgs e)
          {
              RequiresInputCapture = false;
      ​
              for (int i = 0; i < _handlers.Count; i++) 
              {
                  IInputHandler handler = _handlers[i];
                  if (!e.Handled || handler.ProcessHandledEvents)
                  {
                      handler.HandleEvent(e);
                      RequiresInputCapture |= handler.RequiresInputCapture;
                  }
              }
          }
      }
      ​
      public sealed class Shared<TElement> : InputProcessor, IInputHandler
          where TElement : FrameworkElement
      {
          private static readonly List<KeyValuePair<Type, Func<TElement, IInputHandler>>> _handlerFactories = new List<KeyValuePair<Type, Func<TElement, IInputHandler>>>();
      ​
          bool IInputHandler.ProcessHandledEvents { get; }
      ​
          public Shared(TElement element)
          {
              foreach (var kvp in _handlerFactories)
              {
                  AddHandler(kvp.Value(element));
              }
          }
      ​
          static Shared()
          {
              if (typeof(TElement) == typeof(NodifyEditor))
              {
                  EditorState.RegisterDefaultHandlers();
              }
              else if (typeof(TElement) == typeof(ItemContainer))
              {
                  ContainerState.RegisterDefaultHandlers();
              }
              else if (typeof(TElement) == typeof(Connector))
              {
                  ConnectorState.RegisterDefaultHandlers();
              }
              else if (typeof(TElement) == typeof(Minimap))
              {
                  MinimapState.RegisterDefaultHandlers();
              }
              else if (typeof(TElement) == typeof(BaseConnection))
              {
                  ConnectionState.RegisterDefaultHandlers();
              }
          }
      ​
          public void HandleEvent(InputEventArgs e)
              => ProcessEvent(e);
      ​
          public static void RegisterHandlerFactory<THandler>(Func<TElement, THandler> factory)
              where THandler : IInputHandler
          {
              if (_handlerFactories.Any(x => x.Key == typeof(THandler)))
              {
                  throw new InvalidOperationException($"An input handler of type '{typeof(THandler)}' has already been registered.");
              }
      ​
              _handlerFactories.Add(new KeyValuePair<Type, Func<TElement, IInputHandler>>(typeof(THandler), elem => factory(elem)));
          }
      ​
          public static void RemoveHandlerFactory<THandler>()
              => _handlerFactories.RemoveAll(x => x.Key == typeof(THandler));
      ​
          public static void ReplaceHandlerFactory<THandler>(Func<TElement, THandler> factory)
              where THandler : IInputHandler
          {
              int index = _handlerFactories.FindIndex(x => x.Key == typeof(THandler));
              _handlerFactories[index] = new KeyValuePair<Type, Func<TElement, IInputHandler>>(typeof(THandler), elem => factory(elem));
          }
      ​
          public static void ClearHandlerFactories()
              => _handlerFactories.Clear();
      }
      ​
      public static class InputProcessorExtensions
      {
          public static void AddSharedHandlers<TElement>(this InputProcessor inputProcessor, TElement instance)
              where TElement : FrameworkElement
          {
              inputProcessor.AddHandler(new InputProcessor.Shared<TElement>(instance));
          }
      }
    • 整体流程:

      • 客户端:NodifyEditor中持有一个InputProcessor对象,在NodifyEditor的构造函数中,通过InputProcessor的扩展方法,注册一个InputProcessor.Shared<NodifyEditor>(NodifyEditor对象)。目的:客户端NodifyEditor持有一个根节点,根节点是一个封闭类InputProcessor.Shared<NodifyEditor>

      • 客户端调用方法:

        • protected override void OnMouseDown(MouseButtonEventArgs e)
          {
              MouseLocation = e.GetPosition(ItemsHost);
              InputProcessor.ProcessEvent(e);
          }
        • 当接收到WPF的鼠标事件时,触发了InputProcessor.ProcessEvent(e);方法,此时根节点的组件列表_handlers中,只有一个根节点:InputProcessor.Shared<NodifyEditor>,触发该节点的HandleEvent方法,即触发InputProcessor.Shared<NodifyEditor>的ProcessEvent方法。

        • 此时InputProcessor.Shared<NodifyEditor>的组件列表_handlers中,有EditorState.RegisterDefaultHandlers();注册的一组叶子节点,然后循环触发这些叶子节点的HandleEvent(e)方法。

      • 组合节点:InputProcessor.Shared<TElement>

      • 叶子节点:实现InputElementState<TElement>的具体子类:例如Panning,Selecting,Zooming,Cutting具体类。

    • 使用组合设计模式的意义:

      • 1.统一接口,对客户端友好:在Nodify中,有多种FrameworkElement,需要不同的交互行为,例如图表编辑器的交互行为有:平移,缩放,裁剪,框选等。里面的ItemsContainer结点容器的交互行为有:抓取,里面的连接点的交互行为有:连接,断开连接。当用户触发一个MouseDown事件时,我们引发对应的InputProcessor.ProcessEvent(e);,我们根据引发的元素,触发该元素定义的一组交互行为。对我们不同的类,例如NodifyEditor类,还是ItemsContainer类来说,它们都只需要执行触发操作:InputProcessor.ProcessEvent(e);

      • 2.灵活定制控件的行为,易于扩展:如果我们需要定义一个新的控件,这个控件的行为:点击一下沿着点击点,进行原点对称变换。我们会:1.定义一个叶子结点:原点对称变换的交互行为,定义ZeroTransform类,实现IInputHandler接口,这个表示一个叶子。2.定义一个组合结点,我们直接在泛型类中定义好了,InputProcessor.Shared<TElement>,我们只需要在它的静态构造函数中注册:InputProcessor.Shared<ZeroTransformElement>.RegisterHandlerFactory(elem => new ZeroTransform(elem));只需要定义叶子节点,具体的变换行为,就可以完成交互行为

更多推荐