设计模式 01 迭代器和适配器模式

作者 柚爸

Java 编程思想看完, 重新强化了一下Java基础之后, 觉得功力有点增加了, 现在可以考虑看设计模式了.

果然, 现在发现可以很容易的就看明白了, 不像几个月以前感觉还是看天书一样. 这次就结合图解设计模式这本书来过一遍.

在看的过程中发现这本书写的相当早, 是在2001年就成书了, 其中的一些Java代码估计还是非常老的版本, 这次也按照自己的想法更新一下其中的代码.

  1. Iterator 迭代器模式
  2. Java提供的迭代器模式接口
  3. 练习
  4. Adapter 适配器模式 – 继承
  5. Adapter 适配器模式 – 委托
  6. 练习

Iterator模式

迭代器模式现在很容易就看懂了, 一个持有其他的类的对象有一个方法返回一个迭代器对象, 就可以了.

先来看书上的实现, 一个书架持有多个书对象, 然后实现一个接口, 返回迭代器对象, 而迭代器对象实现迭代器接口规定的.next()和.hasNext()方法就可以了.

这本书里的模式和设计体现的很好, 几乎所有的设计模式都是先搭接口, 再用实现类, 接口之间的关系其实就是设计模式了, 实现类其实就是模式的具体实现.

先看两个接口, 即类的集合的接口和迭代器的接口:

//集合的接口
public interface Aggregate {

    Iterator iterator();

}
//迭代器的接口
public interface Iterator {

    boolean hasNext();

    Object next();

}

原书这里的代码在接口里还用了 public abstract 修饰, 按照现在的Java接口规范已经没有必要了. 还使用了Object, 在之后强制转型, 实际上在有了内部类之后, 其实可以更方便的使用内部类来编写迭代器了.

之后是数据对象, 集合和迭代器的实现:

//数据对象Book, 很简单
public class Book {

    private String name;

    public Book(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                '}';
    }
}
//书架, 作为持有书对象的一个集合
public class BookShelf implements Aggregate {

    //这个方法很重要, 要返回对于自身的迭代器对象
    @Override
    public Iterator iterator() {
        return new BookShelfIterator(this);
    }

    private int last = 0;

    private Book[] books;

    public BookShelf(int length) {
        books = new Book[length];
    }

    public Book getBookAt(int index) {
        return this.books[index];
    }

    public void appendBook(Book book) {
        if (last == this.books.length) {
            System.out.println("书架已满, 放入失败");
            return;
        }
        books[last] = book;
        last++;
    }

    //这个方法实际上是暴露给迭代器对象, 用于控制迭代器工作的
    public int getLength() {
        return this.last;
    }
}
public class BookShelfIterator implements Iterator {

    //要根据一个书架对象生成对应的迭代器, 所以在构造器中传入一个书架对象
    private BookShelf bookShelf;

    BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
        this.index = 0;
    }

    //然后需要有一个遍历书架的内容的变量
    private int index;

    //这个方法的关键是判断是否还能继续取下一个, 当索引小于书架内书的数量的时候, 就说明可以来取出书对象
    @Override
    public boolean hasNext() {
        return index < bookShelf.getLength();
    }

    //这个是实际取出下一个
    @Override
    public Object next() {
        return bookShelf.getBookAt(index++);
    }
}

使用迭代器的方法, 就是先获取之, 然后通过判断.hasNext()是否为真, 来取出下一个对象:

public class Main {

    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(10);

        for (int i = 0; i < 15; i++) {
            bookShelf.appendBook(new Book(i + ""));
        }
        Iterator bookshelfIterator = bookShelf.iterator();

        while (bookshelfIterator.hasNext()) {
            System.out.println(bookshelfIterator.next());
        }
    }
}

这里可以发现, 每一个集合的迭代器, 都必须包含一个这个集合的对象才能够具体工作, 在这里是通过给迭代器的构造器传入一个集合引用来做到的.

但实际上在现代Java里, 哪个东西可以隐含一个指向另外一个对象的引用呢, 就是非静态内部类了. 每一个非静态内部类对象都有一个引用指向外围类.

所以可以改写一下, 用一个书架对象加上内部类就可以搞定了, 而且代码更加优雅, 语义也更加明显, 迭代器就是和当前的对象绑定的, 而且也实现了对应的接口:

public class BookShelfWithInnerClass implements Aggregate {

