适配器
意图
适配器是一种结构设计模式,允许具有不兼容接口的对象进行协作。

问题
假设您正在创建一个股票市场监控应用程序。该应用程序以XML格式从多个来源下载股票数据,然后为用户显示漂亮的图表和图表。
在某些时候,你决定通过集成智能第三方分析库来改进应用。但有一个问题:分析库只能处理JSON格式的数据。

你不能“按原样”使用分析库,因为它期望数据的格式与你的应用程序不兼容。
您可以更改库以使用XML。然而,这可能会破坏一些依赖于该库的现有代码。更糟糕的是,您可能一开始就无法访问库的源代码,这使得这种方法不可能实现。
解决方案
您可以创建适配器.这是一个特殊的对象,它转换一个对象的接口,以便另一个对象能够理解它。
适配器包装其中一个对象,以隐藏发生在幕后的转换的复杂性。被包装的对象甚至不知道适配器。例如,您可以用适配器包装一个以米和公里为单位的对象,该适配器将所有数据转换为英制单位(如英尺和英里)。
适配器不仅可以将数据转换成各种格式,还可以帮助具有不同接口的对象进行协作。下面是它的工作原理:
- 适配器获得一个接口,该接口与现有对象之一兼容。
- 使用此接口,现有对象可以安全地调用适配器的方法。
- 在接收到调用时,适配器将请求传递给第二个对象,但是以第二个对象期望的格式和顺序传递。
有时甚至可以创建一个双向适配器来双向转换调用。

让我们回到我们的股票市场应用程序。为了解决格式不兼容的困境,可以为代码直接使用的分析库的每个类创建XML-to-JSON适配器。然后调整代码,使其仅通过这些适配器与库通信。当适配器接收到调用时,它将传入的XML数据转换为JSON结构,并将调用传递给包装分析对象的适当方法。
真实的模拟

出国旅行前后的手提箱。
当你第一次从美国到欧洲旅行时,在给笔记本电脑充电时你可能会大吃一惊。不同国家的电源插头和插座标准不同。这就是为什么你的美国插头装不下德国插座。使用有美式插座和欧式插头的电源插头适配器可以解决这个问题。
结构
对象适配器
这个实现使用对象组合原则:适配器实现一个对象的接口,并包装另一个对象。它可以用所有流行的编程语言实现。

的客户端包含程序现有业务逻辑的类。
的客户端接口描述其他类必须遵循的协议,以便能够与客户端代码协作。
的服务是一些有用的类(通常是第三方类或遗留类)。客户端不能直接使用这个类,因为它有一个不兼容的接口。
的适配器是一个能够同时使用客户端和服务的类:它实现客户端接口,同时包装服务对象。适配器通过适配器接口接收来自客户机的调用,并将其转换为对包装的服务对象的调用,其格式为适配器能够理解的格式。
只要客户机代码通过客户机接口与适配器一起工作,客户机代码就不会耦合到具体的适配器类。因此,您可以在不破坏现有客户端代码的情况下将新类型的适配器引入程序。当服务类的接口被更改或替换时,这可能很有用:您只需创建一个新的适配器类,而无需更改客户机代码。
类适配器
此实现使用继承:适配器同时从两个对象继承接口。注意,这种方法只能在支持多重继承的编程语言中实现,比如c++。

- 的类适配器不需要包装任何对象,因为它继承了来自客户端和服务的行为。调整发生在被覆盖的方法中。产生的适配器可以用来代替现有的客户机类。
伪代码
这个例子适配器图案是基于方钉和圆孔之间的经典冲突。

使方钉适应圆孔。
适配器假装是一个圆钉,其半径等于正方形直径的一半(换句话说,可以容纳正方形钉的最小圆的半径)。
//假设你有两个兼容接口的类:// RoundHole和RoundPeg。类RoundHole是构造函数RoundHole(半径){…} method getRadius() is //返回洞的半径。方法fits(peg: RoundPeg)返回this.getRadius() >= peg. getradius()类RoundPeg是构造函数RoundPeg(半径){…} method getRadius() is //返回peg的半径。//但是有一个不兼容的类:SquarePeg。SquarePeg类是构造函数SquarePeg(width){…} method getWidth() is //返回方块宽度。//一个适配器类可以让你把方钉子装进圆孔。//它扩展了RoundPeg类,让适配器对象充当圆钉。 class SquarePegAdapter extends RoundPeg is // In reality, the adapter contains an instance of the // SquarePeg class. private field peg: SquarePeg constructor SquarePegAdapter(peg: SquarePeg) is this.peg = peg method getRadius() is // The adapter pretends that it's a round peg with a // radius that could fit the square peg that the adapter // actually wraps. return peg.getWidth() * Math.sqrt(2) / 2 // Somewhere in client code. hole = new RoundHole(5) rpeg = new RoundPeg(5) hole.fits(rpeg) // true small_sqpeg = new SquarePeg(5) large_sqpeg = new SquarePeg(10) hole.fits(small_sqpeg) // this won't compile (incompatible types) small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg) large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg) hole.fits(small_sqpeg_adapter) // true hole.fits(large_sqpeg_adapter) // false
适用性
当您希望使用某些现有类,但其接口与其余代码不兼容时,请使用Adapter类。
适配器模式允许您创建一个中间层类,作为您的代码与遗留类、第三方类或任何其他具有奇怪接口的类之间的转换器。
当您希望重用几个现有的子类,这些子类缺乏一些不能添加到超类中的公共功能时,请使用该模式。
您可以扩展每个子类,并将缺少的功能放到新的子类中。但是,您需要在所有这些新类中复制代码闻起来很难闻.
更优雅的解决方案是将缺少的功能放到一个适配器类中。然后将缺少特性的对象包装到适配器中,动态地获得所需的特性。要做到这一点,目标类必须有一个公共接口,适配器的字段应该遵循该接口。这种方法与装饰模式。
如何实施
确保你至少有两个类的接口不兼容:
- 一个有用的服务类,您不能更改(通常是第三方、遗留或有许多现有依赖项)。
- 一个或几个客户端可以从使用服务类中获益的类。
声明客户端接口并描述客户端如何与服务通信。
创建适配器类并使其遵循客户机接口。现在让所有的方法都为空。
向适配器类添加字段,以存储对服务对象的引用。通常的做法是通过构造函数初始化该字段,但有时在调用适配器的方法时将其传递给适配器更方便。
一个接一个地在适配器类中实现客户机接口的所有方法。适配器应该将大部分实际工作委托给服务对象,只处理接口或数据格式转换。
客户端应该通过客户端接口使用适配器。这将允许您在不影响客户端代码的情况下更改或扩展适配器。
利与弊
-
- 单一责任原则.可以将接口或数据转换代码从程序的主要业务逻辑中分离出来。
-
- 打开/关闭原则.您可以在不破坏现有客户端代码的情况下将新类型的适配器引入程序,只要它们通过客户端接口与适配器一起工作即可。
-
- 代码的总体复杂性增加了,因为您需要引入一组新的接口和类。有时,更改服务类以使其与其余代码相匹配会更简单。