使用C# (.NET Core) 實現組合設計模式 (Composite Pattern)

来源:https://www.cnblogs.com/cgzl/archive/2018/04/22/8907753.html
-Advertisement-
Play Games

本文的概念性內容來自深入淺出設計模式一書. 本文需結合上一篇文章(使用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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 上一篇我們已經初步的構建起了一個學生管理系統的模型,現在接著來繼續完善它吧。 1、上傳圖片/文件等資源 有時候需要添加一些附件,例如,新生剛入學,大家相互之間還不熟悉,希望能通過照片來加深印象,並且方便教學管理。 首先,對demo/urls.py文件進行改造,給urlpatterns添加static ...
  • 一.http協議 二.hibernate緩存模式,級別;Hibernate和mybatis的區別和優缺點 三.SQL優化經驗 四.分散式集群和Redis 五.Spring Aop,動態代理; 六.多線程安全問題,多線程實現 thread local 一.HTTP協議(HyperText Transf ...
  • 最近在客戶項目上剛好遇到一個問題,項目需求是要獲取某台機床的實時狀態,問題點剛好就在於該機床不是傳統意義上的數控機床,也不是PLC控制器,只有一個上傳下載程式文件的應用程式,上面剛好有幾個按鈕可以大概判斷當前工作狀態,轉眼一想,是否可以實時獲取幾個按鈕的狀態,從而簡單分析下就確定機床加工狀態。 說乾 ...
  • 5-1 條件測試 :編寫一系列條件測試;將每個測試以及你對其結果的預測和實際結果都列印出來。你編寫的代碼應類似於下麵這樣: car = 'subaru' print("Is car == 'subaru'? I predict True.") print(car == 'subaru') print ...
  • Python中的random模塊用於生成隨機數。 下麵具體介紹random模塊的功能: 1.random.random() #用於生成一個0到1的 隨機浮點數:0<= n < 1.0 2.random.uniform(a,b) #用於生成一個指定範圍內的隨機符點數,兩個參數其中一個是上限,一個是下限 ...
  • using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Linq; using System.Reflection; using Syst ...
  • 主題 本篇我將會介紹驗證用戶的機制當賬戶被創建的時候,同樣 這個過程主要有IUserValidator這個介面來實現的,和密碼驗證一樣Identity同樣也內置已經實現好的賬戶驗證。賬戶驗證的比較簡單,我會先介紹內置的IUserValidator的使用,然後會簡單介紹一些源代碼,最後會演示怎實現一個 ...
  • 問題背景: 最近做一個非常簡單的功能,就是使用ajax請求的方式從服務端請求一段下拉表的數據。 以前也有做過這個功能,只不過這次做這個功能的時候冒出了一個想法: 我請求的這段數據它是一段相對比較固定的數據,也就是說它不怎麼改變,也許幾個月才會改變一次。由於這種數據的變化周期很長,所以以前做這種功能的 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...