diff --git a/DesignPattern/03 结构型设计模式(下).md b/DesignPattern/03 结构型设计模式(下).md new file mode 100644 index 0000000..2242fbc --- /dev/null +++ b/DesignPattern/03 结构型设计模式(下).md @@ -0,0 +1,1280 @@ + +# 结构型设计模式(下) + +本教程主要介绍一系列用于如何将现有类或对象组合在一起形成更加强大结构的经验总结。 + +**知识结构:** + + +![图1 知识结构](https://img-blog.csdnimg.cn/20201114154539907.png) + +--- +## 组合模式 -- 树形结构的处理 + +Sunny 软件公司欲开发一个杀毒(AntiVirus)软件,该软件既可以对某个文件夹(Folder)杀毒,也可以对某个指定的文件(File)进行杀毒。 + +该杀毒软件还可以根据各类文件的特点,为不同类型的文件提供不同的杀毒方式,例如图像文件(ImageFile)和文本文件(TextFile)的杀毒方式就有所差异。现需要提供该杀毒软件的整体框架设计方案。 + +在介绍 Sunny 公司开发人员提出的初始解决方案之前,我们先来分析一下操作系统中的文件目录结构: + +
+ +

图2 Windows目录结构

+
+ + +上图可以简化为如下图所示树形目录结构: + +
+ +

图3 树形目录结构示意图

+
+ +在树形目录结构中,包含文件文件夹两类不同的元素,其中在文件夹中可以包含文件,还可以继续包含子文件夹,但是在文件中不能再包含子文件或者子文件夹。 + +称文件夹为容器Container),不同类型的各种文件是其成员,称为叶子Leaf),一个文件夹也可以作为另一个更大的文件夹的成员。 + + +Sunny 软件公司的开发人员通过分析,定义了图像文件类`ImageFile`、文本文件类`TextFile`和文件夹类`Folder`: + + +图像文件类: +```c +public class ImageFile +{ + private readonly string _name; + + public ImageFile(string name) + { + _name = name; + } + + public void KillVirus() + { + Console.WriteLine("----对图像文件'" + _name + "'进行杀毒"); + } +} +``` + +文本文件类: +```c +public class TextFile +{ + private readonly string _name; + public TextFile(string name) + { + _name = name; + } + public void KillVirus() + { + Console.WriteLine("----对文本文件'" + _name + "'进行杀毒"); + } +} +``` + +文件夹类: +```c +public class Folder +{ + private readonly string _name; + private readonly IList _folderList = new List(); + private readonly IList _imageList = new List(); + private readonly IList _textList = new List(); + + public Folder(string name) + { + _name = name; + } + + public void AddFolder(Folder f) + { + _folderList.Add(f); + } + + public void RemoveFolder(Folder f) + { + _folderList.Remove(f); + } + + public void AddImageFile(ImageFile image) + { + _imageList.Add(image); + } + + public void RemoveImageFile(ImageFile image) + { + _imageList.Remove(image); + } + + public void AddTextFile(TextFile text) + { + _textList.Add(text); + } + + public void RemoveTextFile(TextFile text) + { + _textList.Remove(text); + } + + public void KillVirus() + { + Console.WriteLine("****对文件夹'" + _name + "'进行杀毒"); + foreach(Folder f in _folderList) + { + f.KillVirus(); + } + foreach (ImageFile image in _imageList) + { + image.KillVirus(); + } + foreach(TextFile text in _textList) + { + text.KillVirus(); + } + } +} +``` + +客户端代码: +```c +class Program +{ + static void Main(string[] args) + { + Folder folder1 = new Folder("Sunny的资料"); + 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(); + } +} +``` + +输出结果如下图所示: + +
+ +

图4 输出结果

+
+ + +Sunny 公司开发人员“成功”实现了杀毒软件的框架设计,但通过仔细分析,发现该设计方案存在如下问题: +- 文件夹类`Folder`的设计和实现都非常复杂,需要定义多个集合存储不同类型的成员,而且需要针对不同的成员提供增加、删除等管理成员的方法,存在大量的冗余代码,系统维护较为困难; +- 由于系统没有提供抽象层,客户端代码必须有区别地对待充当容器的文件夹`Folder`和充当叶子的`ImageFile`和`TextFile`,无法统一对它们进行处理; +- 系统的灵活性和可扩展性差,如果需要增加新的类型的叶子和容器都需要对原有代码进行修改,例如在系统中增加一种新类型的视频文件`VideoFile`,则必须修改`Folder`类的源码,否则无法在文件夹中添加视频文件。 + +Sunny 公司的开发人员该如何解决这些问题呢? + + +为了让系统具有更好的灵活性和可扩展性,客户端可以一致地对待文件和文件夹,Sunny 公司开发人员重构了软件框架设计,其基本结构如下图所示: + +
+ +

