設計模式的征途—9.組合(Composite)模式

Edison Chou發表於2017-06-26

樹形結構在軟體中隨處可見,比如作業系統中的目錄結構,公司組織結構等等,如何運用物件導向的方式來處理這種樹形結構是組合模式需要解決的問題。組合模式通過一種巧妙的設計方案來使得使用者可以一致性地處理整個樹形結構或者樹形結構的一部分,也可以一致地處理樹形結構中的葉子節點(不包含子節點的節點)和容器節點(包含子節點的節點),本次我們就將學習一下用來處理樹形結構的組合模式。

組合模式(Composite) 學習難度:★★★☆☆ 使用頻率:★★★★☆

一、防毒軟體的框架設計

1.1 需求介紹

M公司開發部想要開發一個防毒軟體,該軟體既可以針對某個資料夾防毒,也可以針對某個指定的檔案進行防毒。該防毒軟體還可以根據各類檔案的特點,為不同型別的檔案提供不同的防毒方式,例如影象檔案(ImageFile)和文字檔案(TextFile)的防毒方式就有所差異。現需要提供該防毒軟體的整體框架設計方案。

  首先,我們來了解一下Windows作業系統中的目錄結構:

  

1.2 初始設計

  M公司程式猿們通過分析,決定使用物件導向的方式來實現對檔案和資料夾的操作,定義了影象檔案類ImageFile、文字檔案類TextFile和資料夾類Folder,程式碼如下:

  (1)檔案類:

    public class ImageFile
    {
        private string name;

        public ImageFile(string name)
        {
            this.name = name;
        }

        public void KillVirus()
        {
            // 此處模擬防毒操作
            Console.WriteLine("---- 對影象檔案‘{0}’進行防毒", name);
        }
    }

    public class TextFile
    {
        private string name;

        public TextFile(string name)
        {
            this.name = name;
        }

        public void KillVirus()
        {
            // 此處模擬防毒操作
            Console.WriteLine("---- 對文字檔案‘{0}’進行防毒", name);
        }
    }

  (2)資料夾類:

    public class Folder
    {
        private string name;

        private IList<Folder> folderList = new List<Folder>();
        private IList<ImageFile> imageList = new List<ImageFile>();
        private IList<TextFile> textList = new List<TextFile>();

        public Folder(string name)
        {
            this.name = name;
        }

        public void AddFolder(Folder f)
        {
            folderList.Add(f);
        }

        public void AddImageFile(ImageFile image)
        {
            imageList.Add(image);
        }

        public void AddTextFile(TextFile text)
        {
            textList.Add(text);
        }

        // 需要提供3個不同的方法 RemoveFolder, RemoveImageFile, RemoveTextFile來刪除成員,程式碼省略

        // 需要提供3個不同的方法 GetChildFolder(int i), GetChildImageFile(int i), GetChildTextFile(int i)來獲取成員,程式碼省略

        public void KillVirus()
        {
            Console.WriteLine("**** 對資料夾'{0}'進行防毒", name);

            foreach (var item in folderList)
            {
                item.KillVirus();
            }

            foreach (var item in imageList)
            {
                item.KillVirus();
            }

            foreach (var item in textList)
            {
                item.KillVirus();
            }
        }
    }

  (3)客戶端呼叫:

     public class Program
     {
        public static void Main()
        {
            Folder folder1 = new Folder("EDC的資料");
            Folder folder2 = new Folder("影象檔案");
            Folder folder3 = new Folder("文字檔案");

            ImageFile image1 = new ImageFile("小龍女.jpg");
            ImageFile image2 = new ImageFile("張無忌.gif");

            TextFile text1 = new TextFile("九陰真經.txt");
            TextFile text2 = new TextFile("葵花寶典.doc");

            folder2.AddImageFile(image1);
            folder2.AddImageFile(image2);

            folder3.AddTextFile(text1);
            folder3.AddTextFile(text2);

            folder1.AddFolder(folder2);
            folder1.AddFolder(folder3);

            folder1.KillVirus();
        }
    }

  執行結果如下圖所示:

  

  雖然程式設計師們“成功”實現了這個軟體的框架設計,但通過分析,發現存在以下問題:

  (1)檔案類Folder的設計和實現很複雜,需要定義多個集合儲存不同型別的成員,存在大量的冗餘程式碼,系統維護較為困難。

  (2)系統沒有提供抽象層,客戶端程式碼必須有區別地對待充當容器的資料夾Folder和充當葉子的ImageFile和TextFile,無法統一對它們進行處理。

  (3)系統的靈活性和可擴充套件性差,如果需要增加新的型別的葉子和容器都需要對原有程式碼進行修改。

