使用C# (.NET Core) 实现组合设计模式 (Composite Pattern)
本文的概念性内容来自深入浅出设计模式一书.
本文需结合上一篇文章(使用C# (.NET Core) 实现迭代器设计模式)一起看.
上一篇文章我们研究了多个菜单一起使用的问题.
需求变更
就当我们感觉我们的设计已经足够好的时候, 新的需求来了, 我们不仅要支持多种菜单, 还要支持菜单下可以拥有子菜单.
例如我想在DinerMenu下添加一个甜点子菜单(dessert menu). 以我们目前的设计, 貌似无法实现该需求.
目前我们无法把dessertmenu放到MenuItem的数组里.
我们应该怎么做?
- 我们需要一种类似树形的结构, 让其可以容纳/适应菜单, 子菜单以及菜单项.
- 我们还需要维护一种可以在该结构下遍历所有菜单的方法, 要和使用遍历器一样简单.
- 遍历条目的方法需要更灵活, 例如, 我可能只遍历DinerMenu下的甜点菜单(dessert menu), 或者遍历整个Diner Menu, 包括甜点菜单.
组合模式定义
组合模式允许你把对象们组合成树形的结构, 从而来表示整体的层次. 通过组合, 客户可以对单个对象或对象们的组合进行一致的处理.
先看一下树形的结构, 拥有子元素的元素叫做节点(node), 没有子元素的元素叫做叶子(leaf).
针对我们的需求:
菜单Menu就是节点, 菜单项MenuItem就是叶子.
针对需求我们可以创建出一种树形结构, 它可以把嵌套的菜单或菜单项在相同的结构下进行处理.
组合和单个对象是指什么呢?
如果我们拥有一个树形结构的菜单, 子菜单, 或者子菜单和菜单项一起, 那么就可以说任何一个菜单都是一个组合, 因为它可以包含其它菜单或菜单项.
而单独的对象就是菜单项, 它们不包含其它对象.
使用组合模式, 我们可以把相同的操作作用于组合或者单个对象上. 也就是说, 大多数情况下我们可以忽略对象们的组合与单个对象之间的差别.
该模式的类图:
客户Client, 使用Component来操作组合中的对象.
Component定义了所有对象的接口, 包括组合节点与叶子. Component接口也可能实现了一些默认的操作, 这里就是add, remove, getChild.
叶子Leaf会继承Component的默认操作, 但是有些操作也许并不适合叶子, 这个过会再说.
叶子Leaf没有子节点.
组合Composite需要为拥有子节点的组件定义行为. 同样还实现了叶子相关的操作, 其中有些操作可能不适合组合, 这种情况下异常可能会发生.
使用组合模式来设计菜单
首先, 需要创建一个component接口, 它作为菜单和菜单项的共同接口, 这样就可以在菜单或菜单项上调用同样的方法了.
由于菜单和菜单项必须实现同一个接口, 但是毕竟它们的角色还是不同的, 所以并不是每一个接口里(抽象类里)的默认实现方法对它们都有意义. 针对毫无意义的默认方法, 有时最好的办法是抛出一个运行时异常. 例如(NotSupportedException, C#).
MenuComponent:
using System; namespace CompositePattern.Abstractions
{
public abstract class MenuComponent
{
public virtual void Add(MenuComponent menuComponent)
{
throw new NotSupportedException();
} public virtual void Remove(MenuComponent menuComponent)
{
throw new NotSupportedException();
} public virtual MenuComponent GetChild(int i)
{
throw new NotSupportedException();
} public virtual string Name => throw new NotSupportedException();
public virtual string Description => throw new NotSupportedException();
public virtual double Price => throw new NotSupportedException();
public virtual bool IsVegetarian => throw new NotSupportedException(); public virtual void Print()
{
throw new NotSupportedException();
}
}
}
MenuItem:
using System;
using CompositePattern.Abstractions; namespace CompositePattern.Menus
{
public class MenuItem : MenuComponent
{
public MenuItem(string name, string description, double price, bool isVegetarian)
{
Name = name;
Description = description;
Price = price;
IsVegetarian = isVegetarian;
} public override string Name { get; }
public override string Description { get; }
public override double Price { get; }
public override bool IsVegetarian { get; } public override void Print()
{
Console.Write($"\t{Name}");
if (IsVegetarian)
{
Console.Write("(v)");
} Console.WriteLine($", {Price}");
Console.WriteLine($"\t\t -- {Description}");
}
}
}
Menu:
using System;
using System.Collections.Generic;
using CompositePattern.Abstractions; namespace CompositePattern.Menus
{
public class Menu : MenuComponent
{
readonly List<MenuComponent> _menuComponents; public Menu(string name, string description)
{
Name = name;
Description = description;
_menuComponents = new List<MenuComponent>();
} public override string Name { get; }
public override string Description { get; } public override void Add(MenuComponent menuComponent)
{
_menuComponents.Add(menuComponent);
} public override void Remove(MenuComponent menuComponent)
{
_menuComponents.Remove(menuComponent);
} public override MenuComponent GetChild(int i)
{
return _menuComponents[i];
} public override void Print()
{
Console.Write($"\n{Name}");
Console.WriteLine($", {Description}");
Console.WriteLine("------------------------------");
}
}
}
注意Menu和MenuItem的Print()方法, 它们目前只能打印自己的东西, 还无法打印出整个组合. 也就是说如果打印的是菜单Menu的话, 那么它下面挂着的菜单Menu和菜单项MenuItems都应该被打印出来.
那么我们现在修复这个问题:
public override void Print()
{
Console.Write($"\n{Name}");
Console.WriteLine($", {Description}");
Console.WriteLine("------------------------------"); foreach (var menuComponent in _menuComponents)
{
menuComponent.Print();
}
}
服务员 Waitress:
using CompositePattern.Abstractions; namespace CompositePattern.Waitresses
{
public class Waitress
{
private readonly MenuComponent _allMenus; public Waitress(MenuComponent allMenus)
{
_allMenus = allMenus;
} public void PrintMenu()
{
_allMenus.Print();
}
}
}
按照这个设计, 菜单组合在运行时将会是这个样子:
下面我们来测试一下:
using System;
using CompositePattern.Menus;
using CompositePattern.Waitresses; namespace CompositePattern
{
class Program
{
static void Main(string[] args)
{
MenuTestDrive();
Console.ReadKey();
} static void MenuTestDrive()
{
var pancakeHouseMenu = new Menu("PANCAKE HOUSE MENU", "Breakfast");
var dinerMenu = new Menu("DINER MENU", "Lunch");
var cafeMenu = new Menu("CAFE MENU", "Dinner");
var dessertMenu = new Menu("DESSERT MENU", "Dessert of courrse!"); var allMenus = new Menu("ALL MENUS", "All menus combined");
allMenus.Add(pancakeHouseMenu);
allMenus.Add(dinerMenu);
allMenus.Add(cafeMenu); pancakeHouseMenu.Add(new MenuItem("Vegetarian BLT", "(Fakin’) Bacon with lettuce & tomato on whole wheat", true, 2.99));
pancakeHouseMenu.Add(new MenuItem("K&B’s Pancake Breakfast", "Pancakes with scrambled eggs, and toast", true, 2.99));
pancakeHouseMenu.Add(new MenuItem("Regular Pancake Breakfast", "Pancakes with fried eggs, sausage", false, 2.99));
pancakeHouseMenu.Add(new MenuItem("Blueberry Pancakes", "Pancakes made with fresh blueberries", true, 3.49));
pancakeHouseMenu.Add(new MenuItem("Waffles", "Waffles, with your choice of blueberries or strawberries", true, 3.59)); dinerMenu.Add(new MenuItem("Vegetarian BLT", "(Fakin’) Bacon with lettuce & tomato on whole wheat", true, 2.99));
dinerMenu.Add(new MenuItem("BLT", "Bacon with lettuce & tomato on whole wheat", false, 2.99));
dinerMenu.Add(new MenuItem("Soup of the day", "Soup of the day, with a side of potato salad", false, 3.29));
dinerMenu.Add(new MenuItem("Hotdog", "A hot dog, with saurkraut, relish, onions, topped with cheese", false, 3.05));
dinerMenu.Add(new MenuItem("Pasta", "Spaghetti with Marinara Sauce, and a slice of sourdough bread", true, 3.89)); dinerMenu.Add(dessertMenu);
dessertMenu.Add(new MenuItem("Apple pie", "Apple pie with a flakey crust, topped with vanilla ice cream", true, 1.59));
dessertMenu.Add(new MenuItem("Cheese pie", "Creamy New York cheessecake, with a chocolate graham crust", true, 1.99));
dessertMenu.Add(new MenuItem("Sorbet", "A scoop of raspberry and a scoop of lime", true, 1.89)); cafeMenu.Add(new MenuItem("Veggie Burger and Air Fries", "Veggie burger on a whole wheat bun, lettuce, tomato, and fries", true, 3.99));
cafeMenu.Add(new MenuItem("Soup of the day", "A cup of the soup of the day, with a side salad", false, 3.69));
cafeMenu.Add(new MenuItem("Burrito", "A large burrito, with whole pinto beans, salsa, guacamole", true, 4.29)); var waitress = new Waitress(allMenus);
waitress.PrintMenu(); }
}
}
Ok.
慢着, 之前我们讲过单一职责原则. 现在一个类拥有了两个职责...
确实是这样的, 我们可以这样说, 组合模式用单一责任原则换取了透明性.
透明性是什么? 就是允许组件接口(Component interface)包括了子节点管理操作和叶子操作, 客户可以一致的对待组合节点或叶子; 所以任何一个元素到底是组合节点还是叶子, 这件事对客户来说是透明的.
当然这么做会损失一些安全性. 客户可以对某种类型的节点做出毫无意义的操作, 当然了, 这也是设计的决定.
组合迭代器
服务员现在想打印所有的菜单, 或者打印出所有的素食菜单项.
这里我们就需要实现组合迭代器.
要实现一个组合迭代器, 首先在抽象类MenuComponent里添加一个CreateEnumerator()的方法.
public virtual IEnumerator<MenuComponent> CreateEnumerator()
{
return new NullEnumerator();
}
注意NullEnumerator:
using System.Collections;
using System.Collections.Generic;
using CompositePattern.Abstractions; namespace CompositePattern.Iterators
{
public class NullEnumerator : IEnumerator<MenuComponent>
{
public bool MoveNext()
{
return false;
} public void Reset()
{ } public MenuComponent Current => null; object IEnumerator.Current => Current; public void Dispose()
{
}
}
}
我们可以用两种方式来实现NullEnumerator:
- 返回null
- 当MoveNext()被调用的时候总返回false. (我采用的是这个)
这对MenuItem, 就没有必要实现这个创建迭代器(遍历器)方法了.
请仔细看下面这个组合迭代器(遍历器)的代码, 一定要弄明白, 这里面就是递归, 递归:
using System;
using System.Collections;
using System.Collections.Generic;
using CompositePattern.Abstractions;
using CompositePattern.Menus; namespace CompositePattern.Iterators
{
public class CompositeEnumerator : IEnumerator<MenuComponent>
{
private readonly Stack<IEnumerator<MenuComponent>> _stack = new Stack<IEnumerator<MenuComponent>>(); public CompositeEnumerator(IEnumerator<MenuComponent> enumerator)
{
_stack.Push(enumerator);
} public bool MoveNext()
{
if (_stack.Count == )
{
return false;
} var enumerator = _stack.Peek();
if (!enumerator.MoveNext())
{
_stack.Pop();
return MoveNext();
} return true;
} public MenuComponent Current
{
get
{
var enumerator = _stack.Peek();
var menuComponent = enumerator.Current;
if (menuComponent is Menu)
{
_stack.Push(menuComponent.CreateEnumerator());
}
return menuComponent;
}
} object IEnumerator.Current => Current; public void Reset()
{
throw new NotImplementedException();
} public void Dispose()
{
}
}
}
服务员 Waitress添加打印素食菜单的方法:
public void PrintVegetarianMenu()
{
var enumerator = _allMenus.CreateEnumerator();
Console.WriteLine("\nVEGETARIAN MENU\n--------");
while (enumerator.MoveNext())
{
var menuComponent = enumerator.Current;
try
{
if (menuComponent.IsVegetarian)
{
menuComponent.Print();
}
}
catch (NotSupportedException e)
{
}
}
}
注意这里的try catch, try catch一般是用来捕获异常的. 我们也可以不这样做, 我们可以先判断它的类型是否为MenuItem, 但这个过程就让我们失去了透明性, 也就是说 我们无法一致的对待Menu和MenuItem了.
我们也可以在Menu里面实现IsVegetarian属性Get方法, 这可以保证透明性. 但是这样做不一定合理, 也许其它人有更合理的原因会把Menu的IsVegetarian给实现了. 所以我们还是使用try catch吧.
测试:
Ok.
总结
设计原则: 一个类只能有一个让它改变的原因.
迭代器模式: 迭代器模式提供了一种访问聚合对象(例如集合)元素的方式, 而且又不暴露该对象的内部表示.
组合模式: 组合模式允许你把对象们组合成树形的结构, 从而来表示整体的层次. 通过组合, 客户可以对单个对象或对象们的组合进行一致的处理.
针对C#来说, 上面的代码肯定不是最简单最直接的实现方式, 但是通过这些比较原始的代码可以对设计模式理解的更好一些.
改系列的源码在: https://github.com/solenovex/Head-First-Design-Patterns-in-CSharp
使用C# (.NET Core) 实现组合设计模式 (Composite Pattern)的更多相关文章
- php组合设计模式(composite pattern)
过十点. <?php /* The composite pattern is about treating the hierarchy of objects as a single object ...
- 乐在其中设计模式(C#) - 组合模式(Composite Pattern)
原文:乐在其中设计模式(C#) - 组合模式(Composite Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 组合模式(Composite Pattern) 作者:weba ...
- 设计模式系列之组合模式(Composite Pattern)——树形结构的处理
说明:设计模式系列文章是读刘伟所著<设计模式的艺术之道(软件开发人员内功修炼之道)>一书的阅读笔记.个人感觉这本书讲的不错,有兴趣推荐读一读.详细内容也可以看看此书作者的博客https:/ ...
- 浅谈设计模式--组合模式(Composite Pattern)
组合模式(Composite Pattern) 组合模式,有时候又叫部分-整体结构(part-whole hierarchy),使得用户对单个对象和对一组对象的使用具有一致性.简单来说,就是可以像使用 ...
- 二十四种设计模式:组合模式(Composite Pattern)
组合模式(Composite Pattern) 介绍将对象组合成树形结构以表示"部分-整体"的层次结构.它使得客户对单个对象和复合对象的使用具有一致性.示例有一个Message实体 ...
- 【设计模式】组合模式 Composite Pattern
树形结构是软件行业很常见的一种结构,几乎随处可见, 比如: HTML 页面中的DOM,产品的分类,通常一些应用或网站的菜单,Windows Form 中的控件继承关系,Android中的View继承 ...
- 设计模式 - 组合模式(composite pattern) 迭代器(iterator) 具体解释
组合模式(composite pattern) 迭代器(iterator) 具体解释 本文地址: http://blog.csdn.net/caroline_wendy 參考组合模式(composit ...
- python 设计模式之组合模式Composite Pattern
#引入一 文件夹对我们来说很熟悉,文件夹里面可以包含文件夹,也可以包含文件. 那么文件夹是个容器,文件夹里面的文件夹也是个容器,文件夹里面的文件是对象. 这是一个树形结构 咱们生活工作中常用的一种结构 ...
- 设计模式-12组合模式(Composite Pattern)
1.模式动机 很多时候会存在"部分-整体"的关系,例如:大学中的部门与学院.总公司中的部门与分公司.学习用品中的书与书包.在软件开发中也是这样,例如,文件系统中的文件与文件夹.窗体 ...
随机推荐
- New UWP Community Toolkit - Carousel
概述 New UWP Community Toolkit V2.2.0 的版本发布日志中提到了 Carousel 的调整,本篇我们结合代码详细讲解 Carousel 的实现. Carousel 是 ...
- 集大1513 & 1514班 软件工程第二次作业评分与点评
谢谢按时完成作业的同学. 请大家在今后的作业中多思考,认真完成并注意作业的原创性. 学号 作业标题 作业地址 提交日期 分数 201521121087 微信APP简要分析 http://www.cnb ...
- C语言指针作业
一.PTA实验作业 题目1:6-5 判断回文字符串 1. 本题PTA提交列表 2. 设计思路 3.代码截图 4.本题调试过程碰到问题及PTA提交列表情况说明. 第一次做的时候我j直接等于count,其 ...
- EVA 4400存储硬盘故障数据恢复方案和数据恢复过程
EVA系列存储是一款以虚拟化存储为实现目的的HP中高端存储设备,平时数据会不断的迁移,加上任务通常较为繁重,所以磁盘的负载相对是较重的,也是很容易出现故障的.EVA是依靠大量磁盘的冗余空间,以及故障后 ...
- 为微软samples-for-ai贡献代码是种怎么样的体验?
推送原文链接:传送门 关注SomedayWill,了解为微软项目贡献代码的始终. 还记得微软神器samples-for-ai吗?它可不仅仅可以用来安装框架,它其实是个开源的AI样例库,以Visual ...
- Django rest framework源码分析(4)----版本
版本 新建一个工程Myproject和一个app名为api (1)api/models.py from django.db import models class UserInfo(models.Mo ...
- JavaScript查找数组中最大的值
// 查找一个数组中最大的数 // 定义一个方法 searchMax function searchMax(arr) { // 声明一个变量MaxNumber假设为数组中最大的值arr[0]; var ...
- 基于dns搭建eureka集群
eureka集群方案: 1.通常我们部署的eureka节点多于两个,根据实际需求,只需要将相邻节点进行相互注册(eureka节点形成环状),就达到了高可用性集群,任何一个eureka节点挂掉不会受到影 ...
- SpringCloud的服务消费者 (一):(rest+ribbon)访问注册的微服务
采用Ribbon或Feign方式访问注册到EurekaServer中的微服务.1.Ribbon实现了客户端负载均衡,Feign底层调用Ribbon2.注册在EurekaServer中的微服务api,不 ...
- 新概念英语(1-43)Hurry up!
新概念英语(1-43)Hurry up! How do you know Sam doesn't make the tea very often? A:Can you make the tea, Sa ...