图5 杀毒软件框架设计结构图

+
+ +抽象文件类: +```c +public abstract class AbstractFile +{ + public abstract void Add(AbstractFile file); + public abstract void Remove(AbstractFile file); + public abstract void KillVirus(); +} +``` + +文件夹类: +```c +public class Folder : AbstractFile +{ + private readonly string _name; + private readonly IList _fileList = new List(); + + 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 void KillVirus() + { + Console.WriteLine("****对文件夹'" + _name + "'进行杀毒"); + foreach (AbstractFile f in _fileList) + { + f.KillVirus(); + } + } +} +``` + +图像文件类: +```c +public class ImageFile : AbstractFile +{ + private readonly 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 void KillVirus() + { + Console.WriteLine("----对图像文件'" + _name + "'进行杀毒"); + } +} +``` + +文本文件类: +```c +public class TextFile : AbstractFile +{ + private readonly 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 void KillVirus() + { + Console.WriteLine("----对文本文件'" + _name + "'进行杀毒"); + } +} +``` + +视频文件类: +```c +public class VideoFile : AbstractFile +{ + private readonly 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 void KillVirus() + { + Console.WriteLine("----对视频文件'" + _name + "'进行杀毒"); + } +} +``` + +客户端代码: +```c +class Program +{ + static void Main(string[] args) + { + AbstractFile folder1 = new Folder("Sunny的资料"); + AbstractFile folder2 = new Folder("图像文件"); + AbstractFile folder3 = new Folder("文本文件"); + AbstractFile folder4 = new Folder("视频文件"); + + AbstractFile file1 = new ImageFile("小龙女.jpg"); + AbstractFile file2 = new ImageFile("张无忌.gif"); + + AbstractFile file3 = new TextFile("九阴真经.txt"); + AbstractFile file4 = new TextFile("葵花宝典.doc"); + + AbstractFile file5 = new VideoFile("笑傲江湖.rmvb"); + + folder2.Add(file1); + folder2.Add(file2); + folder3.Add(file3); + folder3.Add(file4); + folder4.Add(file5); + + folder1.Add(folder2); + folder1.Add(folder3); + folder1.Add(folder4); + + folder1.KillVirus(); + } +} +``` + + +输出结果如下图所示: + +
+ +

图6 输出结果

+
+ +由于在抽象构件类中声明了所有方法,因此在`ImageFile`等叶子构件类中实现这些方法时必须进行相应的异常处理或错误提示。在容器构件类`Folder`的`killVirus()`方法中将递归调用其成员对象的`killVirus()`方法,从而实现对整个树形结构的遍历。 + + +如果需要更换操作节点,客户端代码只需修改一行即可,将代码: + +```c +folder1.killVirus(); +``` + +改为: + +```c +folder3.killVirus(); +``` + +输出结果如下: + +
+ +

图7 输出结果

+
+ +在具体实现时,我们可以创建图形化界面让用户选择所需操作的根节点,无须修改源代码,符合“开闭原则”,客户端无须关心节点的层次结构,可以对所选节点进行统一处理,提高系统的灵活性。 + + + +组合模式 为处理树形结构提供了一种较为完美的解决方案,它描述了如何将容器和叶子进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器和叶子。 + + + +> 组合模式Composite Pattern): +> 组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性。 + + +组合模式 引入了抽象构件类`Component`,它是所有容器类和叶子类的父类,客户端针对`Component`编程。组合模式结构如下图所示: + +
+ +
+ +

图8 组合模式类图

