一起学习设计模式--07.适配器模式
前言
有的笔记本电脑工作电压是20V,但是国家标准用电电压是220V,如何让20V的笔记本电脑能够在220V的电压下工作?答案是引入一个电源适配器,俗称充电器/变压器,有了这个电源适配器,生活用电和笔记本电脑即可兼容。
在软件开发中,也存在类似的不兼容的情况,也可以像引入电源适配器一样引入一个被称为适配器的角色来协调这些存在不兼容的结构,这种设计方案就是适配器模式。
一、没有源码的算法库
背景
A公司以前开发了一个算法库,里面包含了一些常用的算法,如排序和查找等算法,在进行各类软件开发时经常需要重用该算法库中的算法。在为某个学校开发教务管理系统时,开发人员发现需要对学生成绩进行排序和查找。该系统的设计人员已经开发了一个成绩操作接口 IScoreOperation,在该接口中声明了排序方法 Sort(int[]) 和查找方法 Search(int[], int) 。为了提高排序和查找效率,开发人员决定重用算法库中的快速排序算法类 QuickSort 和二分查找算法类 BinarySearch ,其中 QuickSort 的 QuickSort(int[]) 方法实现了快速排序,BinarySearch 的 BinarySearch(int[], int) 方法实现了二分查找。
由于某些原因,现在A公司的开发人员已经找不到该算法库的源代码,无法直接通过复制和粘贴来重用其中的代码。部分开发人员已经针对 IScoreOperation 接口编程,如果再要求对该接口进行修改或要求大家直接使用 QuickSort 类和 BinarySearch类将导致大量代码需要修改。
A公司开发人员面对这个没有源码的算法库,遇到一个幸福而又烦恼的问题:如何在既不修改现有接口又不需要任何算法库代码的基础上实现算法库的重用。
通过分析,不难得知,现在A公司面临的问题有点类似之前提到的电压的问题。成绩操作接口 IScoreOperation 好比只支持20V电压的笔记本电脑,而算法库好比220V的家庭用电,这两部分都没有办法再进行修改,而且它们原本是两个完全不相关的结构。如图:
现在需要 IScoreOperation 接口能够和已有算法库一起工作,让他们在同一个系统中能够兼容。最好的实现方法是增加一个类似的电源适配器的适配器角色,通过适配器类协调这两个原本不兼容的结构。
二、适配器的概述
适配器模式可以将一个类的接口和另一个类的接口匹配起来,而无需修改原来的适配者接口和抽象目标类接口。
适配器模式的定义如下:
适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式即可以作为类结构型模式,也可以作为对象结构型模式。
在适配器模式中,通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。根据适配器类和适配者类的关系的不同,适配器模式可以分为对象适配器模式和类适配器模式两种。在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。在实际开发中,对象适配器模式的使用频率更高,其结构如图:
上图可以看到包含3个角色:
- Target(目标抽象类):目标抽象类定义客户所需要的接口,可以是一个抽象类或接口,也可以是具体类。
- Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配。适配器类是适配器模式的核心,在对象适配器模式中,它可以通过继承Target并关联一个Adaptee对象使二者产生联系。
- Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配。适配者一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
上图中可以看出,客户端需要调用Request()方法,但是适配者类Adaptee中没有该该方法,但是有一个SpecificRequest() 却是它需要的方法,两个名称不一样怎么办?这就需要提供一个适配器类Adapter来进行衔接,在适配器类中的Request()方法中去调用适配者的 SpecificRequest() 方法。典型对象适配器代码如下:
public class Adapter : Target {
private Adaptee _adaptee;
public Adapter(Adaptee adaptee){
_adaptee = adaptee;
}
public void Request(){
_adaptee.SpecificRequest();//转发调用
}
}
三、完整解决方案
开发人员决定使用适配器模式来重用算法库中的算法。其基本结构如图:
IScoreOperation 接口充当抽象目标,QuickSort 和 BinarySearch 充当适配者,OperationAdapter 充当适配器,完整代码如下:
/// <summary>
/// 抽象成绩操作类:目标接口
/// </summary>
public interface IScoreOperation
{
/// <summary>
/// 成绩排序
/// </summary>
int[] Sort(int[] array);
/// <summary>
/// 成绩查找
/// </summary>
int Search(int[] array, int key);
}
/// <summary>
/// 快速排序类:适配者
/// </summary>
public class QuickSortService
{
public int[] QuickSort(int[] array)
{
Sort(array, 0, array.Length - 1);
return array;
}
public void Sort(int[] array, int p, int r)
{
int q = 0;
if (p < r)
{
q = Partiion(array, p, r);
Sort(array, p, q - 1);
Sort(array, q + 1, r);
}
}
public int Partiion(int[] a ,int p ,int r)
{
int x = a[r];
int j = p - 1;
for(var i = p; i <= r - 1; i++)
{
if (a[i] <= x)
{
j++;
Swap(a, j, i);
}
}
Swap(a, j + 1, r);
return j + 1;
}
public void Swap(int[] a,int i,int j)
{
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
/// <summary>
/// 二分查找类:适配者
/// </summary>
public class BinarySearchService
{
public int BinarySearch(int[] array, int key)
{
int low = 0;
int high = array.Length - 1;
while (low <= high)
{
int mid = (low + high) / 2;
int midVal = array[mid];
if (midVal < key)
{
low = mid + 1;
}
else if (midVal > key)
{
high = mid - 1;
}
else
{
return 1;
}
}
return -1;
}
}
/// <summary>
/// 操作适配器:适配器类
/// </summary>
public class OperationAdapter : IScoreOperation
{
private QuickSortService _quickSort;
private BinarySearchService _binarySearch;
public OperationAdapter()
{
_quickSort = new QuickSortService();
_binarySearch = new BinarySearchService();
}
public int[] Sort(int[] array)
{
return _quickSort.QuickSort(array);
}
public int Search(int[] array, int key)
{
return _binarySearch.BinarySearch(array, key);
}
}
将适配器的类名添加到配置文件中,如果要使用其他的排序或算法,可以新增一个适配器,然后修改配置文件中的适配器类名为新的适配器即可,这样就无需修改原有代码
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="AdapterName" value="LXP.DesignPattern.Adapter.OperationAdapter"/>
</appSettings>
</configuration>
App.Config 帮助类
public class AppConfigHelper
{
public static object GetAdapter()
{
try
{
var adapterName = ConfigurationManager.AppSettings["AdapterName"];
var type = Type.GetType(adapterName);
return type == null ? null : Activator.CreateInstance(type);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
}
客户端代码:
class Program
{
static void Main(string[] args)
{
var operation = (OperationAdapter)AppConfigHelper.GetAdapter();
int[] scores = { 84, 76, 50, 65, 90, 91, 88, 96 };
int[] result;
int score;
Console.WriteLine("成绩排序结果:");
result = operation.Sort(scores);
//便利输出成绩
foreach (var i in result)
{
Console.Write(i + ",");
}
Console.WriteLine();
Console.WriteLine("查找成绩90:");
score = operation.Search(result, 90);
if (score != -1)
{
Console.WriteLine("找到成绩90");
}
else
{
Console.WriteLine("没有找到成绩90");
}
Console.WriteLine("查找成绩92:");
score = operation.Search(result, 92);
if (score != -1)
{
Console.WriteLine("找到成绩92");
}
else
{
Console.WriteLine("没有找到成绩92");
}
Console.ReadKey();
}
}
编译并运行输出结果:
四、适配器模式总结
适配器模式将现有接口转化为客户类所期望的接口,实现了对现有类的复用。他是一种使用频率非常高的设计模式。
1.主要优点
- 将目标类和适配者类解耦。通过引入一个适配器类来重用现有的适配者类,无需修改原有结构。
- 增加了类的透明性和复用性。将具体的业务实现过程封装在适配者类中,对于客户端而言是透明的,提高了适配者类的复用性,同一个适配者类可以在多个不同的系统中复用。
- 灵活性和扩展性都非常好。通过使用配置文件,可以很方便的更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合开闭原则。
对象适配器的优点:
- 一个对象适配器可以把多个不同的适配者适配到同一个目标。
- 可以适配一个适配者的子类。由于适配器和适配者之间是关联关系,根据里氏替换原则,适配者的子类也可以通过该适配器进行适配。
2.适用场景
- 系统需要使用一些现有类,而这些类的接口不符合系统的需要,甚至没有这些类的源代码。
- 想创建一个可以重复使用的类,用于与一些彼此之间的没有太大关联的类,包括一些可能在将来引进的类一起工作。
如果您觉得这篇文章有帮助到你,欢迎推荐,也欢迎关注我的公众号。
示例代码:
https://github.com/crazyliuxp/DesignPattern.Simples.CSharp