本文的概念性内容来自深入浅出设计模式一书.

本文需结合上一篇文章(使用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:

  1. using System;
  2.  
  3. namespace CompositePattern.Abstractions
  4. {
  5. public abstract class MenuComponent
  6. {
  7. public virtual void Add(MenuComponent menuComponent)
  8. {
  9. throw new NotSupportedException();
  10. }
  11.  
  12. public virtual void Remove(MenuComponent menuComponent)
  13. {
  14. throw new NotSupportedException();
  15. }
  16.  
  17. public virtual MenuComponent GetChild(int i)
  18. {
  19. throw new NotSupportedException();
  20. }
  21.  
  22. public virtual string Name => throw new NotSupportedException();
  23. public virtual string Description => throw new NotSupportedException();
  24. public virtual double Price => throw new NotSupportedException();
  25. public virtual bool IsVegetarian => throw new NotSupportedException();
  26.  
  27. public virtual void Print()
  28. {
  29. throw new NotSupportedException();
  30. }
  31. }
  32. }

MenuItem:

  1. using System;
  2. using CompositePattern.Abstractions;
  3.  
  4. namespace CompositePattern.Menus
  5. {
  6. public class MenuItem : MenuComponent
  7. {
  8. public MenuItem(string name, string description, double price, bool isVegetarian)
  9. {
  10. Name = name;
  11. Description = description;
  12. Price = price;
  13. IsVegetarian = isVegetarian;
  14. }
  15.  
  16. public override string Name { get; }
  17. public override string Description { get; }
  18. public override double Price { get; }
  19. public override bool IsVegetarian { get; }
  20.  
  21. public override void Print()
  22. {
  23. Console.Write($"\t{Name}");
  24. if (IsVegetarian)
  25. {
  26. Console.Write("(v)");
  27. }
  28.  
  29. Console.WriteLine($", {Price}");
  30. Console.WriteLine($"\t\t -- {Description}");
  31. }
  32. }
  33. }

Menu:

  1. using System;
  2. using System.Collections.Generic;
  3. using CompositePattern.Abstractions;
  4.  
  5. namespace CompositePattern.Menus
  6. {
  7. public class Menu : MenuComponent
  8. {
  9. readonly List<MenuComponent> _menuComponents;
  10.  
  11. public Menu(string name, string description)
  12. {
  13. Name = name;
  14. Description = description;
  15. _menuComponents = new List<MenuComponent>();
  16. }
  17.  
  18. public override string Name { get; }
  19. public override string Description { get; }
  20.  
  21. public override void Add(MenuComponent menuComponent)
  22. {
  23. _menuComponents.Add(menuComponent);
  24. }
  25.  
  26. public override void Remove(MenuComponent menuComponent)
  27. {
  28. _menuComponents.Remove(menuComponent);
  29. }
  30.  
  31. public override MenuComponent GetChild(int i)
  32. {
  33. return _menuComponents[i];
  34. }
  35.  
  36. public override void Print()
  37. {
  38. Console.Write($"\n{Name}");
  39. Console.WriteLine($", {Description}");
  40. Console.WriteLine("------------------------------");
  41. }
  42. }
  43. }

注意Menu和MenuItem的Print()方法, 它们目前只能打印自己的东西, 还无法打印出整个组合. 也就是说如果打印的是菜单Menu的话, 那么它下面挂着的菜单Menu和菜单项MenuItems都应该被打印出来.

那么我们现在修复这个问题:

  1. public override void Print()
  2. {
  3. Console.Write($"\n{Name}");
  4. Console.WriteLine($", {Description}");
  5. Console.WriteLine("------------------------------");
  6.  
  7. foreach (var menuComponent in _menuComponents)
  8. {
  9. menuComponent.Print();
  10. }
  11. }

服务员 Waitress:

  1. using CompositePattern.Abstractions;
  2.  
  3. namespace CompositePattern.Waitresses
  4. {
  5. public class Waitress
  6. {
  7. private readonly MenuComponent _allMenus;
  8.  
  9. public Waitress(MenuComponent allMenus)
  10. {
  11. _allMenus = allMenus;
  12. }
  13.  
  14. public void PrintMenu()
  15. {
  16. _allMenus.Print();
  17. }
  18. }
  19. }

按照这个设计, 菜单组合在运行时将会是这个样子:

下面我们来测试一下:

  1. using System;
  2. using CompositePattern.Menus;
  3. using CompositePattern.Waitresses;
  4.  
  5. namespace CompositePattern
  6. {
  7. class Program
  8. {
  9. static void Main(string[] args)
  10. {
  11. MenuTestDrive();
  12. Console.ReadKey();
  13. }
  14.  
  15. static void MenuTestDrive()
  16. {
  17. var pancakeHouseMenu = new Menu("PANCAKE HOUSE MENU", "Breakfast");
  18. var dinerMenu = new Menu("DINER MENU", "Lunch");
  19. var cafeMenu = new Menu("CAFE MENU", "Dinner");
  20. var dessertMenu = new Menu("DESSERT MENU", "Dessert of courrse!");
  21.  
  22. var allMenus = new Menu("ALL MENUS", "All menus combined");
  23. allMenus.Add(pancakeHouseMenu);
  24. allMenus.Add(dinerMenu);
  25. allMenus.Add(cafeMenu);
  26.  
  27. pancakeHouseMenu.Add(new MenuItem("Vegetarian BLT", "(Fakin’) Bacon with lettuce & tomato on whole wheat", true, 2.99));
  28. pancakeHouseMenu.Add(new MenuItem("K&B’s Pancake Breakfast", "Pancakes with scrambled eggs, and toast", true, 2.99));
  29. pancakeHouseMenu.Add(new MenuItem("Regular Pancake Breakfast", "Pancakes with fried eggs, sausage", false, 2.99));
  30. pancakeHouseMenu.Add(new MenuItem("Blueberry Pancakes", "Pancakes made with fresh blueberries", true, 3.49));
  31. pancakeHouseMenu.Add(new MenuItem("Waffles", "Waffles, with your choice of blueberries or strawberries", true, 3.59));
  32.  
  33. dinerMenu.Add(new MenuItem("Vegetarian BLT", "(Fakin’) Bacon with lettuce & tomato on whole wheat", true, 2.99));
  34. dinerMenu.Add(new MenuItem("BLT", "Bacon with lettuce & tomato on whole wheat", false, 2.99));
  35. dinerMenu.Add(new MenuItem("Soup of the day", "Soup of the day, with a side of potato salad", false, 3.29));
  36. dinerMenu.Add(new MenuItem("Hotdog", "A hot dog, with saurkraut, relish, onions, topped with cheese", false, 3.05));
  37. dinerMenu.Add(new MenuItem("Pasta", "Spaghetti with Marinara Sauce, and a slice of sourdough bread", true, 3.89));
  38.  
  39. dinerMenu.Add(dessertMenu);
  40. dessertMenu.Add(new MenuItem("Apple pie", "Apple pie with a flakey crust, topped with vanilla ice cream", true, 1.59));
  41. dessertMenu.Add(new MenuItem("Cheese pie", "Creamy New York cheessecake, with a chocolate graham crust", true, 1.99));
  42. dessertMenu.Add(new MenuItem("Sorbet", "A scoop of raspberry and a scoop of lime", true, 1.89));
  43.  
  44. cafeMenu.Add(new MenuItem("Veggie Burger and Air Fries", "Veggie burger on a whole wheat bun, lettuce, tomato, and fries", true, 3.99));
  45. cafeMenu.Add(new MenuItem("Soup of the day", "A cup of the soup of the day, with a side salad", false, 3.69));
  46. cafeMenu.Add(new MenuItem("Burrito", "A large burrito, with whole pinto beans, salsa, guacamole", true, 4.29));
  47.  
  48. var waitress = new Waitress(allMenus);
  49. waitress.PrintMenu();
  50.  
  51. }
  52. }
  53. }

Ok.

慢着, 之前我们讲过单一职责原则. 现在一个类拥有了两个职责...

确实是这样的, 我们可以这样说, 组合模式用单一责任原则换取了透明性.

透明性是什么? 就是允许组件接口(Component interface)包括了子节点管理操作和叶子操作, 客户可以一致的对待组合节点或叶子; 所以任何一个元素到底是组合节点还是叶子, 这件事对客户来说是透明的.

当然这么做会损失一些安全性. 客户可以对某种类型的节点做出毫无意义的操作, 当然了, 这也是设计的决定.

组合迭代器

服务员现在想打印所有的菜单, 或者打印出所有的素食菜单项.

这里我们就需要实现组合迭代器.

要实现一个组合迭代器, 首先在抽象类MenuComponent里添加一个CreateEnumerator()的方法.

  1. public virtual IEnumerator<MenuComponent> CreateEnumerator()
  2. {
  3. return new NullEnumerator();
  4. }

注意NullEnumerator:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using CompositePattern.Abstractions;
  4.  
  5. namespace CompositePattern.Iterators
  6. {
  7. public class NullEnumerator : IEnumerator<MenuComponent>
  8. {
  9. public bool MoveNext()
  10. {
  11. return false;
  12. }
  13.  
  14. public void Reset()
  15. {
  16.  
  17. }
  18.  
  19. public MenuComponent Current => null;
  20.  
  21. object IEnumerator.Current => Current;
  22.  
  23. public void Dispose()
  24. {
  25. }
  26. }
  27. }

我们可以用两种方式来实现NullEnumerator:

  1. 返回null
  2. 当MoveNext()被调用的时候总返回false. (我采用的是这个)

这对MenuItem, 就没有必要实现这个创建迭代器(遍历器)方法了.

请仔细看下面这个组合迭代器(遍历器)的代码, 一定要弄明白, 这里面就是递归, 递归:

  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using CompositePattern.Abstractions;
  5. using CompositePattern.Menus;
  6.  
  7. namespace CompositePattern.Iterators
  8. {
  9. public class CompositeEnumerator : IEnumerator<MenuComponent>
  10. {
  11. private readonly Stack<IEnumerator<MenuComponent>> _stack = new Stack<IEnumerator<MenuComponent>>();
  12.  
  13. public CompositeEnumerator(IEnumerator<MenuComponent> enumerator)
  14. {
  15. _stack.Push(enumerator);
  16. }
  17.  
  18. public bool MoveNext()
  19. {
  20. if (_stack.Count == )
  21. {
  22. return false;
  23. }
  24.  
  25. var enumerator = _stack.Peek();
  26. if (!enumerator.MoveNext())
  27. {
  28. _stack.Pop();
  29. return MoveNext();
  30. }
  31.  
  32. return true;
  33. }
  34.  
  35. public MenuComponent Current
  36. {
  37. get
  38. {
  39. var enumerator = _stack.Peek();
  40. var menuComponent = enumerator.Current;
  41. if (menuComponent is Menu)
  42. {
  43. _stack.Push(menuComponent.CreateEnumerator());
  44. }
  45. return menuComponent;
  46. }
  47. }
  48.  
  49. object IEnumerator.Current => Current;
  50.  
  51. public void Reset()
  52. {
  53. throw new NotImplementedException();
  54. }
  55.  
  56. public void Dispose()
  57. {
  58. }
  59. }
  60. }

服务员 Waitress添加打印素食菜单的方法:

  1. public void PrintVegetarianMenu()
  2. {
  3. var enumerator = _allMenus.CreateEnumerator();
  4. Console.WriteLine("\nVEGETARIAN MENU\n--------");
  5. while (enumerator.MoveNext())
  6. {
  7. var menuComponent = enumerator.Current;
  8. try
  9. {
  10. if (menuComponent.IsVegetarian)
  11. {
  12. menuComponent.Print();
  13. }
  14. }
  15. catch (NotSupportedException e)
  16. {
  17. }
  18. }
  19. }

注意这里的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)的更多相关文章

  1. php组合设计模式(composite pattern)

    过十点. <?php /* The composite pattern is about treating the hierarchy of objects as a single object ...

  2. 乐在其中设计模式(C#) - 组合模式(Composite Pattern)

    原文:乐在其中设计模式(C#) - 组合模式(Composite Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 组合模式(Composite Pattern) 作者:weba ...

  3. 设计模式系列之组合模式(Composite Pattern)——树形结构的处理

    说明:设计模式系列文章是读刘伟所著<设计模式的艺术之道(软件开发人员内功修炼之道)>一书的阅读笔记.个人感觉这本书讲的不错,有兴趣推荐读一读.详细内容也可以看看此书作者的博客https:/ ...

  4. 浅谈设计模式--组合模式(Composite Pattern)

    组合模式(Composite Pattern) 组合模式,有时候又叫部分-整体结构(part-whole hierarchy),使得用户对单个对象和对一组对象的使用具有一致性.简单来说,就是可以像使用 ...

  5. 二十四种设计模式:组合模式(Composite Pattern)

    组合模式(Composite Pattern) 介绍将对象组合成树形结构以表示"部分-整体"的层次结构.它使得客户对单个对象和复合对象的使用具有一致性.示例有一个Message实体 ...

  6. 【设计模式】组合模式 Composite Pattern

    树形结构是软件行业很常见的一种结构,几乎随处可见,  比如: HTML 页面中的DOM,产品的分类,通常一些应用或网站的菜单,Windows Form 中的控件继承关系,Android中的View继承 ...

  7. 设计模式 - 组合模式(composite pattern) 迭代器(iterator) 具体解释

    组合模式(composite pattern) 迭代器(iterator) 具体解释 本文地址: http://blog.csdn.net/caroline_wendy 參考组合模式(composit ...

  8. python 设计模式之组合模式Composite Pattern

    #引入一 文件夹对我们来说很熟悉,文件夹里面可以包含文件夹,也可以包含文件. 那么文件夹是个容器,文件夹里面的文件夹也是个容器,文件夹里面的文件是对象. 这是一个树形结构 咱们生活工作中常用的一种结构 ...

  9. 设计模式-12组合模式(Composite Pattern)

    1.模式动机 很多时候会存在"部分-整体"的关系,例如:大学中的部门与学院.总公司中的部门与分公司.学习用品中的书与书包.在软件开发中也是这样,例如,文件系统中的文件与文件夹.窗体 ...

随机推荐

  1. 敏捷冲刺(Beta版本)

    评分基准: 按时交 - 有分(计划安排-10分,敏捷冲刺-70分),检查的项目包括后文的三个个方面 冲刺计划安排(单独1篇博客,基本分5分,根据完成质量加分,原则上不超过满分10分) 七天的敏捷冲刺( ...

  2. C语言实现Linux命令——od

    C语言实现Linux命令--od 实现要求: 复习c文件处理内容 编写myod.c 用myod XXX实现Linux下od -tx -tc XXX的功能 main与其他分开,制作静态库和动态库 编写M ...

  3. Python多线程案例

    from time import ctime,sleep import threading def music(): for i in range(2): print ("I was lis ...

  4. 让linux远程主机在后台运行脚本

    后台挂起:python xxx.py & 在脚本命令后面加入"&"符号就可以后台运行.结束进程:kill -9 sidps -ef | grep ... 查询sid

  5. EasyUI中DataGrid隔行改变背景颜色。

    <table id="dg" class="easyui-datagrid" style="width: 1000px; height: 300 ...

  6. thinkphp后台向前台传值没有传过去的小问题

    if($listyyarr){ $this->assign('listyyarr',$listyyarr); //$this->assign('nowDated',$endDated); ...

  7. mongo数据库的常见操作

    连接mongodb数据库的命令查看对应数据库mongo.exeuse shujukuming;db.opportunity.findOne({"id":5}); db.opport ...

  8. AngularJS1.X学习笔记13-动画和触摸

    本文主要涉及了ngAnimation和ngTouch模块,自由男人讲的比较少,估计要用的时候还要更加系统的学习一下. 一.安装 没错,就是酱紫. 二.玩玩动画 <!DOCTYPE html> ...

  9. 《深入实践Spring Boot》阅读笔记之二:分布式应用开发

    上篇文章总结了<深入实践Spring Boot>的第一部分,这篇文章介绍第二部分:分布式应用开发,以及怎么构建一个高性能的服务平台. 主要从以下几个方面总结: Spring Boot SS ...

  10. 一个适用于单页应用,返回原始滚动条位置的demo

    如题,最近做一个项目时,由于页面太长,跳转后在返回又回到初始位置,不利于用户体验,需要每次返回到用户离开该页面是的位置.由于是移动端项目,使用了移动端的套ui框架framework7,本身框架的机制是 ...