C#组合模式
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.优缺点:
-
优点:
-
极大地简化了客户端的工作:客户端拥有一个统一的接口,用于交互,从客户端的视角来看,它可以放心地对任何学习资源调用GetPrice(),GetDruation(),GetName()方法。它知道如何与这个学习资源进行交互,而无需关心幕后是否存在一棵包含大量复杂逻辑的巨大树形结构。
-
提供了极大的灵活性:组合对象是可变的,整个结构非常灵活,我们可以构建出任意的树形结构。当我们对树形结构进行添加分支时,客户端仍然可以继续调用相同的交互方法;无需修改任何客户端代码。
-
-
缺点:
-
基类需要由两种不同的具体类来实现,可能会导致接口过度泛化:我们可能会在组件(抽象基类)中,不断添加越来越多的方法,这些方法有时候只适用于某一种组合对象,有时候只适用于某一种叶子节点。因为我们有一个统一的接口,供客户端交互,而其背后可能有多种不同类型的实现,这就使得限制组合对象的组成部分变得困难。这让我们为基类找到合适的接口,合适的方法,合适的方法命名,以及找到对组合和叶节点都有意义的方法变得棘手。
-
可能最终不得不在代码中进行显示类型转换,根据组合对象,或者叶节点的具体类型,来访问所需的方法。在组件的具体实现中,类型检查和类型转换可能变得必不可少,会增加整体的复杂性
-
不能阻止同一叶节点添加到多个组合对象中,或者将同一个组合对象添加到多个其他组合对象中。我们对如何将多种组件组合在一起没有任何限制。
-
组合设计模式在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));只需要定义叶子节点,具体的变换行为,就可以完成交互行为
-
-
更多推荐
所有评论(0)