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


