结构型模式
结构型模式,顾名思义讨论的是类和对象的结构 ,主要用来处理类或对象的组合。它包括两种类型,一是类结构型模式,指的是采用继承机制来组合接口或实现;二是对象结构型模式,指的是通过组合对象的方式来实现新的功能。它包括适配器模式、桥接模式、装饰者模式、组合模式、外观模式、享元模式和代理模式。
适配器模式注重转换接口,将不吻合的接口适配对接
桥接模式注重分离接口与其实现,支持多维度变化
组合模式注重统一接口,将“一对多”的关系转化为“一对一”的关系
装饰者模式注重稳定接口,在此前提下为对象扩展功能
外观模式注重简化接口,简化组件系统与外部客户程序的依赖关系
享元模式注重保留接口,在内部使用共享技术对对象存储进行优化
代理模式注重假借接口,增加间接层来实现灵活控制
适配器模式
适配器模式意在转换接口,它能够使原本不能再一起工作的两个类一起工作,所以经常用来在类库的复用、代码迁移等方面。例如DataAdapter类就应用了适配器模式。适配器模式包括类适配器模式和对象适配器模式,具体结构如下图所示,左边是类适配器模式,右边是对象适配器模式。
示例代码
/// 这里以插座和插头的例子来诠释适配器模式/// 现在我们买的电器插头是2个孔,但是我们买的插座只有3个孔的/// 这是我们想把电器插在插座上的话就需要一个电适配器namespace 设计模式之适配器模式{ ////// 客户端,客户想要把2个孔的插头 转变成三个孔的插头,这个转变交给适配器就好 /// 既然适配器需要完成这个功能,所以它必须同时具体2个孔插头和三个孔插头的特征 /// class Client { static void Main(string[] args) { // 现在客户端可以通过电适配要使用2个孔的插头了 IThreeHole threehole = new PowerAdapter(); threehole.Request(); Console.ReadLine(); } } ////// 三个孔的插头,也就是适配器模式中的目标角色 /// public interface IThreeHole { void Request(); } ////// 两个孔的插头,源角色——需要适配的类 /// public abstract class TwoHole { public void SpecificRequest() { Console.WriteLine("我是两个孔的插头"); } } ////// 适配器类,接口要放在类的后面 /// 适配器类提供了三个孔插头的行为,但其本质是调用两个孔插头的方法 /// public class PowerAdapter:TwoHole,IThreeHole { ////// 实现三个孔插头接口方法 /// public void Request()//重写了三孔插头的方法 { // 调用两个孔插头方法 this.SpecificRequest(); } }}
对应uml图
适配器模式优缺点
类的适配器模式:
优点:
A可以在不修改原有代码的基础上来复用现有类,很好地符合 “开闭原则”
B可以重新定义Adaptee(被适配的类)的部分行为,因为在类适配器模式中,Adapter是Adaptee的子类
仅仅引入一个对象,并不需要额外的字段来引用Adaptee实例(这个即是优点也是缺点)。
缺点:
A用一个具体的Adapter类对Adaptee和Target进行匹配,当如果想要匹配一个类以及所有它的子类时,类的适配器模式就不能胜任了。因为类的适配器模式中没有引入Adaptee的实例,光调用this.SpecificRequest方法并不能去调用它对应子类的SpecificRequest方法。
B采用了“多继承”的实现方式,带来了不良的高耦合。
对象的适配器模式
优点:
A可以在不修改原有代码的基础上来复用现有类,很好地符合 “开闭原则”(这点是两种实现方式都具有的)
B采用 “对象组合”的方式,更符合松耦合。
缺点:
A使得重定义Adaptee的行为较困难,这就需要生成Adaptee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身。
使用情景
1 系统需要复用现有类,而该类的接口不符合系统的需求
2 想要建立一个可重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
3 对于对象适配器模式,在设计里需要改变多个已有子类的接口,如果使用类的适配器模式,就要针对每一个子类做一个适配器,而这不太实际。
应用举例
1.适配器模式在.NET Framework中的一个最大的应用就是COM Interop。COM Interop就好像是COM和.NET之间的一座桥梁。COM组件对象与.NET类对象是完全不同的,但为了使.NET程序象使用.NET对象一样使用COM组件,微软在处理方式上采用了Adapter模式,对COM对象进行包装,这个包装类就是RCW(Runtime Callable Wrapper)。RCW实际上是runtime生成的一个.NET类,它包装了COM组件的方法,并内部实现对COM组件的调用。如下图所示:
2..NET中的另外一个适配器模式的应用就是DataAdapter。ADO.NET为统一的数据访问提供了多个接口和基类,其中最重要的接口之一是IdataAdapter。DataAdpter起到了数据库到DataSet桥接器的作用,使应用程序的数据操作统一到DataSet上,而与具体的数据库类型无关。甚至可以针对特殊的数据源编制自己的DataAdpter,从而使我们的应用程序与这些特殊的数据源相兼容。
桥接模式
桥接模式旨在将抽象化与实现化解耦,使得两者可以独立地变化。意思就是说,桥接模式把原来基类的实现化细节再进一步进行抽象,构造到一个实现化的结构中,然后再把原来的基类改造成一个抽象化的等级结构,这样就可以实现系统在多个维度的独立变化,桥接模式的结构图如下所示。
示例代码
////// 抽象概念中的遥控器,扮演抽象化角色/// public class RemoteControl{ // 字段 private TV implementor; // 属性 public TV Implementor { get { return implementor; } set { implementor = value; } } ////// 开电视机,这里抽象类中不再提供实现了,而是调用实现类中的实现 /// public virtual void On() { implementor.On(); } ////// 关电视机 /// public virtual void Off() { implementor.Off(); } ////// 换频道 /// public virtual void SetChannel() { implementor.tuneChannel(); }}////// 具体遥控器/// public class ConcreteRemote : RemoteControl{ public override void SetChannel() { Console.WriteLine("---------------------"); base.SetChannel(); Console.WriteLine("---------------------"); }}遥控器的实现方法部分代码,即实现化部分代码,此时我们用另外一个抽象类TV封装了遥控器功能的变化,具体实现交给具体型号电视机去完成:////// 电视机,提供抽象方法/// public abstract class TV{ public abstract void On(); public abstract void Off(); public abstract void tuneChannel();}////// 长虹牌电视机,重写基类的抽象方法/// 提供具体的实现/// public class ChangHong : TV{ public override void On() { Console.WriteLine("长虹牌电视机已经打开了"); } public override void Off() { Console.WriteLine("长虹牌电视机已经关掉了"); } public override void tuneChannel() { Console.WriteLine("长虹牌电视机换频道"); }}////// 三星牌电视机,重写基类的抽象方法/// public class Samsung : TV{ public override void On() { Console.WriteLine("三星牌电视机已经打开了"); } public override void Off() { Console.WriteLine("三星牌电视机已经关掉了"); } public override void tuneChannel() { Console.WriteLine("三星牌电视机换频道"); }}
采用桥接模式的客户端调用代码:
////// 以电视机遥控器的例子来演示桥接模式/// class Client{ static void Main(string[] args) { // 创建一个遥控器 RemoteControl remoteControl = new ConcreteRemote(); // 长虹电视机 remoteControl.Implementor = new ChangHong(); remoteControl.On(); remoteControl.SetChannel(); remoteControl.Off(); Console.WriteLine(); // 三星牌电视机 remoteControl.Implementor = new Samsung(); remoteControl.On(); remoteControl.SetChannel(); remoteControl.Off(); Console.Read(); }}
上面桥接模式的实现中,遥控器的功能实现方法不在遥控器抽象类中去实现了,而是把实现部分用来另一个电视机类去封装它,然而遥控器中只包含电视机类的一个引用,同时这样的设计也非常符合现实生活中的情况(我认为的现实生活中遥控器的实现——遥控器中并不包含换台,打开电视机这样的功能的实现,遥控器只是包含了电视机上这些功能的引用,然后红外线去找到电视机上对应功能的的实现)。通过桥接模式,我们把抽象化和实现化部分分离开了,这样就可以很好应对这两方面的变化了。
对应的类图
看完桥接模式的实现后,为了帮助大家理清对桥接模式中类之间关系,这里给出桥接模式的类图结构:
桥接模式优缺点
优点:
A把抽象接口与其实现解耦。
B抽象和实现可以独立扩展,不会影响到对方。
C实现细·节对客户透明,对用于隐藏了具体实现细节。
缺点:增加了系统的复杂度
使用情景
1 如果一个系统需要在构件的抽象化角色和具体化角色之间添加更多的灵活性,避免在两个层次之间建立静态的联系。
2 设计要求实现化角色的任何改变不应当影响客户端,或者实现化角色的改变对客户端是完全透明的。
3 需要跨越多个平台的图形和窗口系统上。
4 一个类存在两个独立变化的维度,且两个维度都需要进行扩展。
应用举例:
桥接模式也经常用于具体的系统开发中,对于三层架构中就应用了桥接模式,三层架构中的业务逻辑层BLL中通过桥接模式与数据操作层解耦(DAL),其实现方式就是在BLL层中引用了DAL层中一个引用。这样数据操作的实现可以在不改变客户端代码的情况下动态进行更换。
装饰者模式
装饰者模式又称包装(Wrapper)模式,它可以动态地给一个对象添加一些额外的功能,装饰者模式较继承生成子类的方式更加灵活。虽然装饰者模式能够动态地将职责附加到对象上,但它也会造成产生一些细小的对象,增加了系统的复杂度。具体的结构图如下所示。
示例代码
////// 手机抽象类,即装饰者模式中的抽象组件类/// public abstract class Phone{ public abstract void Print();}////// 苹果手机,即装饰着模式中的具体组件类/// public class ApplePhone:Phone{////// 重写基类方法/// public override void Print(){ Console.WriteLine("开始执行具体的对象——苹果手机");}////// 装饰抽象类,要让装饰完全取代抽象组件,所以必须继承自Photo/// public abstract class Decorator:Phone{ private Phone phone; public Decorator(Phone p) { this.phone = p; } public override void Print() { if (phone != null) { phone.Print(); } }}////// 贴膜,即具体装饰者/// public class Sticker : Decorator{ public Sticker(Phone p) : base(p) { } public override void Print() { base.Print(); // 添加新的行为 AddSticker(); } ////// 新的行为方法 /// public void AddSticker() { Console.WriteLine("现在苹果手机有贴膜了"); }}////// 手机挂件/// public class Accessories : Decorator{ public Accessories(Phone p) : base(p) { } public override void Print() { base.Print(); // 添加新的行为 AddAccessories(); } ////// 新的行为方法 /// public void AddAccessories() { Console.WriteLine("现在苹果手机有漂亮的挂件了"); }}
此时客户端调用代码如下:
class Customer{ static void Main(string[] args) { // 我买了个苹果手机 Phone phone = new ApplePhone(); // 现在想贴膜了 Decorator applePhoneWithSticker = new Sticker(phone); // 扩展贴膜行为 applePhoneWithSticker.Print(); Console.WriteLine("----------------------\n"); // 现在我想有挂件了 Decorator applePhoneWithAccessories = new Accessories(phone); // 扩展手机挂件行为 applePhoneWithAccessories.Print(); Console.WriteLine("----------------------\n"); // 现在我同时有贴膜和手机挂件了 Sticker sticker = new Sticker(phone); Accessories applePhoneWithAccessoriesAndSticker = new Accessories(sticker); applePhoneWithAccessoriesAndSticker.Print(); Console.ReadLine(); }}
从上面的客户端代码可以看出,客户端可以动态地将手机配件增加到手机上,如果需要添加手机外壳时,此时只需要添加一个继承Decorator的手机外壳类,从而,装饰者模式扩展性也非常好。
装饰者模式的优缺点
优点:
装饰这模式和继承的目的都是扩展对象的功能,但装饰者模式比继承更灵活
通过使用不同的具体装饰类以及这些类的排列组合,设计师可以创造出很多不同行为的组合
装饰者模式有很好地可扩展性
缺点:装饰者模式会导致设计中出现许多小对象,如果过度使用,会让程序变的更复杂。并且更多的对象会是的差错变得困难,特别是这些对象看上去都很像。
使用场景
1 需要扩展一个类的功能或给一个类增加附加责任。
2 需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
3 需要增加由一些基本功能的排列组合而产生的非常大量的功能
装饰者模式采用对象组合而非继承的方式实现了再运行时动态地扩展对象功能的能力,而且可以根据需要扩展多个功能,避免了单独使用继承带来的 ”灵活性差“和”多子类衍生问题“。同时它很好地符合面向对象设计原则中 ”优先使用对象组合而非继承“和”开放-封闭“原则。
装饰者模式的应用举例
在.NET 类库中也有装饰者模式的实现,该类就是System.IO.Stream,下面看看Stream类结构:
上图中,BufferedStream、CryptoStream和GZipStream其实就是两个具体装饰类,这里的装饰者模式省略了抽象装饰角色(Decorator)。下面演示下客户端如何动态地为MemoryStream动态增加功能的。
MemoryStream memoryStream = new MemoryStream(new byte[] {95,96,97,98,99});
// 扩展缓冲的功能
BufferedStream buffStream = new BufferedStream(memoryStream);
// 添加加密的功能
CryptoStream cryptoStream = new CryptoStream(memoryStream,new AesManaged().CreateEncryptor(),CryptoStreamMode.Write);
// 添加压缩功能
GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Compress, true);
组合模式
组合模式允许你将对象组合成树形结构来表现”部分-整体“的层次结构,使得客户以一致的方式处理单个对象以及对象的组合。下面我们用绘制的例子来详细介绍组合模式,图形可以由一些基本图形元素组成(如直线,圆等),也可以由一些复杂图形组成(由基本图形元素组合而成),为了使客户对基本图形和复杂图形的调用保持一致,我们使用组合模式来达到整个目的。
组合模式实现的最关键的地方是——简单对象和复合对象必须实现相同的接口。这就是组合模式能够将组合对象和简单对象进行一致处理的原因。
示例代码
// 通过一些简单图形以及一些复杂图形构建图形树来演示组合模式// 客户端调用class Client{ static void Main(string[] args) { ComplexGraphics complexGraphics = new ComplexGraphics("一个复杂图形和两条线段组成的复杂图形"); complexGraphics.Add(new Line("线段A")); ComplexGraphics CompositeCG = new ComplexGraphics("一个圆和一条线组成的复杂图形"); CompositeCG.Add(new Circle("圆")); CompositeCG.Add(new Circle("线段B")); complexGraphics.Add(CompositeCG); Line l = new Line("线段C"); complexGraphics.Add(l); // 显示复杂图形的画法 Console.WriteLine("复杂图形的绘制如下:"); Console.WriteLine("---------------------"); complexGraphics.Draw(); Console.WriteLine("复杂图形绘制完成"); Console.WriteLine("---------------------"); Console.WriteLine(); // 移除一个组件再显示复杂图形的画法 complexGraphics.Remove(l); Console.WriteLine("移除线段C后,复杂图形的绘制如下:"); Console.WriteLine("---------------------"); complexGraphics.Draw(); Console.WriteLine("复杂图形绘制完成"); Console.WriteLine("---------------------"); Console.Read(); }}////// 图形抽象类,/// public abstract class Graphics{ public string Name { get; set; } public Graphics(string name) { this.Name = name; } public abstract void Draw(); public abstract void Add(Graphics g); public abstract void Remove(Graphics g);}////// 简单图形类——线/// public class Line : Graphics{ public Line(string name) : base(name) { } // 重写父类抽象方法 public override void Draw() { Console.WriteLine("画 " + Name); } // 因为简单图形在添加或移除其他图形,所以简单图形Add或Remove方法没有任何意义 // 如果客户端调用了简单图形的Add或Remove方法将会在运行时抛出异常 // 我们可以在客户端捕获该类移除并处理 public override void Add(Graphics g) { throw new Exception("不能向简单图形Line添加其他图形"); } public override void Remove(Graphics g) { throw new Exception("不能向简单图形Line移除其他图形"); }}////// 简单图形类——圆/// public class Circle : Graphics{ public Circle(string name) : base(name) { } // 重写父类抽象方法 public override void Draw() { Console.WriteLine("画 " + Name); } public override void Add(Graphics g) { throw new Exception("不能向简单图形Circle添加其他图形"); } public override void Remove(Graphics g) { throw new Exception("不能向简单图形Circle移除其他图形"); }}////// 复杂图形,由一些简单图形组成,这里假设该复杂图形由一个圆两条线组成的复杂图形/// public class ComplexGraphics : Graphics{ private ListcomplexGraphicsList = new List (); public ComplexGraphics(string name) : base(name) { } /// /// 复杂图形的画法 /// public override void Draw() { foreach (Graphics g in complexGraphicsList) { g.Draw(); } } public override void Add(Graphics g) { complexGraphicsList.Add(g); } public override void Remove(Graphics g) { complexGraphicsList.Remove(g); }}
安全的组合模式示例代码
由于基本图形对象不存在Add和Remove方法,上面实现中直接通过抛出一个异常的方式来解决这样的问题的,但是我们想以一种更安全的方式来解决——因为基本图形根本不存在这样的方法,我们是不是可以移除这些方法呢?为了移除这些方法,我们就不得不修改Graphics接口,我们把管理子对象的方法声明放在复合图形对象里面,这样简单对象Line、Circle使用这些方法时在编译时就会出错,这样的一种实现方式我们称为安全式的组合模式,然而上面的实现方式称为透明式的组合模式,下面让我们看看安全式的组合模式又是怎样实现的,具体实现代码如下:
/// 安全式的组合模式/// 此方式实现的组合模式把管理子对象的方法声明在树枝构件ComplexGraphics类中/// 这样如果叶子节点Line、Circle使用了Add或Remove方法时,就能在编译期间出现错误/// 但这种方式虽然解决了透明式组合模式的问题,但是它使得叶子节点和树枝构件具有不一样的接口。/// 所以这两种方式实现的组合模式各有优缺点,具体使用哪个,可以根据问题的实际情况而定class Client{ static void Main(string[] args) { ComplexGraphics complexGraphics = new ComplexGraphics("一个复杂图形和两条线段组成的复杂图形"); complexGraphics.Add(new Line("线段A")); ComplexGraphics CompositeCG = new ComplexGraphics("一个圆和一条线组成的复杂图形"); CompositeCG.Add(new Circle("圆")); CompositeCG.Add(new Circle("线段B")); complexGraphics.Add(CompositeCG); Line l = new Line("线段C"); complexGraphics.Add(l); // 显示复杂图形的画法 Console.WriteLine("复杂图形的绘制如下:"); Console.WriteLine("---------------------"); complexGraphics.Draw(); Console.WriteLine("复杂图形绘制完成"); Console.WriteLine("---------------------"); Console.WriteLine(); // 移除一个组件再显示复杂图形的画法 complexGraphics.Remove(l); Console.WriteLine("移除线段C后,复杂图形的绘制如下:"); Console.WriteLine("---------------------"); complexGraphics.Draw(); Console.WriteLine("复杂图形绘制完成"); Console.WriteLine("---------------------"); Console.Read(); }}////// 图形抽象类,/// public abstract class Graphics{ public string Name { get; set; } public Graphics(string name) { this.Name = name; } public abstract void Draw(); // 移除了Add和Remove方法 // 把管理子对象的方法放到了ComplexGraphics类中进行管理 // 因为这些方法只在复杂图形中才有意义 }////// 简单图形类——线/// public class Line : Graphics{ public Line(string name) : base(name) { } // 重写父类抽象方法 public override void Draw() { Console.WriteLine("画 " + Name); }}////// 简单图形类——圆/// public class Circle : Graphics{ public Circle(string name) : base(name) { } // 重写父类抽象方法 public override void Draw() { Console.WriteLine("画 " + Name); }}////// 复杂图形,由一些简单图形组成,这里假设该复杂图形由一个圆两条线组成的复杂图形/// public class ComplexGraphics : Graphics{ private ListcomplexGraphicsList = new List (); public ComplexGraphics(string name) : base(name) { } /// /// 复杂图形的画法 /// public override void Draw() { foreach (Graphics g in complexGraphicsList) { g.Draw(); } } public void Add(Graphics g) { complexGraphicsList.Add(g); } public void Remove(Graphics g) { complexGraphicsList.Remove(g); }}
对应类图
安全式组合模式的类图:
组合模式中涉及到三个角色:
抽象构件(Component)角色:这是一个抽象角色,上面实现中Graphics充当这个角色,它给参加组合的对象定义出了公共的接口及默认行为,可以用来管理所有的子对象(在透明式的组合模式是这样的)。在安全式的组合模式里,构件角色并不定义出管理子对象的方法,这一定义由树枝结构对象给出。
树叶构件(Leaf)角色:树叶对象时没有下级子对象的对象,上面实现中Line和Circle充当这个角色,定义出参加组合的原始对象的行为
树枝构件(Composite)角色:代表参加组合的有下级子对象的对象,上面实现中ComplexGraphics充当这个角色,树枝对象给出所有管理子对象的方法实现,如Add、Remove等。
组合模式的优缺点
优点:
A组合模式使得客户端代码可以一致地处理对象和对象容器,无需关系处理的单个对象,还是组合的对象容器。
B将”客户代码与复杂的对象容器结构“解耦。
C可以更容易地往组合对象中加入新的构件。
缺点:使得设计更加复杂。客户端需要花更多时间理清类之间的层次关系。(这个是几乎所有设计模式所面临的问题)。
注意的问题:
A有时候系统需要遍历一个树枝结构的子构件很多次,这时候可以考虑把遍历子构件的结构存储在父构件里面作为缓存。
B客户端尽量不要直接调用树叶类中的方法(在我上面实现就是这样的,创建的是一个树枝的具体对象,不应该使用Graphics complexGraphics = new ComplexGraphics("一个复杂图形和两条线段组成的复杂图形");),而是借用其父类(Graphics)的多态性完成调用,这样可以增加代码的复用性。
组合模式的使用场景
A需要表示一个对象整体或部分的层次结构。
B希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
应用举例
组合模式在.NET 中最典型的应用就是应用与WinForms和Web的开发中,在.NET类库中,都为这两个平台提供了很多现有的控件,然而System.Windows.Forms.dll中System.Windows.Forms.Control类就应用了组合模式,因为控件包括Label、TextBox等这样的简单控件,同时也包括GroupBox、DataGrid这样复合的控件,每个控件都需要调用方法来进行控件显示,为了表示这种对象之间整体与部分的层次结构,微软把Control类的实现应用了组合模式(确切地说应用了透明式的组合模式)。