二十三种设计模式[8] - 组合模式(Composite Pattern)

前言      

组合模式,类结构模式的一种。在《设计模式 - 可复用的面向对象软件》一书中将之描述为“ 将对象组合成树状结构以表示 “部分-整体” 的层次结构,使得用户对单个对象和组合对象的使用具有一致性 ”。

       工作中我们经常会接触到一个对象中包含0个或多个其它对象,而其它对象依然包含0个或多个其它对象,这种结构我们称之为树状结构。组合模式就是通过递归去帮助我们去管理这类树状结构。

结构

Compsite_3

需要角色如下:

  • Component(所有节点的抽象):所有对象(节点)的抽象或接口,用来定义所有节点的行为;
  • Leaf(叶节点):树状结构中的叶节点(没有子节点),继承抽象并实现行为;
  • Composite(根节点):树状结构中的根节点和子树的根节点,叶节点的容器,用来管理子节点;

场景

       最经典的树状结构莫过于操作系统中的文件目录结构。我们都知道在一个文件夹中会包含0个或多个文件,而这些文件中又会包含0个或多个文件。如下。

Compsite_

       在设计它的结构时往往会增加一个文件夹类,并根据文件夹内的文件类型维护相应的List去存储,以此类推。也就是说在文件夹类中,我们会根据不同的类型去创建不同的List,每当文件夹支持新的类型时我们都要去修改这个文件夹类,并不符合开闭原则。而且随着文件夹类支持的类型越多,这个类也将变得越来越复杂。

       使用组合模式使得我们在编码过程中不必过分关注各个文件的类型(只要是一个文件),并通过递归来简化文件夹类的设计。如下。

Compsite_2

示例

Compsite_4

public interface IFile
{
    IFile Father { set; get; }
    bool IsFolder { get; }
    string ShowMyself();
    IFile GetChild(int index);
    void Add(IFile obj);
    void Remove(IFile obj);
}

public class Txt : IFile
{
    public bool IsFolder => false;
    public string Name { set; get; } = string.Empty;
    public IFile Father { set; get; }

    public Txt(string name)
    {
        this.Name = name;
    }

    public string ShowMyself()
    {
        string spec = string.Empty;
        IFile father = this.Father;
        while (father != null)
        {
            spec += "  ";
            father = father.Father;
        }
        return $"{spec + this.Name}.txt";
    }

    public IFile GetChild(int index)
    {
        throw new NotImplementedException("Sorry,I have not Child");
    }

    public void Add(IFile obj)
    {
        throw new NotImplementedException("Sorry,I have not Child");
    }

    public void Remove(IFile obj)
    {
        throw new NotImplementedException("Sorry,I have not Child");
    }
}

public class Png : IFile
{
    public bool IsFolder => false;
    public string Name { set; get; } = string.Empty;
    public IFile Father { set; get; }

    public Png(string name)
    {
        this.Name = name;
    }

    public string ShowMyself()
    {
        string spec = string.Empty;
        IFile father = this.Father;
        while (father != null)
        {
            spec += "  ";
            father = father.Father;
        }
        return $"{spec + this.Name}.png";
    }

    public IFile GetChild(int index)
    {
        throw new NotImplementedException("Sorry,I have not child");
    }

    public void Add(IFile obj)
    {
        throw new NotImplementedException("Sorry,I have not child");
    }

    public void Remove(IFile obj)
    {
        throw new NotImplementedException("Sorry,I have not child");
    }
}

public class Folder : IFile
{
    public bool IsFolder => true;
    public string Name { set; get; } = string.Empty;
    public IFile Father { set; get; }
    private List<IFile> _childList = new List<IFile>();

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

    public string ShowMyself()
    {
        string spec = string.Empty;
        IFile father = this.Father;
        while (father != null)
        {
            spec += "  ";
            father = father.Father;
        }

        string result = spec + this.Name;
        foreach (IFile child in _childList)
        {
            result += Environment.NewLine + child.ShowMyself();
        }

        return result;
    }

