【设计模式】适配器模式 Adapter Pattern
适配器模式在软件开发界使用及其广泛,在工业界,现实中也是屡见不鲜。比如手机充电器,笔记本充电器,广播接收器,电视接收器等等。都是适配器。
适配器主要作用是让本来不兼容的两个事物兼容和谐的一起工作。比如, 通常我们使用的交流电都是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 = "",
PostCode = "",
Street = "Gao xin",
Country = new Country
{
Abbreviation = "zh-CN",
Name = "China",
Number = "",
},
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 = "",
PostCode = "",
Street = "Gao xin",
Country = new Country
{
Abbreviation = "zh-CN",
Name = "China",
Number = "",
},
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#该如何实现呢?
【设计模式】适配器模式 Adapter Pattern的更多相关文章
- 设计模式 - 适配器模式(adapter pattern) 具体解释
适配器模式(adapter pattern) 详细解释 本文地址: http://blog.csdn.net/caroline_wendy 适配器模式(adapter pattern): 将一个类的接 ...
- 设计模式 - 适配器模式(adapter pattern) 枚举器和迭代器 具体解释
适配器模式(adapter pattern) 枚举器和迭代器 具体解释 本文地址: http://blog.csdn.net/caroline_wendy 參考适配器模式(adapter patter ...
- C#设计模式——适配器模式(Adapter Pattern)
一.概述在软件开发中,常常会想要复用一个已经存在的组件,但该组件的接口却与我们的需要不相符,这时我们可以创建一个适配器,在需复用的组件的接口和我们需要的接口间进行转换,从而能够正常的使用需复用的组件. ...
- 乐在其中设计模式(C#) - 适配器模式(Adapter Pattern)
原文:乐在其中设计模式(C#) - 适配器模式(Adapter Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 适配器模式(Adapter Pattern) 作者:webabc ...
- 怎样让孩子爱上设计模式 —— 7.适配器模式(Adapter Pattern)
怎样让孩子爱上设计模式 -- 7.适配器模式(Adapter Pattern) 标签: 设计模式初涉 概念相关 定义: 适配器模式把一个类的接口变换成client所期待的还有一种接口,从而 使原本因接 ...
- 设计模式系列之适配器模式(Adapter Pattern)——不兼容结构的协调
模式概述 模式定义 模式结构图 模式伪代码 类适配器,双向适配器,缺省适配器 类适配器 双向适配器 缺省适配器 模式应用 模式在JDK中的应用 模式在开源项目中的应用 模式总结 主要优点 主要缺点 适 ...
- 二十四种设计模式:适配器模式(Adapter Pattern)
适配器模式(Adapter Pattern) 介绍将一个类的接口转换成客户希望的另外一个接口.Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作.示例有一个Message实体类 ...
- 设计模式(七): 通过转接头来观察"适配器模式"(Adapter Pattern)
在前面一篇博客中介绍了“命令模式”(Command Pattern),今天博客的主题是“适配器模式”(Adapter Pattern).适配器模式用处还是比较多的,如果你对“适配器模式”理解呢,那么自 ...
- 适配器模式(Adapter Pattern)--设计模式
在生活中,想用苹果充电线给安卓的手机充电时,因为两者的接口不一样,会导致充电口无法进行匹配, 这时候,就需要适配器,将安卓的充电口转化为苹果的接口,这样就可以充电啦.已有的类与新的接口不兼容问题是很普 ...
随机推荐
- [深度应用]·实战掌握Dlib人脸识别开发教程
[深度应用]·实战掌握Dlib人脸识别开发教程 个人网站--> http://www.yansongsong.cn/ 项目GitHub地址--> https://github.com/xi ...
- Asp.Net Core 轻松学-使用MariaDB/MySql/PostgreSQL和支持多个上下文对象
前言 在上一篇文章中(Asp.Net Core 轻松学-10分钟使用EFCore连接MSSQL数据库)[https://www.cnblogs.com/viter/p/10243577.html],介 ...
- docker常规操作——启动、停止、重启容器实例
一.启动一个已经停止的容器实例 docker start 容器ID或容器名,建议使用容器ID,容器ID支持模糊查询而容器名称不支持1. 先查看已经暂停的容器实例信息 2. 通过docker start ...
- ubuntu中使用docker部署.netcore2.1
概述 .netcore发布这么久,到现在才在项目中实际运用,之前算是了解一点,一般找工作都会问是否运用过.netcore,软件研发来说,如果这个技术没用过,觉得挺难,其实不难..netcore ...
- 关于数据库管理系统DBMS--关系型数据库(MySQL/MariaDB)
数据库的结构(3种):层次,网状,关系型(用的最多): DBMS的三层模型: 视图层:面向最终用户: 逻辑层:面向程序员或DBA: 物理层:面向系统管理员: 关系型数据库管理系统——RDBMS: 主要 ...
- json转js对象方法,JS对象转JSON方法
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title&g ...
- css控制元素高度自适应
可以采用元素定位 + padding 的方式使特定元素高度自适应. css 样式: html,body{ height:100%; margin:; padding:; } .wrap { heigh ...
- weblogic816 bug list
weblogic816在aix下的补丁汇总: 严重 Patch 8173326,weblogic server挂起,threaddump显示SERVER HANGS TRYING TO CALL LO ...
- winfrom SVG转Imge
svg矢量图的使用,将svg矢量图展示在pictureBox上,拖动可以应用到其他设计软件上,复杂一点,中间涉及到SVG的下载 以及 SVG转化为 图片等操作 效果图如下: 源码下载地址: htt ...
- Ambari安装HDP问题:User root is not allowed to impersonate anonymous.User: hcat is not allowed to impersonate ambari-qa
User root is not allowed to impersonate anonymous 修改hadoop 配置文件 etc/hadoop/core-site.xml,加入如下配置项 < ...