🦁

デザインパターン - Iterator

2023/12/12に公開

Javaの実装力をあげるための手段の1つとして、デザインパターンの学習を始めました。

こちらの記事の内容をみて、設計力も実装力もあげたい!良さそう!というやってみようということで事前準備として記事にまとめます。

デザインパターンは、オブジェクト指向で再利用性の高いクラスやライブラリを作るための指針になるものです。

23個のデザインパターンがあり、大きく「生成」「構造」「振る舞い」の 3つのカテゴリに分けられます。

Iteratorパターン

オブジェクトの集合体を表現し、集合体の要素を順番にスキャンしていく処理を実装するためのデザインパターンです。
オブジェクトの「振る舞い」に関するデザインパターンです。

先にIteratorパターンをクラス図で表すと以下のようになります。(mermaidで書いたので、縦長になってます)

Iteratorパターンを表現するためにクラスが担う役割は以下の4つへ分けられます。

  • Iterator(反復子) : 要素を順番にスキャンしていくインターフェースを定義します。
  • ConcreteIterator(具体的な反復子) : Iteratorが定義したインターフェースを実際に実装します。スキャンするために必要な情報を持っている必要があります。
  • Aggregate(集合体) : Iterator役を作り出すインターフェースを定義します。
  • ConcreteAggregate(具体的な集合体) : Aggregateが定義したインターフェースを実際に実装します。ConcreteIteratorのインスタンスを作ります。

で、書籍を読んで調べてもみて、書いてもみましたが、自分の中で腹落ちしなかったので、イメージできる説明をChatGPTへ聞いてみました。

以下、列車の例で説明してくれました。この説明の方がイメージしやすいので結果を引用として貼っておきます。

Iterator (反復子)
反復子は、列車の車両を前から後ろへと順番に見ていく人の役割のようなものです。列車の最初の車両から始めて、次々と見ていきます。
ConcreteIterator (具体的な反復子)
具体的な反復子は、列車を実際に見ていく人のことです。どの車両を今見ているのか、次はどの車両を見るのか、といった情報を持っています。
Aggregate (集合体)
集合体は、列車全体のことを表します。列車を見ていく人(反復子)を用意する役割があります。
ConcreteAggregate (具体的な集合体)
具体的な集合体は、実際の列車のことです。列車の車両の情報を持っていて、列車を見ていく人(具体的な反復子)を用意します。
つまり、反復子と具体的な反復子は、列車の車両を一つずつ順番に見ていく役割、集合体と具体的な集合体は、その列車自体と列車を見ていく人を用意する役割があるということです。

Iterator に必要なインターフェース

Iterableインターフェースでは処理を繰り返す対象を表します。

public interface Iterable<E>{
  public abstract Iterator<E> iterator();
}

また、Iteratorインターフェースではループ変数のような役割を果たします。

public interface Iterator<E>{
  public abstract boolean hasNext();
  public abstract E next();
}

Iteratorを使ったサンプルコード

例えば、学校のクラスを表すコードを表現する際にIteratorパターンを実装します。

まずは、生徒を表すStudentクラスを定義します。

public class Student{
  private String name;
  
  public Student(String name) {
    this.name=name;
  }
  
  public String getName(){
    return this.name;
  }
}

次に、教室を表現するためにClassroomクラスを定義します。Iterableインターフェースを実装するクラスになります。

public class Classroom implements Iterable<Student>{
  private Student[] students;
  private int last = 0;
  
  public Classroom(int maxsize){
    this.students = new Student[maxsize];
  }
  
  public Student getStudentAt(int index){
    return students[index];
  }
  
  public void appendStudent(Student student){
    this.students[last] = student;
    last++;
  }
  
  public int getLength(){
    return last;
  }

  @Override
  public Iterator<Student> iterator(){
    return new ClassroomIterator(this);
  }
}

次に生徒を順番に確認するための、ClassroomIteratorを定義します。

public class ClassroomIterator implements Iterator<Student>{
  private Classroom classroom;
  private int index;
  
  public ClassroomIteratorIterator(Classroom classroom){
    this.classroom = classroom;
    this.index = 0;
  }
  
  @Override
  public boolean hasNext(){
    if(index < classroom.getLength()){
      return true;
    }else{
      return false;
    }
  }
  
  @Override
  public Classroom next(){
    if(!hasNext()){
      throw new NoSuchElementException();
    }
    
    Student student = classroom.getStudent(index);
    index++;
    return student;
  }
}

Iteratorを表現するための準備は整いました。ここまで作成したクラスの関係性をクラス図で表現します。

それではこれまでに定義したIteratorを使って処理を実行してみます。

public class Main{
  public static void main(String[] args){
   // 30名が入る教室(固定配列なので、8名以上は着席できない) 
   Classroom classroom = new Classroom(8);
   classroom.appendStudent(new Student("伊藤優"));
   classroom.appendStudent(new Student("田中一郎"));
   // ....
   // ....
   classroom.appendStudent(new Student("山田隆"));
   
   // 教室にいる生徒を順番に確認する
   Iterator<Student> it = classroom.iterator();
   while(it.hasNext()){
     Student student = it.next();
     System.out.println(student.getName);
   }
    System.out.println("------");
  }
}

では、工事により16名まで入れるように教室が拡張されたとしましょう。 この場合、Classroomの配列は固定長の配列なので、コード上は8名以上に拡張することができません。

Classroomを固定配列ではなく、拡張できるようにArrayListで定義しなおすことにします。以下のように修正できます。

public class Classroom implements Iterable<Student>{
  private ArrayList<Student> students;
  private int last = 0;
  
  public Classroom(int size){
    this.students = new ArrayList<Student>(size);
  }
  
  public Student getStudentAt(int index){
    return students[index];
  }
  
  public void addStudent(Student student){
    this.students.add(student);
    last++;
  }
  
  public int getLength(){
    return last;
  }

  @Override
  public Iterator<Student> iterator(){
    return new ClassroomIterator(this);
  }
}

Classroomの実装が変わったので、呼び出す処理も変更しないと,,,,,と一部抜粋して確認してみます。

public class Main{
  public static void main(String[] args){
   // 16名が入る教室、実装はArrayListなので後から追加が可能
   // Classroomの実装を変更しているので、修正が必要
   Classroom classroom = new Classroom(16);
   classroom.addStudent(new Student("伊藤優"));
   classroom.addStudent(new Student("田中一郎"));
   // ....
   // ....
   classroom.addStudent(new Student("山田隆"));
   
   // classroomの修正に依存しないので、呼び出し時は修正不要
   Iterator<Student> it = classroom.iterator();
   while(it.hasNext()){
     Student student = it.next();
     System.out.println(student.getName);
   }
    System.out.println("------");
  }
}

実はこのコードにはIteratorパターンのメリットが含まれています。
Classroomの実装が変わったにも関わらず、呼び出し時の処理を変更する必要がないところです。

whileループがClassroomの実装に依存していないことがメリットです。

まとめ

今回はIteratorパターンの学習をまとめました。 現代の開発現場でガッツリ使われることはなさそうな気はしていますが、
実装に依存せずにクラスを定義するという考え方自体は、設計の場面で活きてくるはずと思っています。

デザインパターンの学習には「Java言語で学ぶデザインパターン入門第3版」を読んで進めています。

あと22個デザインパターンをまとめる終わるまでは、週1ペースでアウトプット目標にしてみます。

Discussion