datchの日記

気がついたら社会人。気になる技術的なことについて少しずつ書いていけたらと思っております。

【デザインパターン】コンパウンドパターン【事前学習編2】

参考書



さらっと読める「OREILLY Head First デザインパターン」を参考にしながら書いています。
しばらくはこいつを参考にデザインパターンについて書いていくよ!

前回までの確認



【デザインパターン】コンパウンドパターン【事前学習編1】 - datchの日記
鴨シミュレータを作成するために今まで習った様々なデザインパターンを適用してきました。

  1. Adapter
  2. Decorator
  3. Factory (AbstractFactory)

事前学習



前回の最後はAbstractFactoryパターンを適用し、鴨の鳴き声のカウンティングを行うクラスの生成をFactoryに集中させました。
これによって毎回DecoratorのQuackCounterクラスの生成を記述する必要がなくなり、クラスの生成に関する変更に対して柔軟にすることが出来ます。

Iterator/Compositeパターン

現在のシミュレータの以下のコードを見て下さい。

Quackable mallardDuck = duckFactory.createMallardDuck();
Quackable redheadDuck = duckFactory.createRedheadDuck();
Quackable ducCallk = duckFactory.createDuckCall();
Quackable gooseDuck = duckFactory.createRubberDuck();
Quackable gooseDuck = new GooseAdapter(new Goose());

simulate(mallardDuck);
simulate(redheadDuck);
simulate(duckCall);
simulate(rubberDuck);
simulate(gooseDuck);

インスタンスの生成に関しては処理を一元化することが出来ましたが、
生成とシミュレーションの処理が繰り返しで行われており、これではクラスが増えた時に面倒です。

ではここでCompositeデザインパターンを使ってみましょう。
Compositeパターンはオブジェクトの集合を単体のオブジェクトとして扱うことが可能にするパターンです。

public class Flockimplements Quackable{
  ArrayList quackers = new ArrayList();

  public void add(Quackable quacker){
    quackers.add(quacker);
  }

  public void quack(){
    Iterator iterator = quackers.iterator();
    while(iterator.hasNext()){
      Quackable quacker = (Quackable)iterator.next();
      quacker.quack();
    }
  }
}

Compositeパターンを使用したクラスを作成しました。
後はこれをDuckSimulatorクラスに適用していきます。

public class DuckSimulator{
  void simulate(AbstractDuckFactory duckFactory){
    Quackable mallardDuck = duckFactory.createMallardDuck();
    Quackable redheadDuck = duckFactory.createRedheadDuck();
    Quackable ducCallk = duckFactory.createDuckCall();
    Quackable gooseDuck = duckFactory.createRubberDuck();
    Quackable gooseDuck = new GooseAdapter(new Goose());

    Flock flockOfDucks = new Flock(); // 様々な鴨を管理するCompositeパターンです

    flockOfDucks.add(redheadDuck);
    flockOfDucks.add(duckCall);
    flockOfDucks.add(rubberDuck);
    flockOfDucks.add(gooseDuck); // Adaptorを通したものでも同様に処理が可能!

    Flock flockOfMallards = new Flock(); // これは小さなマガモの手段を管理するCompositeです

    Quackable mallardOne = duckFactory.createMallardDuck();
    Quackable mallardTwo = duckFactory.createMallardDuck();
    Quackable mallardThree = duckFactory.createMallardDuck();
    Quackable mallardFour = duckFactory.createMallardDuck();

    flockOfMallards.add(mallardOne);
    flockOfMallards.add(mallardTwo);
    flockOfMallards.add(mallardThree);
    flockOfMallards.add(mallardFour);

    flockOfDucks.add(flockOfMallards); // 覚えていますか?Compositeパターンは自身に同様のCompositeを追加することが出来ます!

    simulate(flockOfDucks); // これは様々な鴨の鳴くだけでなく、マガモの集団も鳴きます

    simulate(flockOfDucks); // もちろん、同様の呼び出しでマガモの集団だけを鳴かせることも出来ます
  }
}

これにより、鴨のシミュレーションが呼び出し一つで行えるようになりました。
追加の記述が面倒、これなら普通にsimulate()を複数書いたほうが短くないか?という疑問もあるでしょう。

しかし、この鴨の呼び出しが何度も行われるようなケースではCompositeパターンを使うことでもコードの記述量も減り、使いやすいコードにすることが出来るでしょう。

Observerパターン

どうやら新しい要求が来たようで、逆に個々の鴨も管理の対象に置きたいようで、鴨が鳴いた瞬間を知りたいそうです。
とある動作をした時に、その動作をしたことを知るパターン。
そう、ここでObserverパターンの使い所です。

まずはObserverableという監視対象に適応したいインターフェースを作成します。

public interface QuackObservable{
  public void registerObserver(Observer observer);
  public void notifyObservers();
}