    public IFile GetChild(int index)
    {
        if(index >= this._childList.Count)
        {
            throw new Exception("越界");
        }

        return this._childList[index];
    }

    public void Add(IFile obj)
    {
        IFile father = this;
        while(father != null)
        {
            if(object.ReferenceEquals(obj, father))
            {
                throw new Exception("循环引用");
            }

            father = father.Father;
        }

        if(this._childList.Exists(t=> object.ReferenceEquals(t, obj)))
        {
            throw new Exception("子节点已存在");
        }

        obj.Father = this;
        this._childList.Add(obj);
    }

    public void Remove(IFile obj)
    {
        if(obj.Father == null
            || !this._childList.Exists(t=> object.ReferenceEquals(t, obj)))
        {
            throw new Exception("未找到子节点");
        }

        obj.Father = null;
        this._childList.Remove(obj);
    }
}

static void Main(string[] args)
{
    IFile folder = new Folder("我的文档");
    IFile txtFileA = new Txt("新建文本文档A");
    IFile pngFileA = new Png("QQ截图A");
    IFile folderA = new Folder("新建文件夹A");

    if (folder.IsFolder)
    {
        folder.Add(txtFileA);
        folder.Add(pngFileA);
        folder.Add(folderA);
    }

    IFile txtFileB = new Txt("新建文本文档B");
    IFile pngFileB = new Png("QQ截图B");

    if (folderA.IsFolder)
    {
        folderA.Add(txtFileB);
        folderA.Add(pngFileB);
    }
    
    Console.WriteLine(folder.ShowMyself());
    Console.ReadKey();
}

image

       在示例中,IFile接口定义了IFile类型的属性(在C#里,接口中可以定义属性)用来存储父节点,方便结构的向上操作。函数IsFolder用来标识当前对象是否是一个Composite。ShowMyself函数表示各个节点的基本操作,在Composite角色(Folder类)中一般递归调用子节点的ShowMyself函数。Add、Remove以及GetChild函数用来管理子节点。

       注意:管理子节点的操作函数是在组合模式中比较有争议的一个点。我们不难看出,对于叶节点(类Txt、Png)来说管理子节点的操作是没有意义的(因为它们没有子节点)。在IFile接口中声明这些操作能够保证节点的一致性以及结构的透明性,但会使调用者做一些无意义的操作(比如调用Txt类的Add函数)。而在Composite角色中定义这些操作虽然能够避免调用者的无意义操作,但会使节点的透明性和一致性降低。

       由于组合模式更加强调各个节点的一致性以及通明性,这里更加推荐在接口中定义那些管理子节点的函数。

       为了减少叶节点重复的实现这些对它无意义的子节点管理函数,可以使用适配器模式 (Adapter)对IFile接口做一个适配,为函数提供一个缺省的实现并使所有叶节点继承这个适配器。或者将IFile声明为一个抽象类并为函数提供缺省的实现。

总结

       在组合模式中,通过定义节点的公共接口提高结构的一致性以及透明性,并通过递归来简化类的设计。在我们对结构进行扩展时,只需要增加接口的实现类而无需对现有代码进行改动,符合开闭原则。但在使用的过程中,我们很难实现对各个节点的约束,并且递归的使用使得我们需要花费更多的时间去理解它的层次关系。递归的使用也使得我们需要更加谨慎的处理结构的深度,以免造成内存溢出。

 

       以上,就是我对组合模式的理解,希望对你有所帮助。

       示例源码:https://gitee.com/wxingChen/DesignPatternsPractice

       系列汇总:https://www.cnblogs.com/wxingchen/p/10031592.html

       本文著作权归本人所有,如需转载请标明本文链接(https://www.cnblogs.com/wxingchen/p/10078594.html)

posted @ 2018-12-06 19:13  王兴Chen  阅读(1232)  评论(0编辑  收藏  举报