+
+ + +- `Component`(抽象构件):为叶子和容器对象声明接口,定义了访问及管理它的子对象的方法。 +- `Leaf`(叶子):表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子对象的方法,可以通过异常等方式进行处理。 +- `Composite`(容器):表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子对象的方法,在其业务方法中可以递归调用其子节点的业务方法。 + + +抽象构件: +```c +public abstract class Component +{ + public abstract void Add(Component c); + public abstract void Remove(Component c); + public abstract Component GetChild(int i); + public abstract void Operation(); +} +``` + +叶子类: +```c +public class Leaf : Component +{ + public override void Add(Component c) + { + //异常处理或错误提示 + } + public override void Remove(Component c) + { + //异常处理或错误提示 + } + public override Component GetChild(int i) + { + //异常处理或错误提示 + return null; + } + + public override void Operation() + { + //叶子构建具体业务方法的实现 + } +} +``` + +容器类: +```c +public class Composite : Component +{ + private readonly IList _children = new List(); + + public override void Add(Component c) + { + _children.Add(c); + } + public override void Remove(Component c) + { + _children.Remove(c); + } + public override Component GetChild(int i) + { + return _children[i]; + } + + public override void Operation() + { + foreach (Component c in _children) + { + c.Operation(); + } + } +} +``` + +
+ +组合模式 的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。 + +同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。 + + +**组合模式分类:** + +通过引入组合模式Sunny 公司设计的杀毒软件具有良好的可扩展性,在增加新的文件类型时,无须修改现有代码,只需增加一个新的文件类作为`AbstractFile`类的子类即可,但是由于在该类中声明了大量用于管理和访问子对象的方法,例如,`Add()`、`Remove()`等,我们不得不在新增的文件类中实现这些方法,提供对应的错误提示和异常处理。为了简化代码,我们有以下两个解决方案: + + +解决方案一: + +将叶子对象的`Add()`、`Remove()`等方法的实现代码移至`AbstractFile`类中,由`AbstractFile`提供统一的默认实现,代码如下: + +```c +public abstract class AbstractFile +{ + public void virtual Add(AbstractFile file) + { + Console.WriteLine("对不起,不支持该方法!"); + } + + public void virtual Remove(AbstractFile file) + { + Console.WriteLine("对不起,不支持该方法!"); + } + public abstract void KillVirus(); +} +``` + + +解决方案二: + +在抽象构件`AbstractFile`中不声明任何用于访问和管理成员构件的方法,代码如下所示: + +```c +public abstract class AbstractFile +{ + public abstract void KillVirus(); +} +``` + + +无论客户端如何定义叶子对象都无法调用到这些方法,不需要做任何错误和异常处理,容器对象再根据需要增加访问和管理成员的方法,但这时候也存在一个问题:客户端不得不使用容器类本身来声明容器对象,否则无法访问其中新增的`Add()`、`Remove()`等方法,客户端代码片段如下所示: + +```c +static void Main(string[] args) +{ + + Folder folder1 = new Folder("Sunny的资料"); + Folder folder2 = new Folder("图像文件"); + Folder folder3 = new Folder("文本文件"); + Folder folder4 = new Folder("视频文件"); + + AbstractFile file1 = new ImageFile("小龙女.jpg"); + AbstractFile file2 = new ImageFile("张无忌.gif"); +} +``` + +【1】透明组合模式 + +
+ +

图9 透明组合模式结构图

+
+ +透明组合模式 的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的。 + +叶子对象不可能有下一个层次的对象,因此为其提供`Add()`、`Remove()`以及`GetChild()`等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)。 + + +【2】 安全组合模式 + +
+ +

图10 安全组合模式结构图

+
+ +安全组合模式 的缺点是不够透明,因为叶子对象和容器对象具有不同的方法,且容器对象中那些用于管理子对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子对象和容器对象。 + + + + + +--- +## 装饰模式 -- 扩展系统功能 + +Sunny 软件公司基于面向对象技术开发了一套图形界面组件库,该组件库提供了大量的基本组件,如: + +- 窗体(Window) +- 文本框(TextBox) +- 列表框(ListBox) +- …… + +在使用该组件库时,用户经常要求定制一些特效显示效果,如: +- 带滚动条的窗体(ScrollBarWindow) +- 带黑色边框的文本框(BlackBorderTextBox) +- 既带滚动条又带黑色边框的列表框(ScrollBarAndBlackBorderListBox) +- …… + + + +因此经常需要对该组件库进行扩展以增强其功能,如下图所示: + +
+ +
+ +

图11 带滚动条的窗体示意图

+
+ +如何提高图形界面组件库的可扩展性并降低其维护成本是 Sunny 公司开发人员必须面对的一个问题。 + + +Sunny 软件公司的开发人员针对上述要求,提出了一个基于继承复用的初始设计方案,其基本结构如下图所示: + +
+ +

