9. Composite


Posted by WayneCheng on 2021-01-16

關於 Composite 本篇將討論以下幾個問題

1. 關於 Composite

2. UML

3. 將 UML 轉為程式碼

4. 情境


測試環境:

OS:Windows 10
IDE:Visual Studio 2019


1. 關於 Composite

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

by Gang of Four

  • 將物件組合成樹結構來表現部分與整體的階層
  • 組合可以使呼叫端以同樣方式呼叫單一個體(實體物件)或組合物件(容器)

Composite(組合)屬於結構型(Structural Patterns),當遇到未知階層與數量且可以使用樹結構來呈現的資料時,可以使用 Composite 來保留動態擴展結構的彈性。

優點:

  • 符合 開閉原則(Open Closed Principle)

缺點:

  • 違反 介面隔離原則(Interface Segregation Principle)

2. UML

Class 間關聯:

  • Client 關聯 Component
  • Leaf & Composite 繼承 Component
  • Composite 可包含 Component

Class:

  • Client:呼叫端
  • Component:定義呼叫端接口與管理子階層的接口
  • Leaf:定義組合中實體物件,且沒有下一階層
  • Composite:定義子階層,並存放實體物件或容器

3. 將 UML 轉為程式碼

定義容器 interface

/// <summary>
/// 定義容器 interface
/// </summary>
public interface IComponent
{
    string Name { get; }

    void Add(IComponent c);
    void Remove(IComponent c);
    void Display(int depth);
}

容器實作

/// <summary>
/// 容器實作
/// </summary>
public class Composite : IComponent
{
    private readonly List<IComponent> _children = new List<IComponent>();
    public string Name { get; }

    public Composite(string name)
    {
        Name = name;
    }

    public void Add(IComponent component)
    {
        _children.Add(component);
    }

    public void Remove(IComponent component)
    {
        _children.Remove(component);
    }

    public void Display(int depth)
    {
        var itemName = depth > 0 ? new string('+', depth) + " " + Name : Name;
        Console.WriteLine(itemName);

        foreach (IComponent component in _children)
        {
            component.Display(depth + 3);
        }
    }
}

實體物件實作

/// <summary>
/// 實體物件實作
/// </summary>
public class Leaf : IComponent
{
    public string Name { get; }

    public Leaf(string name)
    {
        Name = name;
    }

    public void Add(IComponent component)
    {
        Console.WriteLine("Cannot add child to leaf");
    }

    public void Remove(IComponent component)
    {
        Console.WriteLine("Cannot remove child from leaf");
    }

    public void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + " " + Name);
    }
}
  1. 建立樹狀結構root
  2. 於樹狀結構中新增/移除容器composite & 實體物件Leaf
static void Main(string[] args)
{
    // 樹結構
    // 建立根目錄
    Default.Composite root = new Default.Composite("root");
    root.Add(new Default.Leaf("項目 A"));
    root.Add(new Default.Leaf("項目 B"));

    // 建立容器 1
    Default.Composite composite1 = new Default.Composite("容器 1");
    composite1.Add(new Default.Leaf("項目 C"));
    composite1.Add(new Default.Leaf("項目 D"));
    // 建立容器 2
    Default.Composite composite2 = new Default.Composite("容器 2");
    composite2.Add(new Default.Leaf("項目 E"));

    // 容器 2 加入 容器 1
    composite1.Add(composite2);

    // 容器 1 加入根目錄
    root.Add(composite1);
    root.Add(new Default.Leaf("項目 F"));

    Default.Leaf leaf_G = new Default.Leaf("項目 G");
    root.Add(leaf_G);

    root.Display(0);

    Console.WriteLine("\n === 移除 項目 G ===\n");

    root.Remove(leaf_G);
    root.Display(0);

    Console.ReadLine();
}

執行結果

root
--- 項目 A
--- 項目 B
+++ 容器 1
------ 項目 C
------ 項目 D
++++++ 容器 2
--------- 項目 E
--- 項目 F
--- 項目 G

 === 移除 項目 G ===