二、組合模式簡介

2.1 模式概述

組合(Composite)模式:組合多個物件形成樹形結構以表示具有“整體-部分”關係的層次結構。組合模式對單個物件(即葉子物件)和組合物件(即容器物件)的使用具有一致性,組合模式又可以稱為“部分-整體”(Part-Whole)模式,它是一種物件結構型模式。  

2.2 結構圖

  在組合模式中引入了抽象構件類Component,它是所有容器類和葉子類的公共父類,客戶端針對Component進行程式設計。組合模式結構如下圖所示:

  組合模式包含以下幾個角色:

  (1)Component(抽象構件):它是介面或抽象類,為葉子構件和容器構件物件宣告介面,在該角色中可以包含所有子類共有行為的宣告和實現。在抽象構件中定義了訪問及管理它的子構件的方法,例如增加子構件、刪除子構件、獲取子構件等。

  (2)Leaf(葉子構件):它在組合模式中表示葉子結點物件,葉子結點沒有子節點,它實現了在抽象構件中定義的行為。

  (3)Composite(容器構件):它在組合模式中表示容器節點物件,容器節點包含子節點,其子節點可以使葉子結點,也可以是容器節點,它提供一個集合用於儲存子節點,實現了在抽象構件中定義的行為。

三、重構防毒軟體框架設計

3.1 重構後的設計結構

  其中,AbstractFile充當抽象構件類,Folder充當容器類,ImageFile、TextFile以及VideoFile充當葉子構件類。

