本文摘取自TerryLee(李会军)老师的设计模式系列文章,版权归TerryLee,仅供个人学习参考。转载请标明原作者TerryLee。部分示例代码来自DoFactory。
概述
在软件系统中,由于应用环境的变化,常常需要将"一些现存的对象"放在新的环境中应用,但是新环境要求的接口是这些现存对象所不满足的。那么如何应对这种"迁移的变化"?如何既能利用现有对象的良好实现,同时又能满足新的应用环境所要求的接口?这就是本文要说的适配器模式。
意图
将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
UML
图1. 类的Adapter模式UML图
图2. 对象的Adapter模式UML图
参与者
这个模式涉及的类或对象:
-
Target
-
定义Client使用的特定领域的接口。
-
Adapter
-
将Adaptee(需要匹配)的接口适配到目标接口。
-
Adaptee
-
定义一个需要适配的已存在的接口。
-
Client
-
与符合目标接口的对象进行合作。
适用性
当在自己的程序中使用第三方类库时,往往其接口很难与自己的接口完全一致。这是应该考虑使用适配器模式。该模式中的核心 – Adapter为Client提供了与自己接口一致的访问方式,内部使用实现不同接口的类来完成具体工作。在新旧程序需要集成在一起环境中适配器模式被大量应用。
适配器同样用于重构场景中。如,你有两个完成相似功能但实现不同接口的类。Client使用了这两个类,但如果它们共享同一个接口,代码回简洁易懂的多。你不能修改接口,但可以使用Adapter来屏蔽它们的不同,使Client可以与一个公共接口进行交互。Adapter提供了共享接口与原始接口间的映射。
在以下各种情况下使用适配器模式:
-
你想使用一个已经存在的类,而它的接口不符合现有接口的需求。
-
你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
-
关于对象 适配器和类适配器,当你想使用一些已经存在的子类,如果使用类适配器模式,就要针对每一个子类做一个适配器,而这不太实际。通过对象适配器可以适配它的父类接口。
DoFactory GoF代码
这个结构化例子演示了通过适配器使一个实现了与另一个对象不同接口的两个对象在一起协同工作。
// Adapter pattern // Structural example using System; namespace DoFactory.GangOfFour.Adapter.Structural { class MainApp { static void Main() { // Create adapter and place a request Target target = new Adapter(); target.Request(); // Wait for user Console.ReadKey(); } } // "Target" class Target { public virtual void Request() { Console.WriteLine("Called Target Request()"); } } // "Adapter" class Adapter : Target { private Adaptee _adaptee = new Adaptee(); public override void Request() { // Possibly do some other work and then call SpecificRequest _adaptee.SpecificRequest(); } } // "Adaptee" class Adaptee { public void SpecificRequest() { Console.WriteLine("Called SpecificRequest()"); } } }
这个例子中演示了怎样使用一个遗留的化学数据库 – 通过RichCompound这个适配器来实现。
例子中涉及到的类与适配器模式中标准的类对应关系如下:
-
Target – Compound
-
Adapter – RichCompound
-
Adaptee - ChemicalDatabank
-
Client - MainApp
// Adapter pattern // Real World example using System; namespace DoFactory.GangOfFour.Adapter.RealWorld { class MainApp { static void Main() { // Non-adapted chemical compound Compound unknown = new Compound(); unknown.Display(); // Adapted chemical compounds Compound water = new RichCompound("Water"); water.Display(); Compound benzene = new RichCompound("Benzene"); benzene.Display(); Compound ethanol = new RichCompound("Ethanol"); ethanol.Display(); // Wait for user Console.ReadKey(); } } // "Target" class Compound { protected float _boilingPoint; protected float _meltingPoint; protected double _molecularWeight; protected string _molecularFormula; public virtual void Display() { Console.WriteLine(" Compound: Unknown ------ "); } } // "Adapter" class RichCompound : Compound { private string _chemical; private ChemicalDatabank _bank; // Constructor public RichCompound(string chemical) { _chemical = chemical; } public override void Display() { // The Adaptee _bank = new ChemicalDatabank(); _boilingPoint = _bank.GetCriticalPoint(_chemical, "B"); _meltingPoint = _bank.GetCriticalPoint(_chemical, "M"); _molecularWeight = _bank.GetMolecularWeight(_chemical); _molecularFormula = _bank.GetMolecularStructure(_chemical); Console.WriteLine(" Compound: {0} ------ ", _chemical); Console.WriteLine(" Formula: {0}", _molecularFormula); Console.WriteLine(" Weight : {0}", _molecularWeight); Console.WriteLine(" Melting Pt: {0}", _meltingPoint); Console.WriteLine(" Boiling Pt: {0}", _boilingPoint); } } // "Adaptee" class ChemicalDatabank { // The databank 'legacy API' public float GetCriticalPoint(string compound, string point) { // Melting Point if (point == "M") { switch (compound.ToLower()) { case "water": return 0.0f; case "benzene": return 5.5f; case "ethanol": return -114.1f; default: return 0f; } } // Boiling Point else { switch (compound.ToLower()) { case "water": return 100.0f; case "benzene": return 80.1f; case "ethanol": return 78.3f; default: return 0f; } } } public string GetMolecularStructure(string compound) { switch (compound.ToLower()) { case "water": return "H20"; case "benzene": return "C6H6"; case "ethanol": return "C2H5OH"; default: return ""; } } public double GetMolecularWeight(string compound) { switch (compound.ToLower()) { case "water": return 18.015; case "benzene": return 78.1134; case "ethanol": return 46.0688; default: return 0d; } } } }
在.NET优化版本的示例中,RichCompound(Adapter)通过属性(C#自动属性极大减少了代码量)控制的成员的读写,并使用protected控制可访问性。另外一些地方使用枚举代替字符串增强类型安全性。
// Adapter pattern // .NET Optimized example using System; namespace DoFactory.GangOfFour.Adapter.NETOptimized { class MainApp { static void Main() { // Non-adapted chemical compound var unknown = new Compound(); unknown.Display(); // Adapted chemical compounds var water = new RichCompound(Chemical.Water); water.Display(); var benzene = new RichCompound(Chemical.Benzene); benzene.Display(); var ethanol = new RichCompound(Chemical.Ethanol); ethanol.Display(); // Wait for user Console.ReadKey(); } } // "Target" class Compound { public Chemical Chemical { get; protected set; } public float BoilingPoint { get; protected set; } public float MeltingPoint { get; protected set; } public double MolecularWeight { get; protected set; } public string MolecularFormula { get; protected set; } public virtual void Display() { Console.WriteLine(" Compound: Unknown ------ "); } } // "Adapter" class RichCompound : Compound { private ChemicalDatabank _bank; // Constructor public RichCompound(Chemical chemical) { Chemical = chemical; // The Adaptee _bank = new ChemicalDatabank(); } public override void Display() { // Adaptee request methods BoilingPoint = _bank.GetCriticalPoint(Chemical, State.Boiling); MeltingPoint = _bank.GetCriticalPoint(Chemical, State.Melting); MolecularWeight = _bank.GetMolecularWeight(Chemical); MolecularFormula = _bank.GetMolecularStructure(Chemical); Console.WriteLine(" Compound: {0} ------ ", Chemical); Console.WriteLine(" Formula: {0}", MolecularFormula); Console.WriteLine(" Weight : {0}", MolecularWeight); Console.WriteLine(" Melting Pt: {0}", MeltingPoint); Console.WriteLine(" Boiling Pt: {0}", BoilingPoint); } } // "Adaptee" class ChemicalDatabank { // The databank 'legacy API' public float GetCriticalPoint(Chemical compound, State point) { // Melting Point if (point == State.Melting) { switch (compound) { case Chemical.Water: return 0.0f; case Chemical.Benzene: return 5.5f; case Chemical.Ethanol: return -114.1f; default: return 0f; } } // Boiling Point else { switch (compound) { case Chemical.Water: return 100.0f; case Chemical.Benzene: return 80.1f; case Chemical.Ethanol: return 78.3f; default: return 0f; } } } public string GetMolecularStructure(Chemical compound) { switch (compound) { case Chemical.Water: return "H20"; case Chemical.Benzene: return "C6H6"; case Chemical.Ethanol: return "C2H5OH"; default: return ""; } } public double GetMolecularWeight(Chemical compound) { switch (compound) { case Chemical.Water: return 18.015; case Chemical.Benzene: return 78.1134; case Chemical.Ethanol: return 46.0688; } return 0d; } } // Chemical enumeration public enum Chemical { Water, Benzene, Ethanol } // State enumeration public enum State { Boiling, Melting } }
适配器模式解说
我们还是以日志记录程序为例子说明Adapter模式。现在有这样一个场景:假设我们在软件开发中要使用一个第三方的日志记录工具,该日志记录工具支持数据库日志记录DatabaseLog和文本文件记录FileLog两种方式,它提供给我们的API接口是Write()方法,使用方法如下:
Log.Write("Logging Message!");
当软件系统开发进行到一半时,处于某种原因不能继续使用该日志记录工具了,需要采用另外一个日志记录工具,它同样也支持数据库日志记录DatabaseLog和文本文件记录FileLog两种方式,只不过它提供给我们的API接口是WriteLog()方法,使用方法如下:
Log.WriteLog("Logging Message!");
该日志记录工具的类结构图如下:
图3. 日志记录工具类结构图
它的实现代码如下:
public abstract class LogAdaptee { public abstract void WriteLog(); } public class DatabaseLog : LogAdaptee { public override void WriteLog() { Console.WriteLine("Called WriteLog Method"); } } public class FileLog : LogAdaptee { public override void WriteLog() { Console.WriteLine("Called WriteLog Method"); } }
在我们开发完成的应用程序中日志记录接口中(不妨称之为ILogTarget接口,在本例中为了更加清楚地说明,在命名上采用了Adapter模式中的相关角色名字),却用到了大量的Write()方法,程序已经全部通过了测试,我们不能去修改该接口。代码如下:
public interface ILogTarget { void Write(); }
这时也许我们会想到修改现在的日志记录工具的API接口,但是由于版权等原因我们不能够修改它的源代码,此时Adapter模式便可以派上用场了。下面我们通过Adapter模式来使得该日志记录工具能够符合我们当前的需求。
前面说过,Adapter模式有两种实现形式的实现结构,首先来看一下类适配器如何实现。现在唯一可行的办法就是在程序中引入新的类型,让它去继承LogAdaptee类,同时又实现已有的ILogTarget接口。由于LogAdaptee有两种类型的方式,自然我们要引入两个分别为DatabaseLogAdapter和FileLogAdapter的类。
图4. 引入类适配器后的结构图
实现代码如下:
public class DatabaseLogAdapter : DatabaseLog, ILogTarget { public void Write() { WriteLog(); } } public class FileLogAdapter : FileLog, ILogTarget { public void Write() { this.WriteLog(); } }
这里需要注意的一点是我们为每一种日志记录方式都编写了它的适配类,那为什么不能为抽象类LogAdaptee来编写一个适配类呢?因为DatabaseLog和FileLog虽然同时继承于抽象类LogAdaptee,但是它们具体的WriteLog()方法的实现是不同的。只有继承于该具体类,才能保留其原有的行为。
我们看一下这时客户端的程序的调用方法:
public class App { public static void Main() { ILogTarget dbLog = new DatabaseLogAdapter(); dbLog.Write("Logging Database..."); ILogTarget fileLog = new FileLogAdapter(); fileLog.Write("Logging File..."); } }
下面看一下如何通过对象适配器的方式来达到我们适配的目的。对象适配器是采用对象组合而不是使用继承,类结构图如下:
图5. 引入对象适配器后的结构图
实现代码如下:
public class LogAdapter : ILogTarget { private LogAdaptee _adaptee; public LogAdapter(LogAdaptee adaptee) { this._adaptee = adaptee; } public void Write() { _adaptee.WriteLog(); } }
与类适配器相比较,可以看到最大的区别是适配器类的数量减少了,不再需要为每一种具体的日志记录方式来创建一个适配器类。同时可以看到,引入对象适配器后,适配器类不再依赖于具体的DatabaseLog类和FileLog类,更好的实现了松耦合。
再看一下客户端程序的调用方法:
public class App { public static void Main() { ILogTarget dbLog = new LogAdapter(new DatabaseLog()); dbLog.Write("Logging Database..."); ILogTarget fileLog = new LogAdapter(new FileLog()); fileLog.Write("Logging Database..."); } }
通过Adapter模式,我们很好的实现了对现有组件的复用。对比以上两种适配方式,可以总结出,在类适配方式中,我们得到的适配器类DatabaseLogAdapter和FileLogAdapter具有它所继承的父类的所有的行为,同时也具有接口ILogTarget的所有行为,这样其实是违背了面向对象设计原则中的类的单一职责原则,而对象适配器则更符合面向对象的精神,所以在实际应用中不太推荐类适配这种方式。再换个角度来看类适配方式,假设我们要适配出来的类在记录日志时同时写入文件和数据库,那么用对象适配器我们会这样去写:
public class LogAdapter : ILogTarget { private LogAdaptee _adaptee1; private LogAdaptee _adaptee2; public LogAdapter(LogAdaptee adaptee1, LogAdaptee adaptee2) { this._adaptee1 = adaptee1; this._adaptee2 = adaptee2; } public void Write() { _adaptee1.WriteLog(); _adaptee2.WriteLog(); } }
如果改用类适配器,难道这样去写:
public class DatabaseLogAdapter : DatabaseLog, FileLog, ILogTarget { public void Write() { //WriteLog(); } }
显然是不对的,这样的解释虽说有些牵强,也足以说明一些问题,当然了并不是说类适配器在任何情况下都不使用,针对开发场景不同,某些时候还是可以用类适配器的方式。
.NET中的适配器模式
-
Adapter模式在.NET Framework中的一个最大的应用就是COM Interop。COM Interop就好像是COM和.NET之间的一条纽带,一座桥梁。我们知道,COM组件对象与.NET类对象是完全不同的,如COM会返回HRESULT表示成功或失败,但.NET遇到错误时会使用抛出异常等方式,但为了使COM客户程序象调用COM组件一样调用.NET对象,使.NET程序象使用.NET对象一样使用COM组件,微软在处理方式上采用了Adapter模式,对COM对象进行包装,这个Adapter类就是RCW(Runtime Callable Wrapper)。RCW实际上是runtime生成的一个.NET类,它包装了COM组件的方法,并内部实现对COM组件的调用,同时其接口可以方便.NET调用。如下图所示:
图6. NET程序与COM互相调用示意图
-
.NET中的另一个Adapter模式的应用就是DataAdapter。ADO.NET为统一的数据访问提供了多个接口和基类,其中最重要的接口之一是IdataAdapter。与之相对应的DataAdpter是一个抽象类,它是ADO.NET与具体数据库操作之间的数据适配器的基类。DataAdpter起到了数据库到DataSet桥接器的作用,使应用程序的数据操作(Fill和Update)统一到DataSet上,而与具体的数据库类型无关。甚至可以针对特殊的数据源编制自己的DataAdpter,从而使我们的应用程序与这些特殊的数据源相兼容。注意这是一个适配器的变体。
实现要点
-
Adapter模式主要应用于"希望复用一些现存的类,但是接口又与复用环境要求不一致的情况",在遗留代码复用、类库迁移等方面非常有用。
-
Adapter模式有对象适配器和类适配器两种形式的实现结构,但是类适配器采用"多继承"的实现方式,带来了不良的高耦合,所以一般不推荐使用。对象适配器采用"对象组合"的方式,更符合松耦合精神。
-
Adapter模式的实现可以非常的灵活,不必拘泥于GOF23中定义的两种结构。例如,完全可以将Adapter模式中的"现存对象"作为新的接口方法参数,来达到适配的目的。
-
Adapter模式本身要求我们尽可能地使用"面向接口的编程"风格,这样才能在后期很方便的适配。
效果
对于类适配器:
-
用一个具体的Adapter类对Adaptee和Target进行匹配。结果是当我们想要匹配一个类以及所有它的子类时,类Adapter将不能胜任工作。
-
使得Adapter可以重定义Adaptee的部分行为,因为Adapter是Adaptee的一个子类。这是类适配器一个主要的作用。
-
仅仅引入了一个对象,并不需要额外的得到Adaptee的实例。
对于对象适配器:
-
允许一个Adapter与多个Adaptee,即Adaptee本身以及它的所有子类(如果有子类的话)同时工作。Adapter也可以一次给所有的Adaptee添加功能。
-
使得重定义Adaptee的行为比较困难。这就需要生成Adaptee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身。
总结
总之,通过运用Adapter模式,就可以充分享受进行类库迁移、类库重用所带来的乐趣。