本文的概念性内容来自深入浅出设计模式一书.
本文需结合上一篇文章 (使用 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 == 0)
- {
- 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
来源: https://www.cnblogs.com/cgzl/p/8907753.html