图12 图形界面组件库初始设计方案

+
+ + +仔细分析该设计方案,我们不难发现存在如下几个问题: + +【1】系统扩展麻烦,在某些编程语言中无法实现。 +- 现在大多数面向对象语言不支持多继承,无法通过继承来实现对来自多个父类的方法的重用。 +- 例如,增加一个透明窗体类`TransparentWindow`,它是`Window`类的子类,现在需要一个同时拥有三项功能(带滚动条、带黑色边框、透明)的窗体,则必须增加一个类作为这三个窗体类的子类,C# 或 Java 语言无法实现。 + +【2】代码重复。由于窗体、文本框、列表框等都需要设置滚动条,因此在`ScrollBarWindow`、`ScrollBarTextBox`和`ScrollBarListBox`等类中都需要包含`SetScrollBar()`方法,该方法的具体实现过程基本相同,代码重复,不利于对系统进行修改和维护。 + + +【3】系统庞大,类的数目繁多。 +- 3 种基本控件和 2 种扩展方式需要定义 9 个具体类; +- 如果再增加 1 个基本控件还需要增加 3 个实体类; +- 如果存在 3 种扩展方式,对于每一个控件而言,需要增加 7 个实体类。 + + +根据“合成复用原则”,在实现功能复用时,要多用关联,少用继承。 +> 将`SetScrollBar()`方法抽取出来,封装在一个独立的类中,在这个类中定义一个`Component`类型的对象,通过调用`Component`的`Display()`方法来显示最基本的组件,同时再通过`SetScrollBar()`方法对基本构件的功能进行增强。 + + + +根据“里氏代换原则”,使用子类的地方都可以透明的使用父类。 +> `Window`、`ListBox`和`TextBox`都是`Component`的子类,程序在运行时,我们只要向这个独立的类中注入具体的`Component`子类的对象即可实现功能的扩展。 + +这个独立的类一般称为装饰器(Decorator),它的作用就是对已有对象的功能进行扩展,以获得更加符合用户需求的对象。 + + +Sunny 公司开发人员重构了该界面组件库,基本结构如下图所示: + +
+ +

图13 图形界面组件库结构图

+
+ + +抽象组件类: +```c +public abstract class Component +{ + public abstract void Display(); +} +``` + +具体组件类: +```c +public class Window : Component +{ + public override void Display() + { + Console.WriteLine("显示窗体。"); + } +} + +public class ListBox : Component +{ + public override void Display() + { + Console.WriteLine("显示列表框。"); + } +} + +public class TextBox : Component +{ + public override void Display() + { + Console.WriteLine("显示文本框。"); + } +} +``` + +装饰器类: +```c +public class ComponentDecorator : Component +{ + private readonly Component _component; + + public ComponentDecorator(Component component) + { + _component = component; + } + + public override void Display() + { + _component.Display(); + } +} +``` + +具体装饰类: +```c +public class ScrollBarDecorator : ComponentDecorator +{ + public ScrollBarDecorator(Component component) : base(component) + { + } + + public void SetScrollBar() + { + Console.WriteLine("为组件增加滚动条。"); + } + + public override void Display() + { + base.Display(); + SetScrollBar(); + } +} + +public class BlackBorderDecorator : ComponentDecorator +{ + public BlackBorderDecorator(Component component) : base(component) + { + } + + public void SetBlackBorder() + { + Console.WriteLine("为组件增加黑色边框。"); + } + + public override void Display() + { + base.Display(); + SetBlackBorder(); + } +} +``` + +客户端: +```c +class Program +{ + static void Main(string[] args) + { + Component component = new Window(); + Component componentSb = new ScrollBarDecorator(component); + componentSb.Display(); + } +} +``` + + +
+ +

图14 运行结果

+
+ +客户端: + +```c +class Program +{ + static void Main(string[] args) + { + Component component = new Window(); + Component componentSb = new ScrollBarDecorator(component); + Component componentBb = new BlackBorderDecorator(componentSb); + componentBb.Display(); + } +} +``` + +
+ +

图15 运行结果

