设计模式——适配器模式
需求
你想使用一个已经存在的类,但是它的接口不符合你的要求,怎么办?这样的问题在生活中很普遍:现在大部分笔记本电脑使用USB接口,而现在大部分键盘使用PS2接口,可以使用PS2/USB接口转换器把它们接起来。编程也可以采用这种转换器的思想。
定义
适配器模式(Adapter Pattern):将一个类的接口,转换成客户期望的另一个类的接口。适配器让原本接口不兼容的类可以合作无间。别名包装(Wrapper)模式。
适配器模式的实现方法可以概括为:单独设计一个适配器(Adapter)类,包装(Wrapper)需要适配的源(Adaptee),继承/实现需要适配的目标(Target),重写(override)/实现(implements)目标的方法,从而实现接口的转换。
适配器模式由3部分组成:(1)适配器(Adapter)类,实现接口转换;(2)适配源(Adaptee),通常包括接口(IAdaptee,interface或者abstract class)和相应子类(Adpatee);(3)适配目标(Target),通常包括接口(ITarget,interface或者abstract class)与相应子类(Target)。
适配器模式有对象适配器和类适配器两种形式的实现结构。二者不同之处在于:对象适配器实现需要适配目标(Target)的接口(interface或者abstract class),而类适配器直接继承需要适配目标的类。
对象适配器常常容纳一个它包装(Wrapper)的类的实例,在需要实现的目标接口方法里调用被包装对象的物理实体,来实现适配(接口的转换)。
类适配器:这种适配器模式下,适配器包装适配源,直接继承它需要适配的目标类(常常会导致万恶的多重继承),重写目标相应方法实现接口转换。
像上面类适配器原理图中,类适配器(Adapter)继承/实现需要适配的源(Adaptee)和目标(Target),内包装(Wapper)了源和目标的对象实例,重写源和目标的方法来实现双方的匹配转换。
类适配器采用“多继承”的实现方式,带来了不良的高耦合,所以一般不推荐使用。对象适配器采用“对象组合”的方式,更符合松耦合精神。
还有种缺省适配器模式:缺省适配器模式是一种特殊的适配器模式,但这个适配器是由一个抽象类实现的,并且在抽象类中要实现目标接口中所规定的所有方法,但很多方法的实现都是“平庸”的实现,也就是说,这些方法都是空方法。而具体的子类都要继承此抽象类。
对象适配器案例
家里有台USB接口的笔记本电脑和一个PS2接口的键盘,加个PS2/USB的转换器后,电脑就可以使用外接键盘了。
class Program
{
static voidMain(string[] args)
{
// 客户程序:为了给某USB接口的笔记本电脑配一个PS2接口的键盘,中间加了个接口转换适配器
Keyboard keyboard = new Keyboard();
PS2toUSBAdapter adapter = new PS2toUSBAdapter(keyboard);
adapter.WorkWithUSB();
NoteBook notebook = new NoteBook();
notebook.WorkWithUSB();
}
}
// 目标类:USB接口
public interface IUSBPort { void WorkWithUSB();}
public class NoteBook : IUSBPort
{
public void WorkWithUSB() { Console.WriteLine("这台笔记本电脑使用USB接口..."); }
}
// 适配源类:PS2接口
public interface IPS2Port { void WorkWithPS2();}
public class Keyboard : IPS2Port
{
public virtual void WorkWithPS2() { Console.WriteLine("这个键盘使用PS2接口!"); }
}
// 适配器类:Wrapper适配源(Adaptee);继承/实现适配目标(Target)。
public class PS2toUSBAdapter : IUSBPort
{
public PS2toUSBAdapter(IPS2Port ps2Port)
{
this._PS2Port = ps2Port;
}
private IPS2Port _PS2Port;
public void WorkWithUSB() // 调用适配源实例,实现适配目标的方法
{
this._PS2Port.WorkWithPS2();
Console.WriteLine("经过这个适配器的转换,可以连接USB设备了!");
}
}
类适配器案例
仍然是上面的故事,但是代码改写了:
class Program
{
static voidMain(string[] args)
{
// 客户程序:为了给某USB接口的笔记本电脑配一个PS2接口的键盘,中间加了个接口转换适配器
Keyboard keyboard = new Keyboard();
NoteBook notebook = new NoteBook();
PS2toUSBAdapter adapter = new PS2toUSBAdapter(keyboard);
adapter.WorkWithUSB();
}
}
// 目标类:USB接口
public interface IUSBPort { void WorkWithUSB();}
public class NoteBook : IUSBPort
{
public virtual void WorkWithUSB() { Console.WriteLine("这台笔记本电脑使用USB接口..."); }
}
// 适配源类:PS2接口
public interface IPS2Port { void WorkWithPS2();}
public class Keyboard : IPS2Port
{
public void WorkWithPS2() { Console.WriteLine("这个键盘使用PS2接口!"); }
}
// 适配器类:继承/实现适配源(Adaptee)和目标(Target)。
public class PS2toUSBAdapter : NoteBook
{
public PS2toUSBAdapter(IPS2Port ps2Port)
{
this._PS2Port = ps2Port;
}
private IPS2Port _PS2Port;
public override void WorkWithUSB() // 重写适配目标的方法
{
base.WorkWithUSB();
Console.WriteLine("实现PS2/USB接口的转换...");
this._PS2Port.WorkWithPS2();
}
}
优缺点
优点:使用设配器模式,可以将一个系统的接口和本来不相容的另一个系统联系起来,从而使得这两个类能够在一起工作,强调对接口的转换。
缺点:对于对象适配器来说,重新定义适配的类的行为比较困难。而类适配器则不能适配一个类以及它的子类;
适用场景
(1)适配器模式主要应用于“希望复用一些现存的类,但是接口又与复用环境要求不一致的情况”。
(2)其实适配器模式有点无奈之举,在前期设计的时候,我们就不应该考虑适配器模式,而应该考虑通过重构统一接口。
(3)在遗留代码复用、类库迁移等方面非常有用。
适配器模式与装饰者模式
它们都可以用来包装对象,本质区别在于:(1)适配器模式将一个接口转换成另外一个接口。(2)装饰者模式不改变接口,只加入职责。
补充
如果适配目标不让继承,怎么办?