Java
デザインパターン
GoF
observer

にゃんこで分かるObserverパターン 前編

概要

Observerパターンってわかりにくいですよね。そこで二匹のねこがごはんをねだるという例を用いて、このパターンを説明したいと思います。
IMG_0751.JPG
(日向ぼっこ中の実家のにゃんこ。もふもふ……)

登場するにゃんこ

飼い猫のタマ
タマは、マンションの3階で暮らしています。飼い主は一人暮らしです。窓は閉まっていることが多く、外に出て遊ぶことはありません。
neko_furikaeri.png

野良猫のトラ
トラは、とある商店街に住んでいます。猫好きの家に行ったり、ゴミを漁ったりして食事を確保しています。
run_cat.png

多分これが一番簡単なObserverパターンだと思います

Cat.java
public class Cat{// Subject クラス
    private Human human;

    public void setHuman(Human human){// setObserver()
        this.human = human;
    }

    public void call(){// notifyObservers()
        System.out.println("にゃー(おなかすいた)");
        this.human.called();
    }
}
Human.java
public class Human{// Observer クラス
    public void called(){// notify() or update()
        System.out.println("カリカリをあげる");
    }
}
Main.java
    public static void main(String[] args) {
        Cat tama = new Cat();
        Human master = new Human();
        tama.setHuman(master);

        tama.call();
    }
にゃー(おなかすいた)
カリカリをあげる

解説

uml1.png

Observerパターンの要点は、2つあります。
1つ目は、Subject クラスがObserver クラスをメンバ変数に持っていることです。つまり、タマ(Catクラス)が、飼い主(Humanクラス)を知っている必要があります。これは、setHuman(= setObserver)関数で表わされます。
2つ目は、Subject クラスが変化したとき、Observerクラスを呼び出すことです。つまり、お腹が空いたら、飼い主を呼び出すということになります。これは、Cat.call関数とHuman.called関数で表わされます。

しかし、もっとも簡単なObserver パターンにおいて、タマは飼い主は一人だけしか覚えていられません。別の人間を覚えようとすると、飼い主のことは忘れてしまいます。飼い主は涙目でしょう。

Main.java
    public static void main(String[] args) {
        Cat tama = new Cat();
        Human master = new Human();
        tama.setHuman(master);

        // 飼い主の友達が遊びにきました
        Human friend = new Human();
        tama.setHuman(friend);

        // 以降はfriendにエサをねだるようになります
        tama.call();
    }

Observer パターンの基本構造

タマにはできなかったことが、トラにはできます。トラはいろいろな家を覚えておいて、お腹が空くたびその家々を回るようです。

Cat.java
public class Cat {// Subject クラス
    private final ArrayList<Human> humans = new ArrayList<>();

    public void addHuman(Human human){// addObserver()
        this.humans.add(human);
    }

    public void deleteHuman(Human human){// deleteObserver()
        this.humans.remove(human);
    }

    public void call(){// notifyObservers()
        System.out.println("にゃー(おなかすいた)");
        for(Human human : this.humans){
            human.called();
        }
    }
}
Human.java
public class Human {// Observer クラス
    public void called(){// notify() or update()
        int dice = new Random().nextInt(99);

        if(dice < 50){
            System.out.println("これ、どうぞ!");

        } else {
            System.out.println("今日は何もないの……。");
        }
    }
}
Main.java
public static void main(String[] args) {
    Cat tora = new Cat();
    Human suzuki = new Human();
    Human saito = new Human();
    Human kobayashi = new Human();

    tora.addHuman(suzuki);
    tora.addHuman(saito);
    tora.addHuman(kobayashi);

    tora.call();
}
にゃー(おなかすいた)
今日は何もないの……。
これ、どうぞ!
今日は何もないの……。

解説

uml2.png

タマとトラのコードの違いで、特に重要なものは3つあります。
1つ目は、Subject クラス のメンバ変数が ArrayList になったことです。これによって、トラは複数の家の軒下でエサをねだることができます。
2つ目は、Subject クラスの関数が、setObserverからaddObserverに変化したことです。単数ではなく複数の要素を設定できるようになったため名称を変更しました。
3つ目は、Observer クラスを登録するだけでなく、削除することもできるようになったことです。deleteObserver関数がその役割を示しています。飼い主ではない人たちはエサを50%の確率でエサをくれますが、残りの50%ではくれません。世知辛いですが、あまりエサをくれない家には行かなくなるのかもしれません(未実装)。

一般化

トラのコードをより汎用的で使いまわせるようにします。すると、よく見るObserver パターンの形になります。

Subject.java
public class Subject {
    private final ArrayList<Observer> observers = new ArrayList<>();

    public void addObserver(Observer observer){
        this.observers.add(observer);
    }

    public void deleteObserver(Observer observer){
        this.observers.remove(observer);
    }

    public void notifyObservers(){
        for(Observer observer : this.observers){
            observer.update();
        }
    }
}
Cat.java
public class Cat extends Subject {
    @Override
    public void notifyObservers(){
        System.out.println("にゃー(おなかすいた)");
        super.notifyObservers();
    }
}
Observer.java
public interface Observer{
    public void update(); // or notify()
}
Human.java
public class Human implements Observer{
    @Override
    public void update(){
        int dice = new Random().nextInt(99);

        if(dice < 50){
            System.out.println("これ、どうぞ!");

        } else {
            System.out.println("今日は何もないの……。");
        }
    }
}
Main.java
public static void main(String[] args) {
    Cat tora = new Cat();
    Human suzuki = new Human();
    Human saito = new Human();
    Human kobayashi = new Human();

    tora.addObserver(suzuki);
    tora.addObserver(saito);
    tora.addObserver(kobayashi);

    tora.notifyObservers();
}
にゃー(おなかすいた)
これ、どうぞ!
これ、どうぞ!
今日は何もないの……。

解説

uml3.png

トラのコードを一般化して変わったところは、3つあります。
1つ目は、Human クラスがObserver インターフェースを実装する形になったことです。Observer インターフェースを挟む理由は、3つ目の変化と共に説明します。
2つ目は、Cat クラスの関数を、Subject クラスとして抽出したことです。それによって、処理内容の変化しやすいCat クラスがよりシンプルになりました。
3つ目は、Subject クラスのArrayListに Human クラスではなく、Observer インターフェースを入れるようになったことです。Observer.update 関数を呼び出すという形にすることで、Human クラスだけでなく、様々なクラスのupdate 関数を呼び出すことができ、より汎用性が上がります。

補足

  1. update 関数は、notify 関数として登場することもありますが、同じ機能を示しています。
  2. javaでは、Subject クラスがObservable クラスとして、Observer インターフェースはそのままの名前で用意してあります。
  3. Subject クラスはイベントハンドラ(Event Handler)、Observer クラスはイベントリスナー(Event Listener)と呼ばれることもあります。

おわりに

この解説は、デザインパターンを勉強し始めたけれど、Observer パターンが難しいと感じている人のために書きました。この解説を読んでから、もう一度クラス図や他の解説をお読みいただければ、このパターンが何をしようとしているかが、より分かりやすくなると思います。
よろしければ後編もお読みください。また、コメントやいいねをいただけましたら幸いです。