+
+ + +装饰模式 是一种用于替代继承的技术,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。在 装饰模式 中引入了装饰类,在装饰类中既可调用待装饰的原有类的方法,又可增加新的方法,以扩充原有类的功能。 + + + +> 装饰模式Decorator Pattern): +> 动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。 + + + +在 装饰模式 中通常定义一个装饰类,通过子类对原有组件进行装饰,其结构如下图所示: + +
+ +

图16 装饰模式类图

+
+ + +```c +public abstract class Component +{ + public abstract void Operation(); +} +``` + + +`Component`(抽象组件类):它是`ConcreteComponent`和`Decorator`的共同父类,声明了在`ConcreteComponent`中实现的业务方法。它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。 + + +```c +public class ConcreteComponent : Component +{ + public override void Operation() + { + Console.WriteLine("具体对象的操作。"); + } +} +``` + + +`ConcreteComponent`(具体组件类):它是抽象组件类的子类,用于定义具体的组件对象,实现了在抽象组件中声明的方法,装饰器可以给它增加额外的职责(方法)。 + + +```c +public class Decorator : Component +{ + private readonly Component _component; + + public Decorator(Component component) + { + _component = component; + } + + public override void Operation() + { + if (_component != null) + { + _component.Operation(); + } + } +} +``` + +`Decorator`(装饰类):它没有真正实施装饰,只是维护一个指向抽象组件对象的引用,通过该引用可以调用装饰之前组件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。 + + +```c +public class ConcreteDecoratorA : Decorator +{ + private string _addedState; + + public ConcreteDecoratorA(Component component) : base(component) + { + } + + public override void Operation() + { + base.Operation(); + _addedState = "New State"; + Console.WriteLine("具体装饰对象A的操作"); + } +} + +public class ConcreteDecoratorB : Decorator +{ + public ConcreteDecoratorB(Component component) : base(component) + { + } + + public void AddedBehavior() + { + } + + public override void Operation() + { + base.Operation(); + AddedBehavior(); + Console.WriteLine("具体装饰对象B的操作。"); + } +} +``` + +`ConcreteDecorator`(具体装饰类):由于在`Decorator`中注入的是`Component`类型的对象,可以将一个具体组件对象注入其中,再通过具体装饰类来进行装饰;也可以将一个已经装饰过的`Decorator`子类的对象再注入其中进行多次装饰,从而对原有功能多次扩展。 + +客户端: + +```c +class Program +{ + static void Main(string[] args) + { + Component c = new ConcreteComponent(); + Component c1 = new ConcreteDecoratorA(c); + Component c2 = new ConcreteDecoratorB(c1); + c2.Operation(); + } +} +``` + +
+ +

图17 输出结果

+
+ + +透明装饰模式与半透明装饰模式 + +装饰模式 存在一个问题:如果客户端希望单独调用实体装饰类新增的方法,而不想通过抽象组件中声明的方法来调用新增方法时将遇到一些麻烦,我们通过一个实例来对这种情况加以说明: + +在 Sunny 软件公司开发的 OA 系统中,采购单(PurchaseRequest)和请假条(LeaveRequest)等文件(Document)对象都具有显示功能,现在要为其增加审批、删除等功能,使用装饰模式进行设计。 + + +可以得到如下图所示结构图: + +
+ +

图18 文件对象功能增加实例结构图

+
+ + +抽象组建类: +```c +public abstract class Document +{ + public abstract void Dispaly(); +} +``` + +具体组建类: + +```c +public class PurchaseRequest : Document +{ + public override void Dispaly() + { + Console.WriteLine("显示采购单。"); + } +} + +public class LeaveRequest : Document +{ + public override void Dispaly() + { + Console.WriteLine("显示请假条。"); + } +} +``` + +装饰器类: +```c +public class Decorator : Document +{ + private readonly Document _document; + public Decorator(Document document) + { + _document = document; + } + public override void Dispaly() + { + _document.Dispaly(); + } +} +``` + +具体装饰器类: +```c +public class Approver : Decorator +{ + public Approver(Document document) : base(document) + { + Console.WriteLine("增加审批功能。"); + } + public void Approve() + { + Console.WriteLine("审批文件"); + } +} + +public class Deleter : Decorator +{ + public Deleter(Document document) : base(document) + { + Console.WriteLine("增加删除功能。"); + } + public void Delete() + { + Console.WriteLine("删除文件。"); + } +} +``` + +客户端: +```c +class Program +{ + static void Main(string[] args) + { + Document doc = new PurchaseRequest(); + Approver newDoc = new Approver(doc); + newDoc.Dispaly(); + newDoc.Approve(); + } +} +``` + +
+ +

