【原】从头学习设计模式(七)——适配器模式
一、引入
在系统开发中,我们常常能遇到以下的几种场景:
1.有一个旧系统,我们想要通过重构的方式,对旧系统进行一些升级,但旧系统内部的依赖性和复杂性很高,可谓是“牵一发而动全身”啊,怎么将影响减到最小?
2.我们要开发一套新的系统去集成现有的旧系统,旧系统中的代码和功能不允许改变,新系统如何去适配旧系统的接口完成对接?
3.购买了第三方的成熟组件,如何在自己的系统中有效的和第三方组件作接口?
要解决以上的这些问题,我们可以考虑使用适配器模式来处理。当然你可以看出来这些场景都是在不得已的情况下的补救措施,是无可奈何时之举,正常设计系统时还是要好好考虑未来的扩展性和可维护性。
来看一下适配器模式的标准定义是怎样的。
适配器模式(Adaptor):将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
举个生活中的例子,你想把一个PS/2接口的键盘,外接到笔记本上,怎么办呢?众所周知,笔记本上没有PS/2接口的,只有USB接口。好在,市面上有一种PS2转USB的转接器,两边简单对接就可以正常用了~
这其实就是适配器模式的原理,将使两边接口统一化。
二、类图
下面来看看适配器模式的组成吧。
1) 目标(Target)角色:定义Client 使用的接口。
2) 被适配(Adaptee)角色:这个角色有一个已存在并使用了的接口,而这个接口是需要我们适配的。
3) 适配器(Adapter)角色:这个适配器模式的核心。它将被适配角色已有的接口转换为目标角色希望的接口。
三、类适配器与对象适配器
适配器从实现方式上可以分为两种:类适配器和对象适配器。两者的区别在于,前者采用继承,后者采用组合。
首先,我们来看一下类适配器是个什么样的。
我们有一个新的接口 NewInterface,但是旧系统中的一个类没有实现这个接口,我们用继承这个类,同时实现这个新接口的方法,构造出一个适配器类 Adaptor, Adaptor统一了接口,客户端可以构造一个NewInterface类型的Adaptor实例对象了,这个实例对象同时具有旧类的功能,又符合了接口规范。
1 /// <summary> 2 /// 新接口 3 /// </summary> 4 public interface NewInterface 5 { 6 void WriteMessage(); 7 } 8 9 /// <summary> 10 /// 旧类(与新接口无法对接) 11 /// </summary> 12 public class OldClass 13 { 14 public void Message() 15 { 16 Console.WriteLine("这是旧类的原方法。"); 17 } 18 } 19 20 /// <summary> 21 /// 适配器 22 /// 继承旧类,并实现新接口 23 /// </summary> 24 public class Adpter : OldClass,NewInterface 25 { 26 public void WriteMessage() 27 { 28 base.Message(); 29 Console.WriteLine("这是适配后的新方法。"); 30 } 31 }
模拟客户端调用:
1 static void Main(string[] args) 2 { 3 NewInterface a=new Adpter(); 4 a.WriteMessage(); 5 6 Console.ReadKey(); 7 }
执行结果显示,旧类的方法和新的接口方法都被调用了。
这就是类适配器的实现方法,但是你是否发现了问题呢? 如果我需要一个适配器去包装多个旧类怎么办?在C#中是不支持类的多重继承的,所以下面的写法是无法通过编译的:
public class Adpter : OldClass1,OldClass2,OldClass3,NewInterface
若要解决这个问题,我们可以考虑另外一种实现方式:对象适配器
实现的思路也很简单,我们不采用继承了,只去实现一下新的接口,然后把所有要适配的旧类作为适配器类的成员对象包含到内部就可以了。
保持其他部分的代码不变,我们只修改一下Adpter这个类,代码如下:
1 /// <summary> 2 /// 适配器 3 /// 只实现接口,内部创建旧类的实例对象成员 4 /// </summary> 5 public class Adpter : NewInterface 6 { 7 private OldClass oldClass=new OldClass(); 8 public void WriteMessage() 9 { 10 oldClass.Message(); 11 Console.WriteLine("这是适配后的新方法。"); 12 } 13 }
执行结果是一样的。
四、特殊适配器与缺省适配器
另外,从使用适配器的目的性上讲,又可以分成特殊适配器和缺省适配器。前者是为了复用原有代码并适配当前接口,我们上面的例子都是特殊适配器。缺省适配器是一种缺省实现的一种方法,也就是避免子类为了保持继承的完整性,不得不去实现一些方法,但方法体为空。
前面说过,适配器其实是一种补救的方式,那我们研究一下是什么原因导致我们要用缺省适配器的方法。
在设计模式的六大原则里有一个叫作“最小接口原则”,就是接口的设计要足够小,职责要足够单一,比如一个手机接口包含“打电话”和“发短信”的方法是合理,如果同时包含“打飞机”这个方法,就违背了“最小接口原则”了,因为“打飞机”并不是一个手机通用的功能,我们应该再设计一个智能手机接口去单独包含它。
这个原则搞明白以后,你肯定就知道问题的来源了,没错,就是因为接口功能不够单一,导致接口过大,而继承他的子类可能并不能提供这样的功能,只能出现方法空着的情况了。
比如我们已经有了如下设计不合理的接口:
1 public interface Phone 2 { 3 //打电话 4 void Call(); 5 //发短信 6 void Message(); 7 //玩打飞机 8 void Plane(); 9 }
下面,比如我们要构造一个老年机,代码如下:
1 /// <summary> 2 /// 老年机 3 /// </summary> 4 public class OldPeoplePhone : Phone 5 { 6 public void Call() 7 { 8 Console.WriteLine("老年机可以打电话"); 9 } 10 11 public void Message() 12 { 13 Console.WriteLine("老年机可以发短信"); 14 } 15 16 public void Plane() 17 { 18 //老年机打不了飞机,没有具体实现 19 } 20 }
你看到了Plane()没有什么可以实现的,但为了继承接口,这个空方法必须摆在那。
为了比较好的解决这个问题呢,我们来看一下缺省适配器如何工作。
1 public class PhoneAdaptor : Phone 2 { 3 public virtual void Call() 4 { 5 //留空,子类如果实现可以重写 6 } 7 8 public virtual void Message() 9 { 10 //留空,子类如果实现可以重写 11 } 12 13 public virtual void Plane() 14 { 15 //留空,子类如果实现可以重写 16 } 17 }
1 /// <summary> 2 /// 老年机 3 /// </summary> 4 public class OldPeoplePhone : PhoneAdaptor 5 { 6 public override void Call() 7 { 8 Console.WriteLine("老年机可以打电话"); 9 } 10 public override void Message() 11 { 12 Console.WriteLine("老年机可以发短信"); 13 } 14 }
以后,子类只要继承自适配器类,去实现自己的方法就可以了,避免了书写和保留大量的空方法。
五、总结
适配器模式是一种补救手段,是为了复用已有类的代码并且将其适配到客户端需要的接口上去。
1.第一种类适配器,由于C#不支持多重类继承的方式,所以适用的范围有限。
2.第二种对象适配器,当要适配的对象多于一个的时候考虑使用。
3.第三种缺省适配器,是为了弥补接口过大的历史问题而导致的不得不去实现所有方法,但很多方法只能留空的情况。
好了,本次适配器模式的分享就到此结束了,如果理解有误请不吝赐教,共同提高~ 谢谢您的收看。