次にQuackableにこのインターフェースを継承します。*1

public interface Quackable extends QuackObservable {
  public void quack();
}

QuackableにQuackObservableを追加した事により、Quackableインターフェースを実装しているクラスは、registerObserver()とnotifyObservers()の具体的な処理を記述する必要性が出てきました。
しかし、すべてのクラスに詳細な処理を記述しているのは非常に手間がかかり、処理も重複する可能性があります。
ここでは、Observableというヘルパークラスを作成することにより、他のクラスにコンポジションすることで実際の処理を一度記述するだけでよくなります。

では、実際にObservableクラスを見て行きましょう。

public class Observable implements QuackObservable {
  ArrayList<Observer> observers = new ArrayList<Observer>();
  QuackObservable duck;
 
  public Observable(QuackObservable duck) {
    this.duck = duck;
  }
  
  // 通知するオブザーバを登録する
  public void registerObserver(Observer observer) {
    observers.add(observer);
  }
  
  // オブザーバに通知するための関数
  public void notifyObservers() {
    Iterator<Observer> iterator = observers.iterator();
    while (iterator.hasNext()) {
      Observer observer = iterator.next();
      observer.update(duck);
    }
  }
}

コンストラクタの部分で実体のQuackObservableを保持することで、このクラスに処理を集中させることが出来るのがわかると思います。*2
実際のコードを見て、Observableがどのように使われるか見てみましょう。

public class MallardDuck implements Quackable {
  Observable observable;
 
  public MallardDuck() {
    observable = new Observable(this);
  }
 
  public void quack() {
    System.out.println("Quack");
    notifyObservers();
  }
 
  public void registerObserver(Observer observer) {
    observable.registerObserver(observer);
  }
 
  public void notifyObservers() {
    observable.notifyObservers();
  }
}

public class Flock implements Quackable {
  ArrayList<Quackable> ducks = new ArrayList<Quackable>();
  
  public void add(Quackable duck) {
    ducks.add(duck);
  }
  
  public void quack() {
    Iterator<Quackable> iterator = ducks.iterator();
    while (iterator.hasNext()) {
      Quackable duck = (Quackable)iterator.next();
      duck.quack();
    }
  }
   
  public void registerObserver(Observer observer) {
    Iterator<Quackable> iterator = ducks.iterator();
    while (iterator.hasNext()) {
      Quackable duck = (Quackable)iterator.next();
      duck.registerObserver(observer);
    }
  }
  
  public void notifyObservers() { }
}

registerObserver()・notifyObservers()の処理を受け取った時、Observableにその処理を流しているのがわかると思います。
処理を委譲しているんですね。

まだ終わっていません。
今は通知を行う側しか存在しないので、その通知を受け取るObserverクラスを作成します!

public interface Observer {
  public void update(QuackObservable duck);
}

次にこのクラスの具象クラスが必要になるので、Quackologistというクラスを作成します。
このクラスにより、鴨が鳴くと、Observerとしてどの鴨が鳴いたかを知ることが出来るようになります!

public class Quackologist implements Observer {
 
  public void update(QuackObservable duck) {
    System.out.println("Quackologist: " + duck + " just quacked.");
  }
}

さて、Observerが完成したので、実際に鴨シミュレータでどのように動作するかを見てみましょう。
simulate関数の中身を抜粋して書いていきます。

Quackologist quackologist = new Quackologist();
flockOfDucks.registerObserver(quackologist);

simulate(flockOfDucks);

Observerを作ってflockOfDucksに監視者として登録させているだけです。
ここで驚きなのは、flockOfDuckというCompositeに登録するだけですべての鴨を監視できていることです。
Compositeを使わなければ、ここで沢山registerObserverを書かなければいけなかったことを考えると、かなり処理が簡略化しているのが伺えますね!

次回


  1. Adapter
    ガチョウも鴨と同様に扱えるようにした
  2. Decorator
    鴨の鳴き声を数えられる機能を追加した
  3. Factory(AbstractFactory)
    インスタンスの生成を一元して管理するようにした
  4. Composite
    繰り返しの処理をグループ管理した
  5. Observer
    処理を監視した

これらのパターンを順々に適応していった。

このような感じでデザインパターンを複雑に組み合わせることで、シンプルなコードが出来上がります!
自分もこんな風にデザインパターンを利用出来るようにしたいですね。

しかし、これはまだCompoundパターンでは無いみたいなのです!
この本で触れるCompoundパターンとは我々、Webエンジニアがよく触るMVCを例にして紹介しています!
次回はMVCについて触れて始めていくよ!

*1:自分はinterfaceにinterfaceがextends出来るということをよくわかっていなかったので、最初にこれを見た時は非常に混乱しました

*2:この書き方、ちょっとDecoratorっぽいですね。implementsではなくコンポジションさせるので、パターンとしては違いますが