重溫《深入淺出設計模式》合成模式 (Book Review of Head First Design Pattern, Composite Pattern)

2020-07-07

合成模式就是將類別建構為樹狀的關係,於是樹資料結構的階層關係、遞迴走訪便可以運用在設計系統上。同時合成模式可以與反覆器做結合,客製出強大的類別,保持新增的彈性同時也兼具走訪的便利。

logo

合成模式

Compose objects into tree structures to represent whole-part hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

合成模式的 UML

After Pattern

範例是以餐廳菜單為例,如何表現出菜單中的菜單(晚餐菜單需要包括甜點菜單),藉由合成模式, MenuComponent 為提供 Menu 與 MenuItem 實作的抽象類別,Menu 則是作為聚合類別,其中包括一個 MenuComponent 型別的聚合物類,作為子聚合,也是合成模式的精華所在; MenuItem 則相當於樹狀資料結構中的 Leaf,對應則是餐廳中具體的品項。

合成模式打開了對於設計模式的想像,結合資料結構實作精神與應用方式,從模式的層級去解決系統的問題。

MenuComponent.cs (Component)

public abstract class MenuComponent
{
    public virtual void Add(MenuComponent component)
    {
        throw new NotImplementedException();
    }

    public virtual void Remove(MenuComponent component)
    {
        throw new NotImplementedException();
    }

    public virtual MenuComponent GetChild(int i)
    {
        throw new NotImplementedException();
    }

    public virtual string Name{ get;}
    public virtual string Description { get; }
    public virtual bool Vegetarian { get; }
    public virtual double Price{ get; }

    public virtual void Print()
    {
        throw new NotImplementedException();
    }
}

Menu.cs (Composite)

public class Menu : MenuComponent
{
    List<MenuComponent> _components = new List<MenuComponent>();
    public Menu(string name, string description)
    {
        Name = name;
        Description = description;
    }

    public override string Name { get; }

    public override string Description { get; }

    public override void Add(MenuComponent component)
    {
        _components.Add(component);
    }

    public override MenuComponent GetChild(int i)
    {
        return base.GetChild(i);
    }

    public override void Print()
    {
        Console.WriteLine($"==={Name}===");
        foreach (var menuComponent in _components)
        {
            menuComponent.Print();
        }
    }

    public override void Remove(MenuComponent component)
    {
        _components.Remove(component);
    }
}

MenuItem.cs (Leaf)

public class MenuItem : MenuComponent
{
    public MenuItem(string name, string description, double price, bool isveg)
    {
        Name = name;
        Description = description;
        Price = price;
        Vegetarian = isveg;
    }

    public override string Name { get; }
    public override string Description { get; }
    public override double Price { get; }
    public override bool Vegetarian { get; }

    public override void Print()
    {
        Console.WriteLine($"{Name} : {Price} {(Vegetarian ? "Veg" : "Meat")} - {Description}");
    }
}

Program.cs

var breakfast = new Menu("Breakfast", "BK Store");
var lunch = new Menu("Lunch", "Lunch Store");
var dinner = new Menu("Dinner", "Ding Store");
var dessert = new Menu("Dessert", "Ding Store");

var menu = new Menu("All", "");

breakfast.Add(new MenuItem("Waffles", "Butterscotch waffles", 140, false));
breakfast.Add(new MenuItem("Corn Flakes", "Kellogs", 80, true));

lunch.Add(new MenuItem("Burger", "Cheese and Onion Burger", 250, true));
lunch.Add(new MenuItem("Sandwich", "Chicken Sandwich", 280, false));

dinner.Add(new MenuItem("Pizza", "Cheese and Tomato Pizza", 210, true));
dinner.Add(new MenuItem("Fries", "Cheese Fries", 100, true));
dinner.Add(new MenuItem("Pasta", "Chicken Pasta", 280, false));

dessert.Add(new MenuItem("Ice Cream", "Vanilla and Chocolate", 120, true));
dessert.Add(new MenuItem("Cake", "Choclate Cake Slice", 180, false));

dinner.Add(dessert);
menu.Add(breakfast);
menu.Add(lunch);
menu.Add(dinner);

menu.Print();

小結

✔️將變動的部分封裝起來
✔️類別應該對需求保持開放;應該對既有的程式碼修改保持關閉
Leaf、Menu 都是會變動的需求,封裝成實作 MenuComponent 的類別,讓擴充與變動保持彈性

✔️多用合成,少用繼承
✔️讓需要互動的物件之間的關係鬆綁
合成模式使用合成的方式讓類別聚合的關係鬆綁

✔️針對介面撰寫,而非針對實踐方式撰寫。
聚合的物件共同實作 MenuComponent 類別

✔️讓物件保有最少的知識
✔️單一類別單一責任
每個 MenuComponent 只認識自己的子聚合物件

參考資料