デザインパターンのすゝめ ~Iteratorパターン編~

はじめに

皆さん、こんにちは!PS-SLの織田です。 SIOS Tech Labアドベントカレンダー8日目になります!今回は、『Java言語で学ぶデザインパターン入門』の第1章を読んだ感想をまとめていきたいと思います。購入してからそこそこ時間が経ってしまったのですが、なんとか第1章を読むことができたので内容をまとめていきたいと思います。

Iteratorパターンとは

第1章ではIteratorパターンに関する内容が記されていました。Iteratorパターンとは何かしらの集合があったときに、それらを順に指していき処理を繰り返し実行することです。文字で説明しても何のこっちゃという感じなので、具体的なコードとともに説明していきます。

基本要素:Book.java

public class Book {
    private String name;

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

    public String getName() {
        return name;
    }
}

このクラスは単純なデータホルダーです。本の名前を保持するprivateフィールドと、それを取得するgetName()メソッドを提供します。コレクションに格納される個々の要素を表現する役割を担います。Iteratorパターンにおいては「要素」の役割を果たし、パターン全体の基盤となるシンプルなクラスです。

集約オブジェクト:BookShelf.java

import java.util.Iterator;

public class BookShelf implements Iterable<Book> {
    private Book[] books;
    private int last = 0;

    public BookShelf(int maxsize) {
        this.books = new Book[maxsize];
    }

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

    public void appendBook(Book book) {
        this.books[last] = book;
        last++;
    }

    public int getLength() {
        return last;
    }

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

本棚を表現するクラスで、Iteratorパターンの中核となる「集約」の役割を担います。内部では固定サイズの配列を使って本を管理し、本の追加(appendBook)、指定位置の本の取得(getBookAt)、現在の本の数を取得(getLength)といった基本機能を提供します。

反復子:BookShelfIterator.java

import java.util.Iterator;
import java.util.NoSuchElementException;

public class BookShelfIterator implements Iterator<Book> {
    private BookShelf bookShelf;
    private int index;

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

    @Override
    public boolean hasNext() {
        if (index < bookShelf.getLength()) {
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Book next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        Book book = bookShelf.getBookAt(index);
        index++;
        return book;
    }
}

実際の反復処理を担当するクラスです。BookShelfへの参照と現在の位置を示すindexフィールドを持ち、hasNext()で次の要素の存在を判定し、next()で要素を順次返します。

このクラスの重要な点は、BookShelfの内部構造を知らずに反復処理を行えることです。getLength()とgetBookAt()メソッドを通じてのみBookShelfにアクセスし、直接配列を操作することはありません。

利用例:Main.java

import java.util.Iterator;

public class Main {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.appendBook(new Book("Around the World in 80 Days"));
        bookShelf.appendBook(new Book("Bible"));
        bookShelf.appendBook(new Book("Cinderella"));
        bookShelf.appendBook(new Book("Daddy-Long-Legs"));

        // 明示的にIteratorを使う方法
        Iterator<Book> it = bookShelf.iterator();
        while (it.hasNext()) {
            Book book = it.next();
            System.out.println(book.getName());
        }
        System.out.println();

        // 拡張for文を使う方法
        for (Book book: bookShelf) {
            System.out.println(book.getName());
        }
        System.out.println();
    }
}

Iteratorパターンの使用方法を実演するクライアントクラスです。BookShelfインスタンスを作成し、複数の本を追加した後、二つの異なる方法で反復処理を行います。

一つ目は明示的にiterator()メソッドを呼び出してIteratorを取得し、while文でhasNext()とnext()を使った伝統的な方法です。二つ目はJava 5以降で導入された拡張for文を使った方法で、Iterableインターフェースの実装により自動的に内部でIteratorが使用されます。

いずれにしても繰り返し処理の中ではIteratorのメソッドのみ呼び出されています。ここは伏線なので覚えておいてください。

Iteratorパターンで嬉しいこと

一見すると、冗長な書き方に見えるかもしれませんが、このパターンを使うことで受けられる恩恵があります。例えば、現在のBookShelfクラスは配列で管理をしているため、最初に指定した本棚の大きさ以上のデータは入れられません。そこで、配列ではなくjava.util.ArrayListを使うよう修正するとします。こうなると、「複数のファイルにまたがって修正を実施しなきゃいけないのか…」と思うかもしれませんが、実はBookShelfクラスだけ修正すれば大丈夫です。

BookShelf.java(修正後)

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

public class BookShelf implements Iterable<Book> {
    private List<Book> books;

    public BookShelf(int initialsize) {
        this.books = new ArrayList<>(initialsize);
    }

    public Book getBookAt(int index) {
        return books.get(index);
    }

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

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

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

先述の通り、Mainの繰り返し処理の中ではIteratorのメソッドのみ呼び出されています。つまりBookShelfクラスに依存していないため修正も不要ということです。

もしiteratorパターンを使っていない状況で、配列からjava.util.Listへの修正を行った場合、main関数の繰り返し処理は以下のように変更する必要があります。

Main.java(配列版)

public class Main {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.appendBook(new Book("本A"));
        bookShelf.appendBook(new Book("本B"));
        
        // 配列を取得して処理
        Book[] books = bookShelf.getBooks();
        for (int i = 0; i < bookShelf.getLength(); i++) {
            System.out.println(books[i].getName());
        }
    }
}

Main.java(List版)

import java.util.List;

public class Main {
    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(4);
        bookShelf.appendBook(new Book("本A"));
        bookShelf.appendBook(new Book("本B"));
        
        // Listを取得して処理
        List<Book> books = bookShelf.getBooks(); // Book[]からList<Book>に変更
        for (int i = 0; i < books.size(); i++) { // getLength()からsize()に変更
            System.out.println(books.get(i).getName()); // books[i]からget(i)に変更
        }       
    }
}

上記のコードの通りMain.javaで複数箇所の修正が必要になり、手間がかかるだけでなく、コンパイルエラーが発生する可能性もあります。

  • import文の追加
  • 変数型の変更
  • メソッド呼び出しの変更

まとめ

第1章を読んだ感想としては、「デザインパターンを意識しなくてもコード を書くのはなんとかなりそう。ただ、可読性や保守性はめっちゃ低いなぁ」という感じでした。例えるなら、きれいに整えられた回路(下図①)と、ただ闇雲にジャンク品を使いながら無計画に作られた回路(下図②)のような具合です。デザインパターンを理解していなくても動くものは作れるでしょう。しかし、作りたいものが複雑になればなるほど、デザインパターンなしでは保守・運用の難易度が跳ね上がります。まだまだ読み始めたばかりですが、今後もデザインパターンを学び、保守・運用のしやすいコードを実際の業務でも書けるように頑張りたいと思います。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です