    //返回内部类对象
    @Override
    public Iterator iterator() {
        return new BookShelfInnerIterator();
    }

    private int last = 0;

    private Book[] books;

    public BookShelfWithInnerClass(int length) {
        books = new Book[length];
    }

    public Book getBookAt(int index) {
        return this.books[index];
    }

    public void appendBook(Book book) {
        if (last == this.books.length) {
            System.out.println("书架已满, 放入失败");
            return;
        }
        books[last] = book;
        last++;
    }

    public int getLength() {
        return this.last;
    }

    public class BookShelfInnerIterator implements Iterator {

        private int index;

        BookShelfInnerIterator() {
            index = 0;
        }

        @Override
        public boolean hasNext() {
            return index < last;
        }

        @Override
        public Book next() {
            //红字部分是对外围类的引用, 其实也可以省略, 直接调用外围类方法即可.
            return BookShelfWithInnerClass.this.getBookAt(index++);
        }
    }
}

然后可以测试一下带有内部类迭代器的集合对象:

public static void main(String[] args) {
    //测试内部类
    BookShelfWithInnerClass bookShelfWithInnerClass = new BookShelfWithInnerClass(10);
    for (int j = 0; j < 8; j++) {
        bookShelfWithInnerClass.appendBook(new Book(j + ""));
    }
    Iterator innerIterator = bookShelfWithInnerClass.iterator();
    while (innerIterator.hasNext()) {
        System.out.println(innerIterator.next());
    }
}

Java内部的一些带有迭代器的集合对象, 也是使用了该模式. 如果要让增强for语句对我们自己编写的类产生效果, 就不是继承我们自己的接口, 而是Java提供的接口才可以了.

Java提供的迭代器模式接口

在例子中我们自己编写了Aggregate接口用于规范集合一定要提供一个迭代器, 然后编写了Iterator接口, 规定了一定要提供.hasNext()和.next()方法.

可见只要实现这两个接口, 就可以完成迭代器模式. Java对于我们自己编写的这两个接口也提供了两个对应的接口, 实现这两个接口之后, Java的增强for语句就能够作用于我们的代码.

这两个接口分别是:

  1. public interface Iterable<T>, 位于java.lang中, 其中规定了方法: Iterator<T> iterator()
  2. public interface Iterator<E>, 位于java.util中, 其中规定了方法: boolean hasNext()E next()方法

两个接口都支持泛型. 现在想让我们的书架集合被增强for支持, 我们只要将自己的类继承的接口换成这两个就可以:

import java.util.Iterator;

//这里由于我们给定的就是Book对象, 因此不使用泛型参数, 直接固定死类型, 就继承Iterable<Book>接口
public class BookShelfForEachSupported implements Iterable<Book> {

    @Override
    //不需要泛型所以这里类型就定好了
    public Iterator<Book> iterator() {
        return new BookShelfInnerIterator();
    }

    private int last = 0;

    private Book[] books;

    public BookShelfForEachSupported(int length) {
        books = new Book[length];
    }

    public Book getBookAt(int index) {
        return this.books[index];
    }

    public void appendBook(Book book) {
        if (last == this.books.length) {
            System.out.println("书架已满, 放入失败");
            return;
        }
        books[last] = book;
        last++;
    }

    public int getLength() {
        return this.last;
    }

    //内部迭代器类继承Iterator<Book>接口, 实现对应方法
    public class BookShelfInnerIterator implements Iterator<Book> {

        private int index;

        BookShelfInnerIterator() {
            index = 0;
        }

        @Override
        public boolean hasNext() {
            return index < last;
        }

        @Override
        public Book next() {
            return BookShelfForEachSupported.this.getBookAt(index++);
        }
    }
}

这里的泛型要和增强for语句中使用的泛型一致, 来测试一下:

public static void main(String[] args) {
        //测试增强for对我们编写的书架类的支持
        BookShelfForEachSupported bookShelfForEachSupported  = new BookShelfForEachSupported(10);
        for (int j = 0; j < 8; j++) {
            bookShelfForEachSupported.appendBook(new Book(j + ""));
        }
        for (Book book : bookShelfForEachSupported) {
            System.out.println(book);
        }
    }

一切OK, 迭代器模式就搞定了. 我又阅读了一下其他的文章, 包括Java 编程思想, 现在还是比较推荐使用内部类的方式来使用迭代器. 而且都是推荐直接使用外部类的方法返回内部类对象, 而无需直接创建内部类对象.

