适配器模式在软件开发界使用及其广泛,在工业界,现实中也是屡见不鲜。比如手机充电器,笔记本充电器,广播接收器,电视接收器等等。都是适配器。
适配器主要作用是让本来不兼容的两个事物兼容和谐的一起工作。比如, 通常我们使用的交流电都是220v,但是手机电池能够承载的5v电压,因此直接将我们使用的220v交流电直接接到手机上,手机肯定就坏,第二个作用是匹配交流电插座和手机充电接口不兼容的问题,因此,一个充电器解决了电和手机存在的俩个问题(电压和接口),并使其正常工作。
那么在软件开发过程中也会经常碰到这样的问题,那就是系统都开发好了,突然有一天客户说要接入其它系统的数据,但是当你看到接口接入文档时发现两边的接口都对不上,数据结构定义的也不一样,比如说,我们系统中有个定义的方法叫 GetUserByUserId(int userId) 返回的数据结构是这样定义的:
public class User { public int UserId { get; set; } public string UserName { get; set; } public int Age { get; set; } public string Address { get; set; } public string TelNumber{get;set;} public string MobileNumber { get; set; } }
而对方系统接口也定义了一个方法叫 GetUserInfoById(int id) 但是返回的数据结构长这样子:
public class UserInfo { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public Address Address { get; set; } public string TelphoneNumber { get; set; } public string CellphoneNumber { get; set; } } public class Address { public Country Country { get; set; } public string City{get;set;} public string Street{get;set;} public string Number { get; set; } public Location Location { get; set; } public string PostCode { get; set; } } public class Country { public string Name { get; set; } public string Number { get; set; } public string Abbreviation { get; set; } } public class Location { public long Longitude { get; set; } public long Latitude { get; set; } }
那我们该怎么对接这个外部系统的用户到我们的系统中来呢? 这就是了我们要讨论的适配器(Adapter) 模式了。
一、适配器模式的定义
适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
二、适配器模式的结构图
1、Target(目标抽象类):
目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。
2、Adapter(适配器类):
适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。在类机构中他直接继承target接口和一个Adaptee类来实现。
3、Adaptee(适配者类):
适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
三、适配器模式的经典实现
public abstract class Target { public abstract void Request(); } public class Adaptee { public void specificRequest() { Console.WriteLine("I'm Adaptee method"); } } public class Adapter : Target { private Adaptee _adaptee; public Adapter(Adaptee adaptee) { _adaptee = adaptee; } public override void Request() { _adaptee.specificRequest(); } }
客户端调用代码:
static void Main(string[] args) { Target target = new Adapter(new Adaptee()); target.Request(); Console.ReadKey(); }
结果输出:
四、适配器模式实例
讨论完适配器模式的概念后我们来使用适配器模式解决文中开头提出来的问题, 怎么将UserProvider 接口适配到IUserService接口(注意:这里所说的接口是广义的接口,而不是C#中用I开头定义的接口),有了适配器模式现在就变得简单了,IUserService 接口就是适配器模式的目标抽象类(Target), UserProvider 就是适配器模式的适配者类(Adaptee),我们新建一个适配器类UserAdapter (Adapter) 就可以让它们工作了。结构图如下:
对象结构型实现:
在UserPorvider类中实例化两个UserInfo对象(模拟数据存储在数据库中),假设它就是要接入的数据。那么代码就是这样子:
public class User { public int UserId { get; set; } public string UserName { get; set; } public int Age { get; set; } public string Address { get; set; } public string TelNumber { get; set; } public string MobileNumber { get; set; } } public class UserInfo { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public Address Address { get; set; } public string TelphoneNumber { get; set; } public string CellphoneNumber { get; set; } } public class Address { public Country Country { get; set; } public string City { get; set; } public string Street { get; set; } public string Number { get; set; } public Location Location { get; set; } public string PostCode { get; set; } } public class Country { public string Name { get; set; } public string Number { get; set; } public string Abbreviation { get; set; } } public class Location { public double Longitude { get; set; } public double Latitude { get; set; } } public class UserProvider { private static IDictionary<int, UserInfo> innerDictionary = new Dictionary<int, UserInfo>(); static UserProvider() { innerDictionary.Add(1, new UserInfo { FirstName = "Kevin", LastName = "Durnt", Age = 30, CellphoneNumber = "136xxxx1234", TelphoneNumber = "010-34567890", Id = 1, Address = new Address { City = "Xi'an", Number = "24", PostCode = "710000", Street = "Gao xin", Country = new Country { Abbreviation = "zh-CN", Name = "China", Number = "018", }, Location = new Location { Latitude = 31.123456, Longitude = 35.23456, } } }); innerDictionary.Add(2, new UserInfo { FirstName = "Kobe", LastName = "Durnt", Age = 39, CellphoneNumber = "139xxxx1234", TelphoneNumber = "010-24567890", Id = 2, Address = new Address { City = "Xi'an", Number = "24", PostCode = "710000", Street = "Gao xin", Country = new Country { Abbreviation = "zh-CN", Name = "China", Number = "018", }, Location = new Location { Latitude = 31.123456, Longitude = 35.23456 } } }); } public UserInfo GetUserById(int id) { return innerDictionary[id]; } } public interface IUserService { User GetUserByUserId(int userId); } public class UserAdapter : IUserService { private UserProvider _userProvider; public UserAdapter(UserProvider userProvider) { _userProvider = userProvider; } public User GetUserByUserId(int userId) { UserInfo userInfo = _userProvider.GetUserById(userId); User user = new User(); user.UserId = userInfo.Id; user.UserName = string.Format("{0} {1}", userInfo.FirstName, userInfo.LastName); user.TelNumber = userInfo.TelphoneNumber; user.MobileNumber = userInfo.CellphoneNumber; user.Age = userInfo.Age; user.Address = string.Format("{0} {1}, {2},{3}, Location:{4}, {5}", userInfo.Address.Street, userInfo.Address.Number, userInfo.Address.Country.Name, userInfo.Address.PostCode, userInfo.Address.Location.Latitude, userInfo.Address.Location.Longitude); return user; } }
客户端调用:
static void Main(string[] args) { IUserService target = new UserAdapter(new UserProvider()); User user=target.GetUserByUserId(1); Console.WriteLine("UserId: " + user.UserId); Console.WriteLine("UserName: " + user.UserName); Console.WriteLine("Age: " + user.Age); Console.WriteLine("TelNumber: " + user.TelNumber); Console.WriteLine("MobileNumber: " + user.MobileNumber); Console.Write("Address: " + user.Address); Console.ReadKey(); }
输出结果:
反射+配置实现热替换
为了达到灵活配置的目的,其实在很多时候,客户端不需要知道第三方接口长什么样,因此,在适配器类里面可以隐藏掉调用第三方代码的细节,那么对Adaptee的实例化直接放到Adapter里,因此,客户端直接依赖高层抽象Target就可以了,这样就可以随时将Adaptee 替换掉, 并且我们可以使用配置+反射来达到这种动态替换的效果。下面我们稍加修改UserAdapter类,并加一个配置来完成这个设想:
A、在UserAdapter构造里去掉类型为UserProvider 的参数,UserAdapter变成这样了:
public class UserAdapter : IUserService { private UserProvider _userProvider; public UserAdapter() { _userProvider = new UserProvider(); } public User GetUserByUserId(int userId) { UserInfo userInfo = _userProvider.GetUserById(userId); User user = new User(); user.UserId = userInfo.Id; user.UserName = string.Format("{0} {1}", userInfo.FirstName, userInfo.LastName); user.TelNumber = userInfo.TelphoneNumber; user.MobileNumber = userInfo.CellphoneNumber; user.Age = userInfo.Age; user.Address = string.Format("{0} {1}, {2},{3}, Location:{4}, {5}", userInfo.Address.Street, userInfo.Address.Number, userInfo.Address.Country.Name, userInfo.Address.PostCode, userInfo.Address.Location.Latitude, userInfo.Address.Location.Longitude); return user; } }
B. 在App.config中加入如下配置:
<appSettings> <add key="Adapter" value="DesignPattern.Adapter.UserAdapter"/> </appSettings>
C.在代码中使用反射得到具体的Adapter 类,然后调用相应方法:
static void Main(string[] args) { var setting = ConfigurationSettings.AppSettings["Adapter"]; Assembly assembly=Assembly.GetExecutingAssembly(); IUserService target = assembly.CreateInstance(setting) as IUserService; User user=target.GetUserByUserId(1); Console.WriteLine("UserId: " + user.UserId); Console.WriteLine("UserName: " + user.UserName); Console.WriteLine("Age: " + user.Age); Console.WriteLine("TelNumber: " + user.TelNumber); Console.WriteLine("MobileNumber: " + user.MobileNumber); Console.Write("Address: " + user.Address); Console.ReadKey(); }
结果:
类结构实现
上面的adapter是对象结构型的实现。adapter 还可以是类结构型模式, 类适配器和对象适配器的不同之处就是适配器与适配者的关系不同。对象适配器,适配器与适配者之间是关联关系,而类适配器,适配器与适配者之间是继承关系。
下来我们使用类结构来实现上面的需求:
public class UserClassAdapter : UserProvider, IUserService { public User GetUserByUserId(int userId) { UserInfo userInfo =this.GetUserById(userId); User user = new User(); user.UserId = userInfo.Id; user.UserName = string.Format("{0} {1}", userInfo.FirstName, userInfo.LastName); user.TelNumber = userInfo.TelphoneNumber; user.MobileNumber = userInfo.CellphoneNumber; user.Age = userInfo.Age; user.Address = string.Format("{0} {1}, {2},{3}, Location:{4}, {5}", userInfo.Address.Street, userInfo.Address.Number, userInfo.Address.Country.Name, userInfo.Address.PostCode, userInfo.Address.Location.Latitude, userInfo.Address.Location.Longitude); return user; } }
仅仅只需要需要将UserAdapter和UserProvider的关系改成集成就可以了。 输出结果和之前是一样的。
在C#中由于类只能是单继承关系, 一个类只能继承自一个类,但可以继承多个接口,如果Target角色是类,Adaptee也是类的话就不能使用类结构模式。
五、适配器模式的缺点
A. 类结构适配器和对象结构适配器共有的优点:
-
将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
- 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则OCP”。
B.除了共有的优点外,类适配器还有如下优点:
- 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
C.除了共有的优点外,对象适配器还有如下优点:
- 一个对象适配器可以把多个不同的适配者适配到同一个目标。
- 可以适配一个适配者的父类,由于适配器和适配者之间是关联关系,根据“里氏代换原则LSP”,适配者的子类也可通过该适配器进行适配。
六、适配器模式的缺点
A.类适配器的缺点
- 由于C#不支持类的多继承,一次最多只能适配一个适配者类,不能同时适配多个适配者。
- 适配者类不能为最终类,C#中不能为sealed类,这样无法继承了。
- 在C#语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。其实这些都是单类继承的语言特性造成的。
B.对象适配器的缺点
- 与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂, 另一种方法是直接在适配器类中将相应的方法重新实现掉。
七、适配器模式的使用场景
- 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
- 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
- 在调用第三方接口是,和现有的系统模型不配是可以使用Adapter模式将模型转化一直。
八、扩展-Default Adapter Parttern
在使用适配器模式的时候经常会碰到一类场景,就是已有的类的所有方法都都正常工作,但是只有那么几个方法需要调用第三方的几个系统提供的API,这时我们使用继承在适配器类里重新实现一遍工作量太大。这就要使用适配器模式的一个变体。这就是默认适配器,默认适配器上Target类是一个具体的类,实现大多数方法,甚至所有方法,但都是成虚方法,这样在适配器中有选择的重写Target中的方法就可以了。这种变体在实践中继承使用。也是很有用的一种模式。
会不会存在一个多功能的双向适配器呢(比如A系统对接B系统,同时B系统也要对接A系统)? 如果用C#该如何实现呢?