root
--- 項目 A
--- 項目 B
+++ 容器 1
------ 項目 C
------ 項目 D
++++++ 容器 2
--------- 項目 E
--- 項目 F

4. 情境

我們接到了一個要將眾多門市依地區畫分的需求

  • 地區有階層關係(e.g. 台北市>大安區)
  • 同一地區若超過 N 間門市時則要再往下拆分一階層 (e.g. 大安區1、大安區2)

定義地區 interface

/// <summary>
/// 定義地區 interface
/// </summary>
public interface IDistrict
{
    string Name { get; }

    void Add(IDistrict district);
    void Remove(IDistrict district);
    void Display(int depth);
}

地區實作

/// <summary>
/// 地區實作
/// </summary>
public class District : IDistrict
{
    private readonly List<IDistrict> _children = new List<IDistrict>();
    public string Name { get; }

    public District(string name)
    {
        Name = name;
    }

    public void Add(IDistrict district)
    {
        _children.Add(district);
    }

    public void Remove(IDistrict district)
    {
        _children.Remove(district);
    }

    public void Display(int depth)
    {
        var itemName = depth > 0 ? new string('+', depth) + " " + Name : Name;
        Console.WriteLine(itemName);

        foreach (IDistrict district in _children)
        {
            district.Display(depth + 3);
        }
    }
}

門市實作

/// <summary>
/// 門市實作
/// </summary>
public class Store : IDistrict
{
    public string Name { get; }

    public Store(string name)
    {
        Name = name;
    }

    public void Add(IDistrict district)
    {
        Console.WriteLine("門市無法新增子階層");
    }

    public void Remove(IDistrict district)
    {
        Console.WriteLine("門市無法移除子階層");
    }

    public void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + " " + Name);
    }
}
  1. 建立地區/門市樹狀結構rootDistrict
  2. 於樹狀結構中新增/移除地區district & 門市store
static void Main(string[] args)
{
    // 樹結構
    // 建立根地區
    Situation.District rootDistrict = new Situation.District("台北市");

    // 建立子地區
    Situation.District districtDaan = new Situation.District("大安區");
    // 建立大安區1
    Situation.District districtDaan1 = new Situation.District("大安區 1");
    districtDaan1.Add(new Situation.Store("門市 A"));
    districtDaan1.Add(new Situation.Store("門市 B"));
    // 建立大安區2
    Situation.District districtDaan2 = new Situation.District("大安區 2");
    districtDaan2.Add(new Situation.Store("門市 C"));
    districtDaan2.Add(new Situation.Store("門市 D"));

    // 大安區1 & 大安區2 加入 大安區
    districtDaan.Add(districtDaan1);
    districtDaan.Add(districtDaan2);

    // 大安區 加入 台北市
    rootDistrict.Add(districtDaan);

    rootDistrict.Display(0);

    Console.ReadLine();
}

執行結果

台北市
+++ 大安區
++++++ 大安區 1
--------- 門市 A
--------- 門市 B
++++++ 大安區 2
--------- 門市 C
--------- 門市 D

完整程式碼

GitHub:Structural_03_Composite


總結

範例中主要展示了樹結構部分,實作中可以將所需邏輯加入到容器(Composite) & 實體物件(Leaf),像是門市每日營業額可以計算之後在容器節點作統計,這樣我們就可以知道每個地區當日營業額是多少。


參考資料

  1. Design Patterns
  2. 大話設計模式
  3. dofactory
  4. Refactoring.Guru

新手上路,若有錯誤還請告知,謝謝


#designpattern #CSharp







Related Posts

[心得] 滑鼠們2 - Logitech

[心得] 滑鼠們2 - Logitech

EDA工作站建置(CENTOS 8)

EDA工作站建置(CENTOS 8)

使用 Golang 打造 Discord 機器人 (二)

使用 Golang 打造 Discord 機器人 (二)


Comments