练习

练习的要求是使用ArrayList作为书架类持有对象的内部集合. 则书架类等于了一个ArrayList的包装, 修改代码也很容易:

import java.util.ArrayList;
import java.util.Iterator;

public class BookShelfUsingArrayList implements Iterable<Book> {

    @Override
    public Iterator<Book> iterator() {
        return new BookShelfInnerIterator();
    }

    //改用ArrayList进行实际存放Book对象的容器
    private ArrayList<Book> books = new ArrayList<>();

    //以下的方法都是对ArrayList API的包装
    public Book getBookAt(int index) {
        return books.get(index);
    }

    public void appendBook(Book book) {
        books.add(book);
    }

    public int getLength() {
        return books.size();
    }

    //内部类在初始化的时候加上一个获取当前总Book数量的变量
    public class BookShelfInnerIterator implements Iterator<Book> {

        private int index;

        private int size;

        BookShelfInnerIterator() {
            index = 0;
            size = getLength();
        }

        @Override
        public boolean hasNext() {
            return index < size;
        }

        @Override
        public Book next() {
            return getBookAt(index++);
        }
    }
}

测试如下:

public static void main(String[] args) {

    //测试使用ArrayLIst的书架类, 无需在构造器中传入长度了
    BookShelfUsingArrayList bookShelfUsingArrayList  = new BookShelfUsingArrayList();
    for (int j = 0; j < 10; j++) {
        bookShelfUsingArrayList.appendBook(new Book(j + ""));
    }
    for (Book book : bookShelfUsingArrayList) {
        System.out.println(book);
    }
}

以后想要编写集合类的时候, 一定要注意随着集合类编写一个迭代器, 可以方便的取出其中的元素进行操作.

Adapter 适配器模式 – 继承

最常见的是什么, 就是电源适配器. 计算机的电源都是使用直流电的, 而市电是交流电. 将计算机的电源当做一种规范, 也就是接口的话, 市电可以看成是现实中已经编写好的具体类或者说功能, 想要让计算机的其他部件能够正确的调用电源接口来获取电源, 我们就需要使用一个电源适配器.

这个适配器实现了电源接口, 这样计算机的其他部件就可以从其中获得所需要的直流电, 而电源适配器另外一端和市电的功能相匹配, 这样就可以工作在市电上, 这就是适配器模式. 即将接口规范与实际功能联系起来. 看起来也有点像一个代理.

适配器有两种模式, 一种是使用继承; 一种是适配器对象持有另外一个对象,也叫做委托模式.

这里先来看看继承模式. 书中的例子是, 一个实际用于显示字符串的Banner类的有若干个方法, 而程序的规范要求是使用Print接口和其中的方法来显示字符串, Banner类和Print接口如下:

public class Banner {

    private String string;

    public Banner(String string) {
        this.string = string;
    }

    public void showWithParen() {
        System.out.print('(');
        System.out.print(string);
        System.out.println(')');
    }

    public void showWithAster() {
        System.out.print('*');
        System.out.print(string);
        System.out.println('*');
    }
}
public interface Print {

    void printWeak();

    void printStrong();
}

由于规范调用的Print接口, 而实际工作的类Banner并没有实现Print接口, 想让Banner类也可以融入我们的程序进行工作, 就可以创建一个适配器类:

public class PrintBannerAdapter extends Banner implements Print {

    public PrintBannerAdapter(String string) {
        super(string);
    }

    @Override
    public void printWeak() {
        showWithParen();
    }

    @Override
    public void printStrong() {
        showWithAster();
    }
}

注意其中的红字部分, 适配器要实现接口, 继承被适配的类. 在被其他按照规范的程序调用Print接口中的方法的时候, 实际工作采用Banner父类的方法来操作. 由于继承, 域和构造器还有方法都可以直接使用Banner类, 方便很多.

测试的程序很简单, 都是只根据Print接口规范来调用, 而适配器类就成功的让原本没有实现接口的类, 也能根据规范得到使用:

public class Main {

    public static void main(String[] args) {
        Print p = new PrintBannerAdapter("saner");
        p.printStrong();
        p.printWeak();

    }
}