图19 输出结果

+
+ +`Document`中没有对`Approve()`方法声明,若`newDoc`使用`Document`类型来定义,将导致客户端无法调用新增业务方法`Approve()`。 + +即:在客户端无法统一对待装饰之前的实体组件对象和装饰之后的组件对象。 + +在实际使用过程中,由于新增行为可能需要单独调用,因此这种形式的 装饰模式 也经常出现,这种 装饰模式 被称为 半透明装饰模式,而标准的 装饰模式透明装饰模式。 + +【1】透明装饰模式 可以让客户端透明地使用装饰之前的对象和装饰之后的对象,无须关心它们的区别,此外,还可以对一个已装饰过的对象进行多次装饰,得到更为复杂、功能更为强大的对象。在实现 透明装饰模式 时,要求实体装饰类的`operation()`方法覆写装饰类的`operation()`方法,除了调用原有对象的`operation()`外还需要调用新增的`AddedBehavior()`方法来增加新行为。 + +【2】半透明装饰模式 对于客户端而言,具体组件类型无须关心,是透明的,但是具体装饰类型必须指定,这是不透明的。其最大的缺点在于不能实现对同一个对象的多次装饰,而且客户端需要有区别地对待装饰之前的对象和装饰之后的对象。在实现半透明的装饰模式时,我们只需在具体装饰类中增加一个独立的`AddedBehavior()`方法来封装相应的业务处理,由于客户端使用具体装饰类型来定义装饰后的对象,因此可以单独调用`AddedBehavior()`方法来扩展系统功能。 + + + +--- +## 代理模式 -- 间接访问真实对象 + + +Sunny 软件公司承接了某信息咨询公司的收费商务信息查询系统的开发任务,该系统的基本需求如下: +- 在进行商务信息查询之前用户需要通过身份验证,只有合法用户才能够使用该查询系统; +- 在进行商务信息查询时系统需要记录查询日志,以便根据查询次数收取查询费用。 + +该软件公司开发人员已完成了商务信息查询模块的开发任务,现希望能够以一种松耦合的方式向原有系统增加身份验证日志记录功能,客户端代码可以无区别地对待原始的商务信息查询模块和增加新功能之后的商务信息查询模块,而且可能在将来还要在该信息查询模块中增加一些新的功能。 + + +通过分析,可以在客户端和信息查询对象之间增加一个代理对象,客户端通过代理对象间接访问具有商务信息查询功能的真实对象,在代理对象中增加身份验证日志记录等功能,而无须直接对原有的商务信息查询对象进行修改,如下图所示: + +
+ +
+ +

图20 商务信息查询系统设计方案示意图

+
+ + + +Sunny 软件公司的开发人员针对上述要求,提出了该商务信息查询系统的设计结构如下图所示: + +
+ +

图21 商务信息查询系统结构图

+
+ + +抽象主题类: +```c +public interface ISearcher +{ + string DoSearch(string userId, string keyword); +} +``` + +真实主题类: +```c +public class RealSearcher : ISearch +{ + public string DoSearch(string userId, string keyword) + { + Console.WriteLine("用户'{0}'使用关键词'{1}'查询商务信息!", userId, keyword); + return "返回具体内容"; + } +} +``` + +权限控制类: +```c +public class AccessValidator +{ + public bool Validate(string userId) + { + Console.WriteLine("在数据库中验证用户'" + userId + "'是否是合法用户?"); + if (userId.Equals("杨过")) + { + Console.WriteLine("'{0}'登录成功!", userId); + return true; + } + Console.WriteLine("'{0}'登录失败!", userId); + return false; + } +} +``` + +日志记录类: +```c +public class Logger +{ + public void Log(string userId) + { + Console.WriteLine("更新数据库,用户'{0}'查询次数加1!", userId); + } +} +``` + +代理主题类: +```c +public class ProxySearcher : ISearcher +{ + private readonly RealSearcher _searcher; + private readonly AccessValidator _validator; + private readonly Logger _logger; + + public ProxySearcher() + { + _searcher = new RealSearcher(); + _validator = new AccessValidator(); + _logger = new Logger(); + } + + public bool Validate(string userId) + { + return _validator.Validate(userId); + } + + public void Log(string userId) + { + _logger.Log(userId); + } + + public string DoSearch(string userId, string keyword) + { + if (Validate(userId)) + { + string result = _searcher.DoSearch(userId, keyword); + Log(userId); + return result; + } + return string.Empty; + } +} +``` + +配置文件: +```c + + + + + + +``` + +客户端: +```c +using System.Configuration; +using System.Reflection; +class Program +{ + static void Main(string[] args) + { + string className = ConfigurationManager.AppSettings["proxy"]; + Assembly assembly = Assembly.Load("SunnyProxy"); + ISearcher searcher = assembly.CreateInstance(className) as ISearcher; + if (searcher != null) + { + string result = searcher.DoSearch("杨过", "helloworld"); + Console.WriteLine(result); + } + } +} +``` + + +
+ +

