设计模式学习:适配器模式
适配器模式就是将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
我不知道大家在日常编码中是否用过适配器模式,但适配器的工作模式在我们的生活中却非常常见。本文还是用那个最老套却最有代表性的例子来讲解。现在有一种价值1000元、在3孔插座下工作的电器,但用户只有一个2孔的插座,显然这个电器没法用了。我们用代码模拟这个过程。
模拟电器:
模拟电器
public class 电器
{
public void 在三孔插座下播放(string 火线,string 地线,string 零线)
{
Console.Write("正常播放音乐……\n\n");
}
}
用户期待的接口:
用户期待接口
public interface 用户期待接口
{
void 在两孔插座下播放(string 火线, string 地线);
}
重新用1000元买一个在两孔插座下工作的具有同种功能的电器可以满足用户的要求,但代价显然太大,用户只是插座规格不适合,我们只需花十几块钱买个插座转换器转换一下就行了。
Code
public class 适配后的电器 : 电器,用户期待接口
{
public void 在两孔插座下工作(string 火线, string 地线)
{
string 零线 = "";
this.在三孔插座下工作(火线, 地线, 零线);
}
}
现在我们再试一下,用户应该会满意了:
Code
static void Main(string[] args)
{
用户期待接口 收音机 = new 适配后的电器();
收音机.在两孔插座下播放("火线", "地线");
Console.Read();
}
上面的技巧也许你在平时的编码中已经不知不觉地使用了,这就是GOF23中提到的“适配器模式”,我们看一下适配器模式的标准UML图:
UML图与上面的电器代码中的角色对应关系如下:
Taget =〉用户期待接口
Adapter =〉适配后的电器
Adaptee =〉电器
注意,我这里用的是“适配后的电器”,而不是像很多教程中简单的用“适配器”,因为从上周五的讨论中我发现,很多同学在没有提前预习的情况下将“Adapter”等价于我们日常生活中的“变压器”或“插头转换器”,认为它只应该具有电压或插头转化功能,而不应该具有播放功能,单词的直译很容易给没有学过“适配器模式”的同学一些误导,GOF23更多的从他的转换原理类似适配器的工作原理,而事实上并没有转换器这个对象存在,我们还是从它的工作意图并结合类图上来加深理解。
好了下面我们重新梳理一下我们关于电器问题的整个解决过程,首先手边有一个现成的电器,但他的工作方式和用户期待的不一样,我们不能随意改造这个电器,为了最大限度利用手边的资源,我们只有转换它的工作方式,使其和用户期待的一致。代码中我们就是通过适配器模式复用电器类,使原本不能工作的电器可以在用户期待的环境下工作。
现在我们在深入下去,我们已经满足了用户的要求,但我们在看看我们的代码,按照上面的继承关系,“适配后的电器”除了实现“用户期待接口”外还具有“电器”的全部功能,这显然违反了“单一原则”,按照我们以前讲的“7个原则”,应该使用“组合”而不是“继承”来降低对象之间的耦合。
事实上,在GOF23中针对该模式提到了两种结构,一种是“类适配器”,就是我们上面讲的结构,还有一种是“对象适配器”。“对象适配器”的结构图如下:
“对象适配器”通过组合除了满足“用户期待接口”还降低了代码间的不良耦合。在工作中推荐使用“对象适配”。我们重新修改一下“适配后的电器”类:
Code
public class 适配后的电器 : 用户期待接口
{
电器 收音机 = new 电器();
public void 在两孔插座下播放(string 火线, string 地线)
{
string 零线 = "零线";
收音机.在三孔插座下播放(火线, 地线, 零线);
}
}
在很多教材中还提到了适配器的简化模式,就是将适配对象作为参数传递给接口方法。我们还通过修改电器的例子来讲解
Code
public interface 用户期待接口
{
void 在两孔插座下播放(电器 三孔电器,string 火线, string 地线);
}
public class 适配后的电器 : 用户期待接口
{
public void 在两孔插座下播放(电器 三孔电器,string 火线, string 地线)
{
string 零线 = "";
三孔电器.在三孔插座下播放(火线, 地线, 零线);
}
}
static void Main(string[] args)
{
电器 收音机 = new 电器();
适配后的电器 新电器 = new 适配后的电器();
新电器.在两孔插座下播放(收音机, "火线", "地线");
Console.Read();
}
工作原理很简单,大家自己仔细看看。
有很多朋友认为适配器和代理、装饰都是通过组合一个现存对象,通过调用该对象的方法来实现自己的功能,说他们之间很像,其实结构型模式都是继承和组合的方式来实现新的功能,如果单看实现过程,他们的确都很像,但如果从意图上分析,他们的区别就大了。
代理模式着重将复杂部分抽到中间层,通过这个中间层(代理层)来控制对目标对象的访问,要求代理层和目标对象的接口相同。而适配器模式解决的恰恰是接口发生了变化导致现有对象不能工作的情景,通过组合这个现有对象,将现有接口转化为目标接口的。
装饰模式强调的是通过组合来动态扩展对象功能。比如上述的电器收音机,本身具有播放功能,现在有很多种录音机(日语录音机、中文录音机),在这个收音机上组装一种录音机,使其除了播放功能外还具有录音功能,而且组装不同的录音机,将具有不同语言的录音功能。这种需求就是装饰模式的应用场景。它和适配器模式在意图上有明显的不同。
如果仅仅讲“如何实现适配器”那很简单,但要解释“为什么用、什么时候用适配器模式”就难得多,上面是我在现在这个水平对 “适配器模式”的理解,我相信对设计模式每个人因为工作经验的不同会有不同的认识,欢迎大家讨论并纠正我理解上的偏差。