可以看到, PrintBannerAdapter的实例使用Print类型, 采用多态调用, 即规范不关心具体的实现类. 而通过适配器, 可以把原本不直接属于规范的类, 纳入到规范中进行使用.

Adapter 适配器模式 – 委托

委托模式只需要略微的修改一下适配器的代码, 让其中持有一个实际提供服务的类即可, 而不是直接继承实际提供服务的类:

public class PrintBannerAdapter2 implements Print {

    //持有一个banner对象, 实例化的时候将字符串交给banner对象
    private Banner banner;

    public PrintBannerAdapter2(String string) {
        this.banner = new Banner(string);
    }

    //方法中实际干活的依然是banner对象
    @Override
    public void printWeak() {
        banner.showWithParen();
    }

    @Override
    public void printStrong() {
        banner.showWithAster();
    }
}

测试代码几乎无需变化, 因为都是统一根据接口调用:

public class Main {

    public static void main(String[] args) {
        Print p = new PrintBannerAdapter2("saner");
        p.printStrong();
        p.printWeak();

    }
}

适配器模式看上去好像增加了代码, 比如为什么不在接口中直接定义好要适配的类呢, 这是由于解耦的需要, 让适配器可以更灵活的工作.特别是软件升级, 新功能使用新开发的部分, 旧功能通过适配器, 交给原来老的稳定的部分, 是一种非常常见的设计模式.

当然, 适配器和实际工作的代码, 其作用必须是一致的, 只是类的具体结构上有所不同, 是不能把一个打印字符串的接口适配画图的功能上去的.

可以将适配器模式用于填补不同API但是相同功能之间的裂缝, 像胶水一样粘合起相同的功能, 所以适配器一是要连接不同的类, 而是需要改变API(比如banner的API就不是直接由Main调用, 而是调用接口的方法). 所以和Bridge以及装饰器模式有所区别.

练习

练习2.1 很简单, 由于针对的是接口, 如果使用PrintBanner类型, 则只能用于这个适配器, 无法作用于其他类型的适配器. 使用了接口之后, 可以作用于所有实现了该接口的适配器. 如果要特别的调用某个适配器的方法, 进行类型转换即可. 一切都是为了解耦啊.

练习2.2, 需要编写一个 将 java.util.Properties 类适配到自行编写的 FileIO 接口中的适配器类. FileIO接口如下:

import java.io.IOException;

public interface FileIO {

    void readFromFile(String filename) throws IOException;

    void writeToFile(String filename) throws IOException;

    void setValue(String key, String value);

    String getValue(String key);

}

上边的接口要抛异常, 不然一会编写具体方法的时候, 也会让你在接口方法上声明异常.

而底层实际干活的是java.util.Properties类, 我们需要编写一个适配器名称叫做FileProperties. 思路其实很简单, 可以采取委托模式:

import java.io.*;
import java.util.Properties;

public class FileProperties implements FileIO {

    //实际干活的java.util.Properties对象
    Properties properties = new Properties();

    //看了一下API, 可以支持Reader对象, 正好用刚学的I/O套壳
    @Override
    public void readFromFile(String filename) throws IOException {
        properties.load(new BufferedReader(new FileReader("file.txt")));
    }

    //写入方法也使用刚学的PrintWriter简便方法
    @Override
    public void writeToFile(String filename) throws IOException {
        properties.store(new PrintWriter(filename), "written by FileProperties");

    }

    //剩下两个方法都是直接套用java.util.Properties的方法
    @Override
    public void setValue(String key, String value) {
        properties.setProperty(key, value);
    }

    @Override
    public String getValue(String key) {
        return properties.getProperty(key);
    }
}

实际使用的时候不管具体实现类, 按照接口规范使用:

import java.io.IOException;

public class Main {

    public static void main(String[] args) {
        FileIO f = new FileProperties();
        try {
            f.readFromFile("file.txt");
            f.setValue("year", "2014");
            f.setValue("month", "6");
            f.setValue("day", "29");
            f.writeToFile("newfile.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

至此看完了第一部分: 适应设计模式. 这一部分作者讲是挑出了最简单的设计模式. 难的还在后边呢, 回想起第一次看这个书真的是稀里糊涂, 到现在很明晰的就看明白了. 功力确实有所增加.

不过不光是Java, 依然推荐要自学计算机的朋友不管怎么样, 先操起C语言然后把CSAPP看了, CSAPP可是真正的易筋经.