设计模式的征途—7.适配器(Adapter)模式
在现实生活中,我们的笔记本电脑的工作电压大多数都是20V,而我国的家庭用电是220V,如何让20V的笔记本电脑能够工作在220V的电压下工作?答案:引入一个电源适配器,俗称变压器,有了这个电源适配器,生活用电和笔记本电脑即可兼容。
在软件开发中,有时候也会存在这种不兼容的情况,我们也可以像电源适配器一样引入一个称之为适配器的角色来协调这些存在不兼容的结构,这种设计方案即称之为适配器模式。
适配器模式(Builder) | 学习难度:★★☆☆☆ | 使用频率:★★★★☆ |
一、木有源码的算法库
Background : M公司在很久以前曾经开发了一个算法库,里面包含了一些常用的算法,例如排序和查找算法,在进行各类软件开发时经常需要重用该算法库中的算法。在为某学校开发教务管理系统时,开发人员发现需要对学生成绩进行排序和查找。该系统的设计人员已经开发了一个成绩操作接口IScoreOperation,在该接口中声明了排序方法Sort(int[])和查找方法Search(int[],int)。为了提高排序和查找的效率,开发人员决定重用算法库中的快速排序算法类QuickSort和二分查找算法类BinarySearch。但是,由于某些原因,现在M公司开发人员已经找不到该算法库的源代码,无法直接通过复制合粘贴操作来重用其中的代码;部分开发人员已经针对IScoreOperation接口编写代码,如果这时再要求对该接口修改或者要求大家直接使用QuickSort类和BinarySearch类将会导致大量代码需要修改。
因此,M公司开发人员面对这个没有远吗的算法库,遇到了一个幸福而又烦恼的问题:如何在既不修改现有接口又不需要任何算法库代码的基础上实现算法库的重用?
通过分析,不难得知,现在M公司面对的问题有点类似于我们在最开始提到的电压问题,成绩操作接口IScoreOperation有点类似于只支持20V电压的笔记本电脑,而算法库好比220V的家庭用电,这两部分都没法再进行修改,而且它们原本是两个完全不相关的结构。
为了让IScoreOperation接口与已有算法库一起工作,让它们在同一个系统中能够兼容,最好的实现方法是增加一个类似电源适配器一样的适配器角色,通过适配器来协调这两个原本不兼容的结构。
二、适配器模式简介
2.1 适配器模式定义
适配器模式的实现就是把客户类的请求转化为对应适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对于客户类来说是透明的,客户类并不直接访问适配者类。因此,适配器让那些由于接口不兼容而不能交互的类可以一起工作。
适配器(Adapter)模式:将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作。
2.2 适配器模式主要角色
适配器模式一般包含以下3个角色:
(1)Target(目标抽象类):目标抽象类定义了客户所需要的接口,可以是一个抽象类或接口,也可以是一个具体的类。
(2)Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配。适配器类是适配者模式的核心,在适配器模式中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
(3)Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
三、借助适配器重用算法库
3.1 解决方案结构图
其中,IScoreOpertion接口充当抽象目标,QuickSort和BinarySearch类充当适配者,而OperationAdapter充当适配器。
3.2 具体实现
(1)Target(目标抽象类):
/// <summary> /// 目标接口:抽象成绩操作类 /// </summary> public interface IScoreOperation { // 成绩排序 int[] Sort(int[] array); // 成绩查找 int Search(int[] array, int key); }
(2)Adaptee(适配者类):
/// <summary> /// 适配者A:快速排序类 /// </summary> public class QuickSortHelper { 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 = Partition(array, p, r); Sort(array, p, q - 1); Sort(array, q + 1, r); } } public int Partition(int[] array, int p, int r) { int x = array[r]; int j = p - 1; for (int i = p; i <= r - 1; i++) { if (array[i] <= x) { j++; Swap(array, j, i); } } Swap(array,j+1,r); return j + 1; } public void Swap(int[] array, int i, int j) { int t = array[i]; array[i] = array[j]; array[j] = t; } }
public class BinarySearchHelper { 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; // 找到元素返回1 } } return -1; // 未找到元素返回-1 } }
(3)Adapter(适配器类):
/// <summary> /// 适配器:成绩操作适配器类 /// </summary> public class OperationAdapter : IScoreOperation { private QuickSortHelper sortTarget; private BinarySearchHelper searchTarget; public OperationAdapter() { sortTarget = new QuickSortHelper(); searchTarget = new BinarySearchHelper(); } public int Search(int[] array, int key) { return searchTarget.BinarySearch(array, key); } public int[] Sort(int[] array) { return sortTarget.QuickSort(array); } }
(4)Client 客户端测试代码
public class Client { public static void Main(string[] args) { IScoreOperation operation = (IScoreOperation)AppConfigHelper.GetAdapterInstance(); if (operation == null) { return; } int[] scores = { 84, 76, 50, 69, 90, 91, 88, 96 }; int[] result; int score; Console.WriteLine("测试成绩排序结果:"); result = operation.Sort(scores); foreach (int s in result) { Console.Write("{0},", s.ToString()); } Console.WriteLine(); Console.WriteLine("查找是否有90分的人:"); score = operation.Search(scores, 90); if (score == -1) { Console.WriteLine("抱歉,这个真没找到~~~"); } else { Console.WriteLine("恭喜,的确存在90分选手~~~"); } Console.WriteLine("查找是否有92分的人:"); score = operation.Search(scores, 92); if (score == -1) { Console.WriteLine("抱歉,这个真没找到~~~"); } else { Console.WriteLine("恭喜,的确存在92分选手~~~"); } Console.ReadKey(); } }
为了让系统具有良好的灵活性和可扩展性,引入了配置文件和AppConfigHelper类。
其中,将具体的Adapter实例配置在配置文件中,如果需要使用其他的排序算法和查找算法类,可以增加一个新的适配器类,使用新的适配器来适配新的算法,原有代码无需修改。
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="AdapterName" value="Manulife.ChengDu.DesignPattern.Adapter.OperationAdapter, Manulife.ChengDu.DesignPattern.Adapter" /> </appSettings> </configuration>
AppConfigHelper主要用于读取配置文件并通过反射生成实例,可以在不修改客户端代码地情况下使用新的适配器,其具体代码如下:
public class AppConfigHelper { public static string GetAdapterName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["AdapterName"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetAdapterInstance() { string assemblyName = AppConfigHelper.GetAdapterName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } }
编译并运行,结果如下图所示:
四、适配器模式小结
4.1 主要优点
(1)将目标类和适配者类解耦,从而无须修改原有结构(只需新增一个适配器类)
(2)增加了类的透明性(适配者类中的业务实现过程)和复用性(同一个适配者类可以在多个不同的系统中复用)
(3)灵活性和可扩展性很好(借助配置文件和反射机制,可以方便地切换适配器,符合开闭原则)
4.2 应用场景
(1)系统需要使用一些现有的类,而这些类的接口(例如方法名)不符合系统的需要,甚至没有这些类的源码。
(2)想要创建一个可以复用的类,用于一些彼此之间没有太大关联的类,包括一些可能在将来引进的类一起工作。
参考资料
刘伟,《设计模式的艺术—软件开发人员内功修炼之道》