一起学习设计模式--09.组合模式
模式目标
树形结构的处理
前言
树形结构在软件中随处可见,例如操作系统中的目录结构、应用软件中的菜单、办公系统中的公司组织结构等。如何运用面向对象的方式来处理这种树形结构是组合模式需要解决的问题。组合模式通过一种巧妙的设计方案使得用户可以一致性的处理整个树形结构或者树形结构的一部分,也可以一致性的处理树形结构中的叶子节点(不包含子节点的节点)和容器节点(包含子节点的节点)。
一、设计杀毒软件的框架结构
A公司打算开发一个杀毒软件,该软件既可以对某个文件夹杀毒,也可以对某个指定的文件进行杀毒。该杀毒软件还可以根据各类文件的特点,为不同类型的文件提供不同的杀毒方式,例如,图像文件和文本文件的杀毒方式就有所差异。现需要提供该杀毒软件的整体框架设计方案。
树形结构示意图:
A公司开发人员通过分析,决定使用面向对象的方式来实现对文件和文件夹的操作,定义了图像文件类 ImageFile、文本文件类 TextFile 和文件夹类 Folder,代码如下:
//图像文件类
public class ImageFile
{
private string _name;
public ImageFile(string name)
{
_name = name;
}
public void KillVirus()
{
//// 此处模拟杀毒操作
Console.WriteLine("---- 对图像文件‘{0}’进行杀毒", _name);
}
}
//文本文件类
public class TextFile
{
private string _name;
public TextFile(string name)
{
_name = name;
}
public void KillVirus()
{
//// 此处模拟杀毒操作
Console.WriteLine("---- 对文本文件‘{0}’进行杀毒", _name);
}
}
//文件夹类
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个不同的方法来删除相应的成员,此处省略
//还需要提供3个不同的方法来获取成员,此处省略
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();
}
}
}
客户端测试代码如下:
class Program
{
static void Main(string[] args)
{
Folder folder1 = new Folder("十一的资料");
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();
}
输出结果如下:
尽管开发人员“成功”实现了杀毒软件的框架设计,但通过仔细分析,发现该设计方案存在以下问题:
- 文件夹类 Folder 的设计和实现都非常复杂,需要定义多个集合存储不同类型的成员,而且需要针对不同的成员提供增加、删除和获取等管理和访问成员的方法,存在大量冗余代码,系统维护较为困难。
- 由于系统没有提供抽象层,客户端代码必须有区别的对待充当容器的文件夹Folder和充当叶子的 ImageFile 和 TextFile ,无法统一对它们进行处理。
- 系统的灵活性和可扩展性差,如果需要增加新的类型的叶子或容器都需要对原有代码进行修改。
二、组合模式概述
1.定义
对于树形结构,当容器对象的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(可以是容器对象,也可以是叶子对象)并调用执行,牵一而动百,其中使用了递归调用的机制来对整个结构进行处理。由于容器对象和叶子对象在功能上的区别,在使用这些对象的代码中必须有区别的对待容器对象和叶子对象,而实际上大多数情况下希望一致的处理它们,因此对于这些对象的区别对待将会使得程序非常复杂。组合模式为解决此类问题而诞生,它可以让叶子对象和容器对象的使用具有一致性。
组合模式定义如下:
组合模式(Composite Pattern):组合多个对象形成树形结构以表示具有“部分—整体”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,又可以称为“部分—整体”(Part-Whole)模式,它是一种对象结构型模式。
2.结构
在组合模式中引入了抽象构件类Component,它是所有容器类和叶子类的公共父类,客户端针对Component进行编程。结构如下:
组合模式中包含以下3个角色:
- Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,例如:添加子构件、删除子构件、获取子构件等。
- Leaf(叶子构件):它在组合模式结构中表示叶子节点对象。叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过捕获异常等方式进行处理。
- Composite(容器构件):它在组合模式结构中表示容器节点对象。容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点。它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
3.关键
组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,也可以代表容器。客户端针对该抽象构件类进行编程,无需知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立了一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。
三、完整解决方案
为了让系统具有更好的灵活性和可扩展性,客户端可以一致性地对待文件和文件夹,A公司开发人员使用组合模式来进行杀毒软件的框架设计,其结构如下:
上图中 AbstractFile 充当抽象构件类,Folder 充当容器构件类,ImageFile、VideoFile、TextFile充当叶子构件类。完整代码如下:
/// <summary>
/// 抽象文件类:抽象构件
/// </summary>
public abstract class AbstractFile
{
public abstract void Add(AbstractFile file);
public abstract void Remove(AbstractFile file);
public abstract AbstractFile GetChild(int i);
public abstract void KillVirus();
}
/// <summary>
/// 图像文件类:叶子构件
/// </summary>
public class ImageFile : AbstractFile
{
private string _name;
public ImageFile(string name)
{
_name = name;
}
public override void Add(AbstractFile file)
{
Console.WriteLine("对不起不支持该方法");
}
public override void Remove(AbstractFile file)
{
Console.WriteLine("对不起不支持该方法");
}
public override AbstractFile GetChild(int i)
{
Console.WriteLine("对不起不支持该方法");
return null;
}
public override void KillVirus()
{
//模拟杀毒
Console.WriteLine("------ 对图像文件'{0}'进行杀毒", _name);
}
}
/// <summary>
/// 文本文件类:叶子构件
/// </summary>
public class TextFile:AbstractFile
{
private string _name;
public TextFile(string name)
{
_name = name;
}
public override void Add(AbstractFile file)
{
Console.WriteLine("对不起不支持该方法");
}
public override void Remove(AbstractFile file)
{
Console.WriteLine("对不起不支持该方法");
}
public override AbstractFile GetChild(int i)
{
Console.WriteLine("对不起不支持该方法");
return null;
}
public override void KillVirus()
{
//模拟杀毒
Console.WriteLine("------ 对文本文件'{0}'进行杀毒", _name);
}
}
/// <summary>
/// 视频文件类:叶子构件
/// </summary>
public class VideoFile : AbstractFile
{
private string _name;
public VideoFile(string name)
{
_name = name;
}
public override void Add(AbstractFile file)
{
Console.WriteLine("对不起不支持该方法");
}
public override void Remove(AbstractFile file)
{
Console.WriteLine("对不起不支持该方法");
}
public override AbstractFile GetChild(int i)
{
Console.WriteLine("对不起不支持该方法");
return null;
}
public override void KillVirus()
{
//模拟杀毒
Console.WriteLine("------ 对视频文件'{0}'进行杀毒", _name);
}
}
/// <summary>
/// 文件夹类:容器构件
/// </summary>
public class Folder : AbstractFile
{
/// <summary>
/// 定义集合 _fileList,用于存储AbstractFile类型的成员
/// </summary>
private List<AbstractFile> _fileList = new List<AbstractFile>();
private string _name;
public Folder(string name)
{
_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 i)
{
return _fileList[i];
}
public override void KillVirus()
{
//模拟杀毒
Console.WriteLine("****** 对文件夹'{0}'进行杀毒", _name);
//递归调用成员构件的KillVirus()方法
_fileList.ForEach(file =>
{
file.KillVirus();
});
}
}
客户端测试代码:
class Program
{
static void Main(string[] args)
{
var folder1 = new V2.Folder("十一的资料");
var folder2 = new V2.Folder("图像文件");
var folder3 = new V2.Folder("视频文件");
var folder4 = new V2.Folder("文本文件");
var image1 = new V2.ImageFile("小龙女.jpg");
var image2 = new V2.ImageFile("张无忌.gif");
var text1 = new V2.TextFile("降龙十八掌.txt");
var text2 = new V2.TextFile("乾坤大挪移.doc");
var video1 = new V2.TextFile("天龙八部.mp4");
var video2 = new V2.TextFile("笑傲江湖.rmvb");
folder2.Add(image1);
folder2.Add(image2);
folder3.Add(video1);
folder3.Add(video2);
folder4.Add(text1);
folder4.Add(text2);
folder1.Add(folder2);
folder1.Add(folder3);
folder1.Add(folder4);
//从“十一的资料”节点开始进行杀毒操作
folder1.KillVirus();
}
}
编译并运行输出结果:
如果需要更换操作节点,例如只需要对文件夹“文本文件”进行杀毒,客户端代码只需要修改一行即可:
//folder1.KillVirus();
folder4.KillVirus();
执行结果如下:
在具体实现时,可以创建图形化界面让用户选择所需要操作的根节点,无需修改源代码,符合开闭原则。客户端无需关心节点的层次结构,可以对所选节点进行统一处理,提高系统的灵活性。
四、组合模式总结
组合模式使用面向对象的思想来实现树形结构的构件与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单,灵活性好。
1.主要优点
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次。它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 客户端可以一致的使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
- 在组合模式中增加新的容器构件或叶子构件都很方便,无须对现有类库进行任何修改,符合开闭原则。
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案。通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
2.主要缺点
组合模式的主要缺点是:在增加新构件时很难对容器中的构件类型进行限制。有时希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件。使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自相同的抽象层,在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。
3.使用场景
- 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致性的对待它们。
- 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
- 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,将来需要增加一些新的类型。
如果您觉得这篇文章有帮助到你,欢迎推荐,也欢迎关注我的公众号。
示例代码:
https://github.com/crazyliuxp/DesignPattern.Simples.CSharp