使用C# (.NET Core) 实现组合设计模式 (Composite Pattern)

简介: 本文的概念性内容来自深入浅出设计模式一书. 本文需结合上一篇文章(使用C# (.NET Core) 实现迭代器设计模式)一起看. 上一篇文章我们研究了多个菜单一起使用的问题. 需求变更 就当我们感觉我们的设计已经足够好的时候, 新的需求来了, 我们不仅要支持多种菜单, 还要支持菜单下可以拥有子菜单.

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

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

  1. 返回null
  2. 当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

 

下面是我的关于ASP.NET Core Web API相关技术的公众号--草根专栏:

目录
相关文章
|
14天前
|
开发框架 .NET 开发者
简化 ASP.NET Core 依赖注入(DI)注册-Scrutor
Scrutor 是一个简化 ASP.NET Core 应用程序中依赖注入(DI)注册过程的开源库,支持自动扫描和注册服务。通过简单的配置,开发者可以轻松地从指定程序集中筛选、注册服务,并设置其生命周期,同时支持服务装饰等高级功能。适用于大型项目,提高代码的可维护性和简洁性。仓库地址:&lt;https://github.com/khellang/Scrutor&gt;
37 5
|
12天前
|
开发框架 监控 .NET
C#进阶-ASP.NET WebForms调用ASMX的WebService接口
通过本文的介绍,希望您能深入理解并掌握ASP.NET WebForms中调用ASMX WebService接口的方法和技巧,并在实际项目中灵活运用这些技术,提高开发效率和应用性能。
31 5
|
1月前
|
开发框架 .NET C#
在 ASP.NET Core 中创建 gRPC 客户端和服务器
本文介绍了如何使用 gRPC 框架搭建一个简单的“Hello World”示例。首先创建了一个名为 GrpcDemo 的解决方案,其中包含一个 gRPC 服务端项目 GrpcServer 和一个客户端项目 GrpcClient。服务端通过定义 `greeter.proto` 文件中的服务和消息类型,实现了一个简单的问候服务 `GreeterService`。客户端则通过 gRPC 客户端库连接到服务端并调用其 `SayHello` 方法,展示了 gRPC 在 C# 中的基本使用方法。
41 5
在 ASP.NET Core 中创建 gRPC 客户端和服务器
|
20天前
|
算法 Java 测试技术
Benchmark.NET:让 C# 测试程序性能变得既酷又简单
Benchmark.NET是一款专为 .NET 平台设计的性能基准测试框架,它可以帮助你测量代码的执行时间、内存使用情况等性能指标。它就像是你代码的 "健身教练",帮助你找到瓶颈,优化性能,让你的应用跑得更快、更稳!希望这个小教程能让你在追求高性能的路上越走越远,享受编程带来的无限乐趣!
70 13
|
22天前
|
开发框架 缓存 .NET
GraphQL 与 ASP.NET Core 集成:从入门到精通
本文详细介绍了如何在ASP.NET Core中集成GraphQL,包括安装必要的NuGet包、创建GraphQL Schema、配置GraphQL服务等步骤。同时,文章还探讨了常见问题及其解决方法,如处理复杂查询、错误处理、性能优化和实现认证授权等,旨在帮助开发者构建灵活且高效的API。
25 3
|
1月前
|
人工智能 开发框架 前端开发
C#/.NET/.NET Core技术前沿周刊 | 第 12 期(2024年11.01-11.10)
C#/.NET/.NET Core技术前沿周刊 | 第 12 期(2024年11.01-11.10)
|
1月前
|
开发框架 网络协议 .NET
C#/.NET/.NET Core优秀项目和框架2024年10月简报
C#/.NET/.NET Core优秀项目和框架2024年10月简报
|
28天前
|
数据库连接 数据库 C#
Windows下C# 通过ADO.NET方式连接南大通用GBase 8s数据库(上)
Windows下C# 通过ADO.NET方式连接南大通用GBase 8s数据库(上)
|
28天前
|
数据库连接 数据库 C#
Windows下C# 通过ADO.NET方式连接南大通用GBase 8s数据库(下)
本文接续前文,深入讲解了在Windows环境下使用C#和ADO.NET操作南大通用GBase 8s数据库的方法。通过Visual Studio 2022创建项目,添加GBase 8s的DLL引用,并提供了详细的C#代码示例,涵盖数据库连接、表的创建与修改、数据的增删查改等操作,旨在帮助开发者提高数据库管理效率。
|
1月前
|
人工智能 开发框架 安全
C#/.NET/.NET Core技术前沿周刊 | 第 13 期(2024年11.11-11.17)
C#/.NET/.NET Core技术前沿周刊 | 第 13 期(2024年11.11-11.17)

热门文章

最新文章