adapter

第八章 适配器模式(Adapter Pattern)

1 简介

适配器模式(Adapter Pattern)是一种结构模式(Structural Design Pattern)。在日常生活中,人们经常会使用适配器。例如,当去欧洲旅游时,人们需要携带电源插头适配器,因为中国的电源接口与欧洲的电源接口不同。当人们连接电脑显示器时,也可能会使用DVI(Digital Visual Interface)转HDMI(High-Definition Multimedia Interface)适配器。这些适配器的作用和功能与适配器模式的作用和功能十分相似。

2 适配器模式的结构

适配器模式的主要功能是适配不兼容的接口。当开发人员认为需要的功能已基本实现,但是因为接口不同无法直接使用时,可考虑使用适配器模式进行接口适配。适配器模式包含以下四个参与方。

  1. Target表示的是开发人员希望使用的接口。
  2. Adaptee表示的是已实现的功能。但是因为接口不同,开发人员无法直接使用。
  3. Adapter表示的是适配器,实现了Target接口。将Target接口适配到Adaptee接口上。
  4. Client表示的是适配器的使用者。他希望通过Target接口,使用Adaptee的功能。

图一 适配器模式结构图

图一 适配器模式结构图

3 适配器模式示例

我们使用一个日期和时间的例子来说明适配器模式的使用方法。如下面的代码所示,我们定义了一个接口DateFormatter,用于将LocalDate对象格式化为一个可读的字符串。DateFormatterImpl实现了这个接口。DateFormatter是适配器模式中的Adaptee,已实现的功能。

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public interface DateFormatter {
    String formatDate(LocalDate day);
}

public class DateFormatterImpl implements DateFormatter {
    @Override
    public formatDate(LocalDate date) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
        return date.format(formatter);
    }
}

可是,在项目推进过程中,开发人员还需要通过使用年、月、日三个参数的方式格式化一个日期字符串。显然,上述的DateFormatter并不支持这种传入三个参数的接口。因此,我们计划定义一个新的YearMonthDayFormatter接口,但是复用DateFormatter的功能。YearMonthDayFormatter是适配器模式中的Target。

public interface YearMonthDayFormatter {
    String formatYearMonthDay(Integer year, Integer month, Integer day);
}

最后,我们开发一个适配器类DateFormatterAdapter,它实现了YearMonthDayFormatter的接口。因此,开发人员可以用年、月、日三个参数生成格式化字符串。在DateFormatterAdapter中保留了对DateFormatter的引用,因为,DateFormatterAdapter需要使用DateFormatter的功能来实现字符串格式化。DateFormatterAdapter是适配器模式中的adapter。

import java.time.LocalDate;
public class DateFormatterAdapter implements YearMonthDayFormatter {
    private DateFormatter dateFormatter = null;

    public DateFormatterAdapter(DateFormatter dateFormatter) {
        this.dateFormatter = dateFormatter;
    }

    @Override
    public String formatYearMonthDay(Integer year, Integer month, Integer day) {
        return this.dateFormatter.formatDate(LocalDate.of(year, month, day));
    }
}

在使用时,我们不能直接使用DateFormatter接口,而是需要通过YearMonthDayFormatter适配器,将不兼容的DateFormatter接口转换为可以直接使用的YearMonthDayFormatter接口。

public class AdapterPatternExample {
    public static void main(String args) {
        DateFormatter formatter = new DateFormatterImpl();
        YearMonthDayFormatter adapter = new DateFormatterAdapter(formatter);

        String dateString = adapter.format(2020, 10, 15);
    }
}

4 应用举例

在Java标准库中,我们常常使用适配器模式。我们将在本小节中展示两个例子InputStreamReader和Arrays.asList()。

4.1 InputStreamReader

Java标准库中的输入流(java.io.InputStream)和输出流(java.io.OutputStream)中运用了适配器模式。我们以输入流InputStream为例,阅读一下InputStream和InputStreamReader的源代码。类似的应用还存在于其他InputStream中和相应的OutputStream中。如果对它们的实现感兴趣,读者可自行阅读。

