前言:
适配器的最经典的解释就是一个插线板,里面只有2个孔的插座,而电器用的插头是3个孔的。然后用了个适配器。即把插头插在适配器三个孔上。适配器再插到插线板2个孔上。
这是个简单而形象的比喻。最近在看《代码大全2》其中谈到了隐喻,及将代码的设计,或结构的设计等用建筑工程等实际例子做比喻。很有趣!比如上面举的例子,我们会发现一个问题,就是插头是3个孔的,插了2个孔的插座,还有一个孔的功能没有实现,怎么办。对应具体代码中,老系统或老代码提供的接口就少了这样一个实现,你又必需去整合老系统,怎么办?
从上面的比喻中,我们看到适配器主要提供一个转换功能。将提供方的数据转换后提供给接收方。所以适配器模式解决的是接口不兼容模式。当我们可以修改任何一方的接口时,就不需要适配器这个东西了。就如同,我们将两孔插座改成了3孔。改了之后不支持原来的2孔了。或者改造插座,同时提供2孔和3孔。抑或将2孔插头改成3孔插头。这些都意味着需要改动原有的稳定的接口。带来不稳定因素。当产品尚未稳定,尚未发布,尚未投入使用,改造还是可以的。但一旦投入使用,已经稳定的产品,是不建议进行改动的。更适合采用适配器模式。
事实上,适配器模式适用于异构系统和2个稳定而不做改动的系统中的接口不一致时采用。
在短信平台项目中,短信发送就是个典型的适配器模式。
短信的组织、接收者的选择等用户操作部分已经稳定下来,而短信平台将短信提交至短信运营商发送也已稳定。用户操作界面与短信平台均已稳定,并在若干个项目中投入使用。
但用户操作界面系统提供的接口,只是将待发送短信送入数据库待发送列表,而短信平台则提供WebService接收短信并发送。2者并无法直接对接,就像前面提供3孔插头无法插入2孔插座一样。因此我们开发了个适配器,一个后台Windows服务,定期从数据库中读取待发送短信提交至WebService。
从上面的描述来看,适配器模式好像很好理解。但在没有设计直接编码实现的代码世界里,会面临很多问题。就好比提供电源的插座和电器插头是连在一起的,这还不只如此,里面供电的线路异常复杂,可能有好几处电线搅在一起。你可能觉得不可思议,但翻看代码,你就会发现这样的问题,会遍地存在。
首先是插座和插头是连在一起的。也就是代码对提供者和供应者没有进行抽象分开,没有定义双方通信的接口,直接就调用实现。类似于代码中直接New 了一个对象,直接使用。而未定义个接口或抽象类。但这还不是最糟的,还有直接将本该对象实现的代码直接写在方法体内了(面向过程的写法)。类似于,电线内大约与上千根细小的铜线,各个都独立供电。我们在看现实世界的时候很多问题能好理解,然后在代码世界往往缺少对整体设计的理解和把握。
概述
将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。
解决的问题
即Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。
模式中的角色
1 目标接口(Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
2 需要适配的类(Adaptee):需要适配的类或适配者类。
3 适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成目标接口。
模式解读
注:在GoF的设计模式中,对适配器模式讲了两种类型,类适配器模式和对象适配器模式。由于类适配器模式通过多重继承对一个接口与另一个接口进行匹配,而C#、java等语言都不支持多重继承,因而这里只是介绍对象适配器。
适配器模式的类图:
用插座,插头的例子来进行对比:Client类好比电器(比如:电风扇) Target类好比插头,Request方法好比3足插头,Adapter好比3孔2足适配器,Adaptee好比2孔插座。
现实生活中,Client和Target(Reqeust)一般是紧密联系在一起,Adapter 和Adaptee、Client三者均完全独立,可独立更换。
适配器模式代码本身非常简单,我们仍然从代码演进开始一步步走向最终设计:
示例一:面向过程设计
private voidForm1_Load(object sender, EventArgs e)
{
Console.Write("这段文字需要写入日志");
}
直接输出需要输出的文字。这样写,在没有变化的情况下是没有任何问题的。遇到变化时候的适应性很差。电器自带电源。因此我们将此段实现丢给一个类去具体实现。具体变化如下:
示例二:面向对象设计
private voidForm1_Load(object sender, EventArgs e)
{
Logger log =new Logger();
log.WriteLog("这段文字需要写入日志");
}
public classLogger
{
public void WriteLog(string msg)
{
Console.Write(msg);
}
}
将写日志抽象成也类来实现其功能。当需要将日志写入数据库,或写入文件,可以直接在Logger类中修正即可。无需项目中遍地去修改。电器定义了自己的插头,并设计了带有匹配此插头的电源,但可拆卸更换电源。但无法使用其他人提供的电源。此时电源已经有电器零部件的意思了,但未定义电器插头的标准。故无法使用第三方电源。为了扩展电源提供,定义接口标准。具体变化如下:
示例三:定义接口
private void Form1_Load(objectsender, EventArgs e)
{
ILogger log= new Logger();
log.WriteLog("这段文字需要写入日志");
}
public interface ILogger
{
void WriteLog(string msg);
}
public class Logger:ILogger
{
public void WriteLog(string msg)
{
Console.Write(msg);
}
}
正如前面所说,定义接口,形成调用者与被调用者双方的契约。明确被调用者的职责。电器外置电源,将电器与电源分开,并定义了使用的标准插头。电器通过标准插头获取电源。极大的丰富了电源的提供商。
示例四:适配器模式
private void Form1_Load(object sender, EventArgs e)
{
ILogger log= new Logger();
log.WriteLog("这段文字需要写入日志");
}
public interface ILogger
{
void WriteLog(string msg);
}
public class Logger : ILogger
{
ILogger2 logger2 = new Logger2();
public void WriteLog(string msg)
{
logger2.Write(msg, "c://a.txt");
//Console.Write(msg);
}
}
public interface ILogger2
{
void Write(string msg, string type);
}
public classLogger2 : ILogger2
{
public void Write(string msg, stringpath)
{
System.IO.File.WriteAllText(path, msg);
}
}
这步跨度有点大,需要说明的有几点:
ILogger2 和Logger2是另外一个系统或者第三方提供的DLL我们无法改变其内部逻辑。
其次Logger类就是我们所称的适配器类。严格按照我们上面所举的列子的话,它应该独立存在一个DLL中。当使用不同的方式记录日志时,采用不同的DLL(适配器)。使用已有插座提供电源,并找到对应的适配器。
该例子中,Form1_Load为Client即电器,Ilogger 是标准,提供电源,或者称插座标准。Logger 插头适配器,
Logger2 插座。
关于隐喻:
我们对照代码世界和现实世界,还是会发现会有很多不一样的地方。比如插座和插头现实世界中是完全分开的,我们可以看的很清楚。但代码世界中,“插头"和"插座"有可能还是在一起的。同个项目中,同个DLL中。用现实世界的眼光看代码世界,似乎插座和插头连在了一起。而严格按照现实世界来改造代码世界,似乎很恐怖,一个类就有可能一个DLL一个简单项目成百上千个DLL(为了方便随时替换)。当然此处的类比对象是代码中的类对象而非DLL。只是当适配对象和被适配对象同在一个项目(DLL)中,缺乏明确的类对象间职责划分,尤其是自己写的代码,容易直接修改接口,而非扩展适配。
各种实现方式的讨论:
示例一编程风格,这里不做评论,面临改动的时将带来很大工作量,导致系统不稳定,不适合大项目中。
示例二编程风格,遇到需要切换输出日志方式时,我们可以新增一个类,实现新的日志输出方式,或者直接使用现有的日志类。我们只要改造Logger类,即可实现实现现成类。适配的功能。代码如下:
private void Form1_Load(object sender, EventArgs e)
{
Logger log =new Logger();
log.WriteLog("这段文字需要写入日志");
}
public classLogger
{
Logger2 logger2 = new Logger2();
public void WriteLog(string msg)
{
logger2.Write(msg, "c://a.txt");
//Console.Write(msg);
}
}
public class Logger2 {
public void Write(string msg, stringpath)
{
System.IO.File.WriteAllText(path, msg);
}
}
Logger也是个适配器,这电器使用了特定的插头,而插座也非通用插座。每次都要特定的适配器。这就是没有定义标准带来的问题。这个适配器也不通用,只能在这样的一种情境中使用。
5. 模式总结
5.1 优点
5.1.1 通过适配器,客户端可以调用同一接口,因而对客户端来说是透明的。这样做更简单、更直接、更紧凑。
5.1.2 复用了现存的类,解决了现存类和复用环境要求不一致的问题。
5.1.3 将目标类和适配者类解耦,通过引入一个适配器类重用现有的适配者类,而无需修改原有代码。
5.1.4 一个对象适配器可以把多个不同的适配者类适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。
5.2 缺点
对于对象适配器来说,更换适配器的实现过程比较复杂。(可以用工厂模式解决)
5.3 适用场景
5.3.1 系统需要使用现有的类,而这些类的接口不符合系统的接口。
5.3.2 想要建立一个可以重用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
5.3.3 两个类所做的事情相同或相似,但是具有不同接口的时候。
5.3.4 旧的系统开发的类已经实现了一些功能,但是客户端却只能以另外接口的形式访问,但我们不希望手动更改原有类的时候。
5.3.5 使用第三方组件,组件接口定义和自己定义的不同,不希望修改自己的接口,但是要使用第三方组件接口的功能。