树形结构在软件中随处可见,比如操作系统中的目录结构,公司组织结构等等,如何运用面向对象的方式来处理这种树形结构是组合模式需要解决的问题。组合模式通过一种巧妙的设计方案来使得用户可以一致性地处理整个树形结构或者树形结构的一部分,也可以一致地处理树形结构中的叶子节点(不包含子节点的节点)和容器节点(包含子节点的节点),本次我们就将学习一下用来处理树形结构的组合模式。
组合模式(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)在一个使用面向对象语言开发的系统中需要处理一个树形结构。
参考资料
刘伟,《设计模式的艺术—软件开发人员内功修炼之道》