java.io.InputStream是Java标准库中的输入流。当调用read()方法时,程序会从输入流中读取数据,并存放在传入的数组参数中。可是,InputStream中的read()方法只能接受字节数组(Byte Array),而开发人员往往希望使用字符数组(Character Array),因为,程序可能处理的是字符串数据。因此,此时遇到了接口不兼容问题。

// OpenJDK 15
package java.io;

public abstract class InputStream implements Closeable {
    ...
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    public int read(byte b[], int off, int len) throws IOException {
        ...
    }
    ...
}

为了解决这个接口不兼容问题,Java标准库还提供了java.io.Reader类,用于读取数据,并存放在字符数组中。Reader类中的read()方法的接口与InputStream中read()方法的功能非常相似,只是参数类型不同而以。

// OpenJDK 15
package java.io;

public abstract class Reader implements Readable, Closeable {
    ...
    public int read(char cbuf[]) throws IOException {
        return read(cbuf, 0, cbuf.length);
    }

    public abstract int read(char cbuf[], int off, int len) throws IOException;
    ...
}

java.io.InputStreamReader使用了适配器模式。它继承自Reader类,实现了read()方法。在InputStreamReader的构造函数中,它接受一个InputStream参数,用于保存读出数据的输入流对象。

// OpenJDK 15
package java.io;

public class InputStreamReader extends Reader {
    private final StreamDecoder sd;
    ...
    
    public InputStreamReader(InputStream in) {
        ...
    }

    public int read(char cbuf[], int offset, int length) throws IOException {
        return sd.read(cbuf, offset, length);
    }
    ...
}

因此,开发人员可以使用InputStreamReader从一个InputStream对象中读取数据,并将其存放在字符数组中。InputStreamReader完成了接口的适配。在这个例子中,InputStreamReader是Adapter;Reader是Target;InputStream是Adaptee。

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;

public class InputStreamReaderExample {
    public void read(InputStream in) throws IOException {
        char[] buf = new char[2048];
        InputStreamReader reader = new InputStreamReader(in);
        reader.read(buf);
    }
}

4.2 Arrays.asList()

java.util.List是Java标准库中用于表示链表的接口,也是Java程序中最为常见的集合接口(Collection)。通常情况下,开发人员可以使用new操作符来创建一个新链表对象。然而,List接口并未提供根据一组元素来创建链表的接口。

例如,当开发人员希望创建一个包含整数1和2的链表时,List接口无法满足这样的需求。开发人员不得不先创建一个空的链表对象,再向这个链表对象中插入元素1和2。这样实现的源代码较为冗长。

为了解决这个问题,Java标准库设计了Arrays.asList()方法。这个方法实际上运用的是适配器模式的思想。asList()方法接受可变个数的参数,返回一个包含传入元素的链表对象。换句话说,用户可以使用asList(1, 2)创建一个包含2个元素的链表。也可以使用asList(1, 2, 3, 4, 5)创建一个包含5个元素的链表。

import java.util.List;
import java.util.LinkedList;
import java.util.Arrays;

public class ListExample {
    public static void main(String[] args) {
        List l1 = new LinkedList();
        l1.add(1);
        l1.add(2);

        List l2 = Arrays.asList(1, 2);
        List l3 = Arrays.asList(1, 2, 3, 4, 5);
    }
}

在java.util.Arrays类中,实际上asList()方法接受可变个数的参数a。在Arrays类中,还实现了一个私有的(private)内部类ArrayList。这个类不是开发人员常用的java.util.ArrayList类,只不过它们恰好名字相同。

Arrays类中的内部类ArrayList继承自AbstractList,实现了List接口。它的构造函数接受一个数组。所以,asList()方法能够直接使用可变参数a创建并返回一个List对象。

// OpenJDK 15
package java.util;

public class Arrays {
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }

    private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        private final E[] a;

        ArrayList(E[] array) {
            a = Objects.requireNonNull(array);
        }
        ...
    }
}

5 小结

本章介绍了适配器模式。当项目中已实现类似的功能,但是又不能直接使用这个功能时,开发人员可以考虑设计和使用适配器模式,进行接口转换。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.