24天学透设计模式之适配器模式

96 阅读12分钟

什么是适配器模式

问题定义

想象一下,你正在开发一个电子书阅读应用,该应用当前已经支持PDF格式的电子书。然而,随着市场需求的变化,你需要增加对EPUB格式电子书的支持。这两种格式的电子书有不同的打开和阅读方式:PDF电子书通过openread方法进行操作,而EPUB电子书则使用loadstart_reading方法。

现有的PdfBook类如下所示:

class PdfBook:
    def open(self):
        # 打开PDF文件的代码...
        pass

    def read(self):
        # 读取PDF文件的代码...
        pass

新的EpubBook类如下所示:

class EpubBook:
    def load(self):
        # 加载EPUB文件的代码...
        pass

    def start_reading(self):
        # 开始阅读EPUB文件的代码...
        pass

在你的应用中,阅读器类使用以下方法来读取电子书:

class Reader:
    def __init__(self, book):
        self.book = book

    def read_book(self):
        self.book.open()
        self.book.read()

从上面的代码可以看出,尽管PdfBookEpubBook都是电子书,但它们的接口——也就是完成相同功能的方法命名不一致。这意味着如果我们想在Reader类中直接使用EpubBook,我们需要改变Reader的实现方式,比如通过判断book的类型来决定调用self.book.open() 还是self.book.load()。这种设计会导致Reader类频繁修改以适应新的电子书格式,违背了开闭原则——软件实体应对扩展开放,对修改关闭。适配器模式就是用来解决这种问题。

适配器模式的应用

针对上述问题,我们可以使用适配器模式。首先,我们定义一个统一的电子书接口,然后创建一个适配器类EpubAdapter,它实现了电子书接口,并且内部封装了EpubBook类:

from abc import ABC, abstractmethod

class Ebook(ABC):  # 统一电子书接口
    @abstractmethod
    def open(self):
        pass

    @abstractmethod
    def read(self):
        pass


class EpubAdapter(Ebook):  # 适配器类
    def __init__(self, epub_book):
        self.epub_book = epub_book

    def open(self):
        self.epub_book.load()

    def read(self):
        self.epub_book.start_reading()

现在,无论是PDF还是EPUB格式的电子书,我们都可以通过同一种方式进行操作了:

pdf_book = PdfBook()
epub_book = EpubAdapter(EpubBook())

def client_code(ebook: Ebook):
    ebook.open()
    ebook.read()

client_code(pdf_book)  # 打开并读取PDF文件
client_code(epub_book)  # 打开并读取EPUB文件

通过使用适配器模式,我们成功地让PdfBookEpubBook共享了一个共同的接口(open和read),而无需修改原有的代码。这也保证了我们的代码遵循了开闭原则,即对扩展开放,对修改封闭。

适配器模式中的角色

  1. 目标(Target):这是客户端期望看到的接口,也就是定义了特定领域的接口。在我们的例子中,目标就是Ebook类:

    class Ebook(ABC):  # 目标接口
        @abstractmethod
        def open(self):
            pass
    
        @abstractmethod
        def read(self):
            pass
    
  2. 适配器(Adapter):这是适配器模式的核心部分。适配器通过包装一个已有的类,提供一个与目标接口兼容的新接口。在我们的例子中,适配器就是EpubAdapter类:

    class EpubAdapter(Ebook):  # 适配器类
        def __init__(self, epub_book):
            self.epub_book = epub_book
    
        def open(self):
            self.epub_book.load()
    
        def read(self):
            self.epub_book.start_reading()
    
  3. 被适配者(Adaptee):这是需要被适配的类。被适配者定义了一个可能不与目标接口兼容的现存接口。在我们的例子中,被适配者就是EpubBook类。

  4. 客户端(Client):使用目标接口进行通信的类。在适配器模式中,客户端通过目标接口和适配器进行通信,而适配器则与被适配者通信,从而使得客户端能够间接使用被适配者的功能。在我们的例子中,客户端是执行client_code(pdf_book)client_code(epub_book)的代码部分。

image.png

应用场景

1. 集成遗留系统

让我们想象一下,你正在为一个现代化的公司工作,这个公司有一套复杂的遗留系统。新需求需要你将新功能集成到这个旧系统中。但遗留系统的接口并不符合你的预期,直接修改可能导致代码混乱甚至引入错误。你可以创建一个适配器,将遗留系统的接口转换成新系统能理解的形式。例如,如果旧系统使用oldMethod(),而新系统使用newMethod(),你可以创建一个适配器类如下:

class Adapter:
    def __init__(self, old_system):
        self.old_system = old_system

    def newMethod(self):
        self.old_system.oldMethod()

2. 设计可插拔组件

当你正在设计一个可插拔的模块或者组件,比如支付模块。你的系统需要处理多种支付方式(如信用卡、PayPal、Apple Pay等),这些支付方式在处理付款时都有自己独特的API和数据格式。你可以创建一个标准的支付接口,并为每种支付方式创建一个对应的适配器:

class PaymentAdapter(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

class CreditCardPaymentAdapter(PaymentAdapter):
    # 实现具体的支付方式...

3. 适配不同的数据源

大型软件系统中经常需要处理来自数据库、文件、云服务等不同数据源的数据,它们可能有各自的接口和数据格式。你可以为每种数据源创建一个适配器,实现统一的数据处理流程。例如,可以定义一个DatabaseAdapter类和FileAdapter类,它们都有一个通用的readData()方法。

总结,适配器模式在解决不兼容接口问题、提高代码的可重用性、简化复杂系统集成以及增强系统的灵活性等方面都发挥了重要作用。

优缺点

适配器模式的优点

  1. 提高了类的复用性:适配器模式可以让原本由于接口不兼容无法一起工作的类共同协作。例如,你可能有一个旧的打印机类,它的接口与新的报告生成器不兼容。通过创建一个适配器,你可以重新使用这个旧的打印机类,而无需对其进行大规模的重构。

  2. 提升了系统的扩展性:适配器模式支持扩展新的适配器来适应新的类。例如,如果你的系统需要支持新的数据格式,只需编写一个新的适配器来转换这种数据格式,而无需修改现有的代码。

  3. 增强了代码的清晰性:适配器模式将所有关于数据转换的复杂代码都封装在适配器内部,使得代码的整洁和清晰。用户只需调用适配器即可完成数据转换,无需关心具体的转换细节。

适配器模式的缺点

  1. 过多地使用适配器会让系统非常零乱:如果一个系统中存在大量的适配器,这可能会导致系统变得复杂且难以理解和管理。例如,如果你有数十个不同的数据源,每个都需要一个适配器,这将使代码库变得非常庞大和混乱。

  2. 会增加系统的复杂性:虽然适配器模式可以解决接口不兼容问题,但也可能引入新的复杂性。例如,你必须确保适配器正确地实现了目标接口,这可能需要额外的测试和验证。此外,当与其他开发者交流时,你可能需要解释为什么选择使用适配器模式,以及它是如何工作的。

适配器模式无法解决的问题

虽然适配器模式在解决接口不兼容的问题上非常有用,但它并不能解决所有问题。以下是一些不能通过适配器模式解决的问题:

1. 深层次的不兼容性

适配器模式主要处理接口不兼容的问题,但如果两个系统存在深层次的不兼容性,比如基础设计理念、核心数据结构或业务逻辑的差异,那么适配器就无法有效工作。这主要是因为适配器模式只负责在表面级别转换调用接口,它不能解决那些根源于系统之间底层设计、数据结构或业务逻辑差异的问题。

例如,假设你有一个使用关系型数据库(比如MySQL)的旧系统,和一个采用非关系型数据库(如MongoDB)的新系统。旧的系统以表格形式存储和查询数据,而新的系统则使用文档样式的数据模型。

你可能想通过编写一个适配器将关系型数据库的数据抽象转换成非关系型数据库可以理解的格式,从而使得新的系统能够对旧的系统进行操作。然而,由于关系型数据库和非关系型数据库在数据组织、索引策略和查询方式等方面存在根本性的区别,仅仅通过适配器模式往往无法实现这种转换。

在这种情况下,适配器模式可能会导致复杂和低效的代码,因为它必须处理两种类型数据库之间的所有差异,并可能需要执行复杂的数据转换。此外,由于关系型数据库和非关系型数据库在事务处理、一致性和数据完整性等方面有不同的策略和保证,因此适配器模式甚至可能无法满足所有的业务需求。

2. 动态增加功能的需求

适配器主要用于连接两个预先存在且不兼容的接口,而并非用于动态地添加新功能。每次新增功能或修改现有功能时,都可能需要改变或创建新的适配器,这会导致代码变得越来越复杂,同时也会增加代码的维护成本。

举例来说,如果你正在开发一个电子商务应用,并打算允许用户通过各种各样的支付方式(如信用卡、电子钱包、银行转账等)进行购物。为了能够支持更多的支付方式,你可能需要为每一种支付方式编写一个特定的适配器。但随着支付方式的不断增加,你将不得不频繁地编写和更新适配器,这会使代码变得过于复杂和难以管理。

在这种情况下,使用其他设计模式,如策略模式或工厂模式,可能会是更好的解决方案。例如,使用策略模式,你可以定义一个通用的支付接口,并为每种支付方式实现这个接口。然后,你可以在运行时根据用户的选择动态地切换支付策略。这样,当需要添加新的支付方式时,只需要实现一个新的支付策略即可,而无需修改任何现有代码。

3. 需要改变源对象行为的场景

适配器模式主要用于在不修改源代码的情况下使两个不同的接口能够协同工作。但是如果你需要改变源对象的行为,那么适配器模式就无法满足这样的需求。

例如,假设你有一个老的库,它提供了一种特定的加密算法。现在,由于安全性的原因,你想要使用一个更新的、更安全的加密算法,但是你并不能修改这个老的库。在这种情况下,即使你使用适配器模式来创建一个新的接口,也无法改变原有的加密算法。在这种场景下,可能需要寻找其他的解决方案,比如使用装饰器模式或者代理模式。

4. 多对多的适配问题

另外一个适配器模式并不能很好处理的问题是多对多的适配问题。适配器模式通常用于将一个接口转换为另一个接口,但是如果一个系统中有多个接口需要转换为另一个系统中的多个接口,那么使用适配器模式可能会导致代码复杂度大幅度提升。

举例来说,假设你正在开发一个多语言支持的应用,这个应用需要将界面和文本从一种语言转换为用户选择的另一种语言。如果只是简单地从一种语言转换为另一种语言,适配器模式就足够了。但是如果需要支持很多种语言之间的相互转换,那么就需要为每一种语言对创建一个独立的适配器,这将导致大量的重复代码和高昂的维护成本。

在这种情况下,可能需要寻找其他的解决方案,比如使用桥接模式或者门面模式,它们可以更好地处理复杂的接口转换问题。

5. 高性能要求

在性能需求极高的系统中,适配器模式可能不是最佳选择。由于适配器需要进行额外的处理,这可能导致一定的性能开销。例如,在一个实时图形渲染系统中,如果每个图形对象都需要通过适配器进行转换,那么这个额外的处理过程可能影响到整体的渲染性能。

总的来说,适配器模式是一个非常实用的设计模式,但并不适应所有场景。我们应该根据具体的问题、需求和环境来选择最合适的设计模式。