【Design Pattern】Structural - Adapter

Posted by 西维蜀黍 on 2018-11-12, Last Modified on 2022-12-10

Adapter Pattern

  • The adapter design pattern solves problems like:

    • How can a class be reused that does not have an interface that a client requires?
    • How can classes that have incompatible interfaces work together?
    • How can an alternative interface be provided for a class?

    Often an (already existing) class can’t be reused only because its interface doesn’t conform to the interface clients require.

其实,这个思想类似于电源的转换器。比如,有一天,朋友送了我一个有英标插头的电器,我希望能够在家里使用:

  • 希望直接利用墙上的中国标准插头来为这个电器提供电力;

  • 墙上的插口是中国标准插孔,而这个电器拥有英国标准插头;

  • 不希望改造现有的家里的供电系统,即通过自己在墙上造一个英标母插孔来实现对这个电器的电力供应(实在是小题大做了,费时费力,而且没有充分利用现有资源);

  • 同时,也不希望破坏这个新电器的现有结构(毕竟把英标插头剪掉,而通过自己接线把一个中国标准插头重新接到这个电器上的方式,可能会导致漏电安全隐患)。

最终,我们发明了一个叫电源转换器的东西。这样既没有破坏电器上的英标公插头,也没有破坏任何家中墙上的中标母插孔,同时也实现了对新电器的电力供应。

类似地,通过引入一个新类适配器,使得我们既没有改变一个新类的实现,也复用了一组已经存在且满足同一接口的类,同时这个新类在当前场景中能够被复用,而且满足这个接口。


在软件开发中,调用者(Client)虽然可以通过直接调用目标类以访问它所提供的服务(在英国,我们无需转换器即可直接使用这个电器)。

但是,我们希望这个目标新类能被复用的同时(我们能够使用这个电器),不破坏这个目标新类本身的实现(不破坏电器本身),通过某种方式实现,这个目标类也遵循在现有体系中与这个目标类所行使职能类似的现有类的对应接口(在中国也能直接使用这个电器)。

换句话说,这个目标新类虽然可以满足调用者的基本任务完成需求,但它所提供的调用签名不一定是调用者所期望的,这可能是因为这个目标新类中定义的方法名 与 现有类对应的接口中的方法名的不一致导致的。

此时,需要将目标新类需要转换成调用者所期待的接口,才能实现对这个目标新类域现有类的统一。

在适配器模式中,可以通过引入一个包装类,来包装不兼容接口的实体,这个包装类就是适配器(Adapter),适配器所包装的实体就是被适配者(Adaptee),即被适配的类。

当调用者调用适配器的方法时,在适配器的内部将调用被适配者类的对应方法,这个过程对调用者来说是透明的(调用者并不直接访问被适配者类)。

最终,适配器使得接口不兼容的类最终可以一起工作。

UML

The client class that requires a target interface cannot reuse the adaptee class directly because its interface doesn’t conform to the target interface. Instead, the client works through an adapter class that implements the target interface in terms of adaptee:

  • The object adapter way implements the target interface by delegating to an adaptee object at run-time (adaptee.specificOperation()).
  • The class adapter way implements the target interface by inheriting from an adaptee class at compile-time (specificOperation()).

示例

Lloyds银行是一家国际银行,并向全世界提供服务。

  • 对于拥有海外账号(Offshore Account)的用户,税率为0.03%。
  • 对于英国用户,提供两种账号:普通账号(Standard Account)和白金账号(Platinum Account)。且没有税率。

我们作为Lloyds银行系统的开发者,需要对外提供一个的账号信息接口(Account),以提供账号信息查询服务。

UML图

实现思路

  • 为被适配者OffshoreAccount增加一个适配器AccountAdapter,使之遵循Account接口

OffshoreAccount.java

public class OffshoreAccount {
    private double balance;

    /** The tax for the country where the account is */
    private static final double TAX_RATE = 0.04;

    public OffshoreAccount(final double balance) {
        this.balance = balance;
    }

    public double getTaxRate() {
        return TAX_RATE;
    }

    public double getOffshoreBalance() {
        return balance;
    }

    public void debit(final double debit) {
        if (balance >= debit) {
            balance -= debit;
        }
    }

    public void credit(final double credit) {
        balance += balance;
    }
}

Account.java

public interface Account {
    public double getBalance();    
    public boolean isOverdraftAvailable();    
    public void credit(final double credit);
}

AbstractAccount.java

public class AbstractAccount implements Account {
    private double balance;    
    private boolean isOverdraftAvailable;

    public AbstractAccount(final double size) {
        this.balance = size;
    }

    @Override
    public double getBalance() {
        return balance;
    }

    @Override
    public boolean isOverdraftAvailable() {
        return isOverdraftAvailable;
    }

    public void setOverdraftAvailable(boolean isOverdraftAvailable) {
        this.isOverdraftAvailable = isOverdraftAvailable;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + " Balance=" + getBalance()
                + " Overdraft:" + isOverdraftAvailable();
    }

    @Override
    public void credit(final double credit) {
        balance += credit;
    }
}

PlatinumAccount.java

public class PlatinumAccount extends AbstractAccount {

    public PlatinumAccount(final double balance) {
        super(balance);
        setOverdraftAvailable(true);
    }
}

StandardAccount.java

public class StandardAccount extends AbstractAccount {

    public StandardAccount(final double balance) {
        super(balance);
        setOverdraftAvailable(false);
    }
}

AccountAdapter.java

public class AccountAdapter extends AbstractAccount {

    // Adaptee - The class we are adapting from
    private OffshoreAccount offshoreAccount;

    public AccountAdapter(final OffshoreAccount offshoreAccount) {
        super(offshoreAccount.getOffshoreBalance());

        // holds adaptee reference
        this.offshoreAccount = offshoreAccount;
    }

    /**
     * Calculate offshore account balance after deducting the tax owed for
     * offshore account
     */
    @Override
    public double getBalance() {
        final double taxRate = offshoreAccount.getTaxRate();
        final double grossBalance = offshoreAccount.getOffshoreBalance();

        final double taxableBalance = grossBalance * taxRate;
        final double balanceAfterTax = grossBalance - taxableBalance;
        return balanceAfterTax;
    }
}

AdapterTest.java

public class AdapterTest {
    public static void main(String[] args) {

        Account sa = new StandardAccount(2000);
        System.out.println("Account Balance= " + sa.getBalance());
        
        //Calling getBalance() on Adapter
        Account adapter = new AccountAdapter(new OffshoreAccount(2000));
        System.out.println("Account Balance= " + adapter.getBalance());        
    }
}

优缺点

优点

  • 将目标类与适配者类解耦,通过引入一个适配器类来重用现有的适配者类,且无需修改原有代码

缺点

  • 对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类

适用场景

  • 系统需要使用现有的类,而这些类的接口不需要系统的需要
  • 又希望复用这些现有的类

模式应用

Sun公司在1996年公开了Java语言的数据库连接工具JDBC,JDBC使得Java语言程序能够与数据库连接,并使用SQL语言来查询和操作数据。JDBC给出一个客户端通用的抽象接口,每一个具体数据库引擎(如SQL Server、Oracle、MySQL等)的JDBC驱动软件都是一个介于JDBC接口和数据库引擎接口之间的适配器软件。抽象的JDBC接口和各个数据库引擎API之间都需要相应的适配器软件,这就是为各个不同数据库引擎准备的驱动程序。

Reference