图22 运行结果

+
+ +在代理类`ProxySearcher`中实现对真实主题类的权限控制和引用计数,如果需要在访问真实主题时增加新的访问控制机制和新功能,只需增加一个新的代理类,再修改配置文件,在客户端代码中使用新增代理类即可,源代码无须修改,符合开闭原则。 + + + +当无法直接访问某个对象或访问某个对象存在困难时可以通过一个代理对象来间接访问,为了保证客户端使用的透明性,所访问的真实对象与代理对象需要实现相同的接口。 + + +> 代理模式Proxy Pattern): +> 给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。 + + +代理模式 结构如下图所示: + +
+ +
+ +

图23 代理模式结构图

+
+ +```c +public abstract class Subject +{ + public abstract void Request(); +} +``` + + +`Subject`(抽象主题角色):它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。 + + +```c +public class RealSubject : Subject +{ + public override void Request() + { + Console.WriteLine("真实的请求!"); + } +} +``` + +`RealSubject`(真实主题角色):它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。 + + +```c +public class Proxy : Subject +{ + private readonly RealSubject _realSubject; + + public Proxy() + { + _realSubject = new RealSubject(); + } + + public void PreRequest() + { + Console.WriteLine("前置条件"); + } + public void PostRequest() + { + Console.WriteLine("后置条件"); + } + public override void Request() + { + PreRequest(); + _realSubject.Request(); + PostRequest(); + } +} +``` + +`Proxy`(代理主题角色):它是抽象主题类的子类,维持一个对真实主题对象的引用,调用在真实主题中实现的业务方法。调用时可以在原有业务方法的基础上附加一些新的方法来对功能进行扩充或约束。 + +```c +class Program +{ + static void Main(string[] args) + { + Subject proxy = new Proxy(); + proxy.Request(); + } +} +``` + +
+ +

图24 运行结果

+
+ + +由于 代理模式 的使用目不同,可将 代理模式 分为多种类型,应用于不同的场合,满足用户的不同需求,例如: + +(1)远程代理Remote Proxy): + +- 远程代理使得客户端程序可以访问在远程主机上的对象。 +- 远程代理可以将网络的细节隐藏起来,客户端完全可以认为被代理的远程业务对象是在本地而不是在远程,而 远程代理对象 承担了大部分的网络通信工作,并负责对远程业务方法的调用。 + +
+ +
+ +

图25 远程代理示意图

+
+ + +(2)虚拟代理Virtual Proxy): + +用一个“虚假”的代理对象来代表真实对象,通过代理对象来间接引用真实对象,可以在一定程度上提高系统的性能。 + +- 当一个对象由于网络等原因需要较长的加载时间时,可以结合多线程技术,用一个线程显示加载时间相对较短的虚拟代理对象,其他线程用于加载真实对象。 +- 当一个对象的加载十分耗费系统资源时,虚拟代理可以让那些占用大量内存或处理起来非常复杂的对象推迟到使用它们的时候才创建,而在此之前用一个相对来说占用资源较少的代理对象来代表真实对象,再通过代理对象来引用真实对象。 + + +(3)缓冲代理Cache Proxy): + +为某一个被频繁访问的操作结果提供一个临时存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。 + +- 如果需要远程访问的数据在缓存中已经存在,则无须再重复执行获取数据的方法,直接返回存储在缓存中的数据即可。 + + +(4)保护代理Protect Proxy): + +需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时使用。 + + + +(5)智能引用代理Smart Reference Proxy): + +需要为一个对象的访问提供一些额外的操作时使用,例如将对象被调用的次数记录下来等。 + +