はじめに
この記事はJava Advent Calendar 2023の12日目の記事になります。
本記事は、デザインパターンのIteratorパターンについて取り上げ、Javaで実装してみた事例の紹介になります。対象は、デザインパターンについてあまり知らない方向けで、実際に業務で設計する際に意識してもらえるきっかけになればいいと思います。
デザインパターンについて
デザインパターンは、GoF(Gang of four)によって提唱された、ソフトウェア設計の問題や課題に対する解決策を提供するための再利用可能な設計のテンプレートです。ソフトウェア開発において、特定の問題に遭遇した際にデザインパターンを適用することで、効率的で柔軟なソリューションを提供することができます。デザインパターンはいくつかの種類があり、その中でも今回はIteratorパターンについて取り上げます。
Iteratorパターンの概要
Iteratorパターンとは、集約されたオブジェクトを列挙するのに使われるデザインパターンです。配列やオブジェクトなどの要素をfor文等で列挙するときに役立ちます。とはいっても、説明やクラス図を見ただけでは具体的にどういう場面で使うか想像が難しいと思います。
本記事では、ポケモンの世界を例に挙げてこのIteratorパターンについて学んでいきましょう。
Iteratorパターンなしでの実装
まずは、デザインパターンを使用せずにこのような要件をJavaを用いて実装してみようと思います。
- ポケモントレーナーの手持ちのポケモンを全て列挙する
- ポケモンは名前(name)とレベル(level)を要素として持つ
まずは、このようなポケモンのモデルクラスを実装します
Pokemon.java
public class Pokemon {
private String name;
private int level;
public Pokemon(String name, int level) {
this.name = name;
this.level = level;
}
public String getName() { return name; }
public int getLevel() { return level; }
}
各要素のgetterとコンストラクタだけ定義しておきます。
次に、Pokemonクラスを要素に持つ、PokemonListクラスを作成します。
PokemonList.java
public class PokemonList {
private Pokemon[] pokemons;
private int last = 0;
public PokemonList(int size) {
this.pokemons = new Pokemon[size];
}
public void add(Pokemon pokemon) {
pokemons[last] = pokemon;
last++;
}
public Pokemon get(int index) {
return pokemons[index];
}
public int getLastNum() {
return last;
}
}
PokemonListコンストラクタ作成時に、配列の要素の最大値を設定するようにします。
ポケモンの追加は(add)で、最後の要素の添え字を(getLastNum)で取り出せるようにします。
また、配列の要素の i 番目のポケモンを取り出せるようにします(get)
続いて、ポケモントレーナーの手持ちのクラスを作成します。
TrainersHand.java
public class TrainersHand {
private PokemonList pokemonList;
public TrainersHand(int size) {
this.pokemonList = new PokemonList(size);
}
public void addPokemon(Pokemon pokemon) {
pokemonList.add(pokemon);
}
public PokemonList getPokemonList() {
return pokemonList;
}
}
TrainersHandクラスは、先ほど定義したPokemonListクラスを要素として持ちます。TrainersHandクラスのインスタンスを作成するときに、手持ちの数を確定させます。ポケモントレーナーの手持ちは本来は最大6匹なので、必要に応じてエラー処理を入れてください。
(本記事では省略しています)
後は、ポケモンの追加、取得ができるようになっています。
そしてMainクラスの実装です。トレーナーのたんぱんこぞうはコラッタ(Lv.10)、オニスズメ(Lv.9)、ポッポ(Lv.8)を手持ちにいるとします。たんぱんこぞうの手持ちポケモンをfor文を使って列挙してみましょう。
(わりと序盤ってこんな手持ちしてませんかね...)
Main.java
import java.util.Iterator;
public class Main {
public static void main(String[] args) {
TrainersHand trainersHand = new TrainersHand(3);
trainersHand.addPokemon(new Pokemon("コラッタ", 10));
trainersHand.addPokemon(new Pokemon("オニスズメ", 9));
trainersHand.addPokemon(new Pokemon("ポッポ", 8));
PokemonList pokemonList = trainersHand.getPokemonList();
for(int i=0; i< pokemonList.getLastNum(); i++){
Pokemon pokemon = pokemonList.get(i);
System.out.println(pokemon.getName() + " Lv."+ pokemon.getLevel());
}
}
}
実行結果
コラッタ Lv.10
オニスズメ Lv.9
ポッポ Lv.8
このような形で要素の数だけfor文のループを回し、i 番目の要素を取り出すことでリストの要素を出得することが出来ますね。
この要素を逆順に表示したい場合はfor文をこのように書きます。Mainのfor文のロジックに手を加える必要がありますね。
for(int i= pokemonList.getLastNum()-1; i >= 0; i--){
Pokemon pokemon = pokemonList.get(i);
System.out.println(pokemon.getName() + " Lv."+ pokemon.getLevel());
}
実行結果
ポッポ Lv.8
オニスズメ Lv.9
コラッタ Lv.10
Iteratorパターンによる実装
次に、上記の実装をIteratorパターンを用いてみましょう。
以下は、Iteratorパターンのクラス図です。
クラス図
-
Iterator
要素を順番に数え上げるインターフェース(API)です。次の要素が存在するか(hasNext)と、次の要素を得る(next)を定義します。 -
Aggregate
Iteratorを作りだすインターフェース(API)を定めます。要素を順番にスキャンする役割であるiteratorメソッドを定義します。 -
ConcreteAggregate
Iteratorが定めたhasNext、nextを実装するクラスです。 -
ConcreteIterator
Aggregateが定めたiteratorを実装するクラスです
インターフェースの実装
Iterator.java
public interface Iterator {
public abstract boolean hasNext();
public abstract Object next();
}
Aggregate.java
public interface Aggregate {
public abstract Iterator iterator();
}
この2つのインターフェースは、それぞれ実装するためのメソッドを定義しておきます。
続いて、ConcreteIteratorの実装になります。
PokemonListIterator.java
public class PokemonListIterator implements Iterator{
private PokemonList pokemonList;
private int index;
public PokemonListIterator(PokemonList pokemonList) {
this.pokemonList = pokemonList;
this.index = 0;
}
@Override
public boolean hasNext() {
if(index < pokemonList.getLastNum())
return true;
else{
return false;
}
}
@Override
public Object next() {
Pokemon pokemon = pokemonList.get(index);
index++;
return pokemon;
}
}
hasNext()では、PokemonListの要素が最後かどうか判定します。
next()は、次の要素のPokemonを返すようにします。
PokemonListは、Aggregateインターフェースをimplementし、iteratorメソッドを実装します。
PokemonList.java
public class PokemonList implements Aggregate{
private Pokemon[] pokemons;
private int last = 0;
public PokemonList(int size) {
this.pokemons = new Pokemon[size];
}
public void add(Pokemon pokemon) {
pokemons[last] = pokemon;
last++;
}
public Pokemon get(int index) {
return pokemons[index];
}
public int getLastNum() {
return last;
}
@Override
public Iterator iterator(){
return new PokemonListIterator(this);
}
}
Mainクラスは、Iteratorを取得し、for文の実装を以下のようにしてください。
hasNext()で次の要素があるか判定します。次の要素がなくなるまでwhileループを回し、next()で、次の要素を取得します。
Main.java
public class Main{
public static void main(String[] args) {
Iterator it = (Iterator) pokemonList.iterator();
while(it.hasNext()){
Pokemon pokemon = (Pokemon) it.next();
System.out.println(pokemon.getName() + " Lv."+ pokemon.getLevel());
}
}
}
実行結果
コラッタ Lv.10
オニスズメ Lv.9
ポッポ Lv.8
実行結果は、Iteratorを使う前の実装と変わらないですね。
逆順の表示もIteratorでやってみましょう。
PokemonListIterator.java
public class PokemonListIterator implements Iterator{
private PokemonList pokemonList;
private int index;
public PokemonListIterator(PokemonList pokemonList) {
this.pokemonList = pokemonList;
this.index = pokemonList.getLastNum()-1;
}
@Override
public boolean hasNext() {
if(index < pokemonList.getLastNum() && index >= 0)
return true;
else{
return false;
}
}
@Override
public Object next() {
Pokemon pokemon = pokemonList.get(index);
index--;
return pokemon;
}
}
このようにhasNext()とnext()のロジックを変更してあげます。Mainのロジックは変えません。
実行結果
ポッポ Lv.8
オニスズメ Lv.9
コラッタ Lv.10
Mainのwhileループはそのままで実行結果はちゃんと逆順になっていますね。
クラスの型が変更になった場合
次に、Iteratorで回すリストが、ArrayListになった時のパターンとその変更方法についても検討してみます。
変更前のリストの型
Pokemon[] pokemons
変更後のリストの型
ArrayList<Pokemon> pokemons
実装はこのようになります。
PokemonList.java
public class PokemonList implements Aggregate{
private ArrayList<Pokemon> pokemons;
private int last = 0;
public PokemonList(int size) {
this.pokemons = new ArrayList<Pokemon>(size);
}
public void add(Pokemon pokemon) {
pokemons.add(pokemon);
last++;
}
public Pokemon get(int index) {
return pokemons.get(index);
}
public int getLastNum() {
return last;
}
public Iterator iterator(){
return new PokemonListIterator(this);
}
}
型の変更に合わしてあげるだけでよくなりましたね。
Mainクラスは、先ほど同様に変更を加えなくて大丈夫です。Iteratorを使わない場合は、リストの型が違うのでfor文を書き換えないといけないですね。
まとめ
今回は、Iteratorパターンを取り上げ、使わない場合と使った場合の実装の違いについて理解していただけたと思います。Iteratorパターンを使うことで、配列であろうとArrayListであろうとMainの実装とは切り離せます。また、一度作成したIteratorは再利用性が高く、Mainでの数え上げるメソッドを変えずIteratorの部分だけ変更することで動作します。このような設計や考え方は大規模な開発においてとても重要です。
また、本記事では、Iteratorパターンの理解に重きを置くために、あえて拡張for文を用いませんでしたが、Javaの拡張for文って実はIteratorパターンを使って実装されていたりします。ただ、内部でどのような動きをしているのか分かっている状態で使うとより理解が深まるかもしれませんね。