3.2 重構後的程式碼實現

  (1)抽象構件:AbstractFile

    /// <summary>
    ///  抽象檔案類:抽象構件
    /// </summary>
    public abstract class AbstractFile
    {
        public abstract void Add(AbstractFile file);
        public abstract void Remove(AbstractFile file);
        public abstract AbstractFile GetChild(int index);
        public abstract void KillVirus();
    }

  (2)葉子構件:ImageFile、VideoFile、TextFile類

    /// <summary>
    /// 葉子構件:影象檔案、文字檔案 和 視訊檔案
    /// </summary>
    public class ImageFile : AbstractFile
    {
        private string name;

        public ImageFile(string name)
        {
            this.name = name;
        }

        public override void Add(AbstractFile file)
        {
            Console.WriteLine("對不起,系統不支援該方法!");
        }

        public override void Remove(AbstractFile file)
        {
            Console.WriteLine("對不起,系統不支援該方法!");
        }

        public override AbstractFile GetChild(int index)
        {
            Console.WriteLine("對不起,系統不支援該方法!");
            return null;
        }

        public override void KillVirus()
        {
            // 此處模擬防毒操作
            Console.WriteLine("**** 對影象檔案‘{0}’進行防毒", name);
        }
    }

    public class TextFile : AbstractFile
    {
        private string name;

        public TextFile(string name)
        {
            this.name = name;
        }

        public override void Add(AbstractFile file)
        {
            Console.WriteLine("對不起,系統不支援該方法!");
        }

        public override void Remove(AbstractFile file)
        {
            Console.WriteLine("對不起,系統不支援該方法!");
        }

        public override AbstractFile GetChild(int index)
        {
            Console.WriteLine("對不起,系統不支援該方法!");
            return null;
        }

        public override void KillVirus()
        {
            // 此處模擬防毒操作
            Console.WriteLine("**** 對文字檔案‘{0}’進行防毒", name);
        }
    }

    public class VideoFile : AbstractFile
    {
        private string name;

        public VideoFile(string name)
        {
            this.name = name;
        }

        public override void Add(AbstractFile file)
        {
            Console.WriteLine("對不起,系統不支援該方法!");
        }

        public override void Remove(AbstractFile file)
        {
            Console.WriteLine("對不起,系統不支援該方法!");
        }

        public override AbstractFile GetChild(int index)
        {
            Console.WriteLine("對不起,系統不支援該方法!");
            return null;
        }

        public override void KillVirus()
        {
            // 此處模擬防毒操作
            Console.WriteLine("**** 對視訊檔案‘{0}’進行防毒", name);
        }
    }

  (3)容器構件:Folder

    /// <summary>
    /// 資料夾類:容器構件
    /// </summary>
    public class Folder : AbstractFile
    {
        private IList<AbstractFile> fileList = new List<AbstractFile>();
        private string name;

        public Folder(string name)
        {
            this.name = name;
        }

        public override void Add(AbstractFile file)
        {
            fileList.Add(file);
        }

        public override void Remove(AbstractFile file)
        {
            fileList.Remove(file);
        }

        public override AbstractFile GetChild(int index)
        {
            return fileList[index];
        }

        public override void KillVirus()
        {
            // 此處模擬防毒操作
            Console.WriteLine("---- 對資料夾‘{0}’進行防毒", name);

            foreach (var item in fileList)
            {
                item.KillVirus();
            }
        }
    }

  (4)客戶端呼叫

    public class Program
    {
        public static void Main()
        {
            AbstractFile folder1 = new Folder("EDC的資料");
            AbstractFile folder2 = new Folder("影象檔案");
            AbstractFile folder3 = new Folder("文字檔案");
            AbstractFile folder4 = new Folder("視訊檔案");

            AbstractFile image1 = new ImageFile("小龍女.jpg");
            AbstractFile image2 = new ImageFile("張無忌.gif");

            AbstractFile text1 = new TextFile("九陰真經.txt");
            AbstractFile text2 = new TextFile("葵花寶典.doc");

            AbstractFile video1 = new VideoFile("笑傲江湖.rmvb");
            AbstractFile video2 = new VideoFile("天龍八部.mp4");

            folder2.Add(image1);
            folder2.Add(image2);

            folder3.Add(text1);
            folder3.Add(text2);

            folder4.Add(video1);
            folder4.Add(video2);

            folder1.Add(folder2);
            folder1.Add(folder3);
            folder1.Add(folder4);

            folder1.KillVirus();
        }
    }

  執行結果如下圖所示:

  

  如果只需更換操作節點,例如只需要針對資料夾“文字檔案”進行防毒,只需要修改客戶端程式碼如下:

    //folder1.KillVirus();
    folder3.KillVirus();

  執行結果如下圖所示:

  

  在具體實現時,可以建立圖形介面讓使用者自己選擇所需操作的根節點,無需修改原始碼,符合開閉原則,客戶端無須關心節點的層次結構,可以對所選節點進行統一處理,提高系統的靈活性。

四、組合模式小結

4.1 主要優點

  (1)可以清楚地定義分層次的複雜物件,表示物件的全部或部分層次,使客戶忽略了層次的差異,方便對整個層次結構進行控制。

  (2)增加新的容器構件和葉子構件都十分方便,無需對現有類庫程式碼進行任何修改,符合開閉原則

  (3)為樹形結構的物件導向實現提供了靈活地解決方案,可以形成複雜的樹形結構,但對樹形結構的控制卻很簡單

4.2 主要缺點

  增加新構件時很難對容器中的構建型別進行限制。

4.3 適用場景

  (1)在具有整體和部分的層次結構中,希望通過一種方式忽略整體與部分的差異,客戶端可以一致地對待他們。

  (2)在一個使用面嚮物件語言開發的系統中需要處理一個樹形結構。

參考資料

  DesignPattern

  劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》

 

相關文章