LoginSignup
68
68

More than 3 years have passed since last update.

C#でObserverパターンをきちんと理解して実装する

Last updated at Posted at 2020-12-17

Observerパターンとは

Observerパターンとは、「クラスから通知を発行する仕組み」と、「他のクラスから発行された通知を受け取る仕組み」を実現するためのデザインパターンです。
このデザインパターンは、主にクラス間のデータのやり取りをするときに使われますが、以下のような状況において特に有用です。

  1. 双方向ではなく、一方からもう一方へと一方通行でデータを発行する場合
  2. 1回ではなく、複数回データを発行する場合
  3. 任意のタイミングでデータを発行したい場合
  4. 発行されたデータを、複数のクラスが同時に受け取りたい場合

特に1番はObserverパターンの必須要件で、双方向のデータのやり取りには使用できません。

ObserverとObservable

このデザインパターンでは、「データを受け取るクラス」と「データを発行するクラス」を明確に分けて考えます。
このうち、「データを受け取るクラス」のことを「Observer」、「データを発行するクラス」のことを「Observable」と呼びます。

Observerとは、「監視者」や「観察者」といった意味の英単語で、データを発行するクラス(Observable)からの通知を受け取る(つまり、通知を観察する、監視する)ことからその名が付けられています。
対してObservableとは、Observerの-able系なので、「観察可能」といった意味になります。
Observableは「観察者」であるObserverにデータを発行するので、Observerから「観察されることができる」、「観察可能」といったことから、この名がつけられています。

ObserverとObservableは非常に重要なので、以下のようにまとめます。

Observer Observable
単語の意味 観察者 観察可能
観察されることができる
クラスの意味 通知を受け取るクラス 通知を発行するクラス

基本的な仕組み

Observerパターンの基本的な仕組みは、次の3ステップとなります。

  1. ObserverがObservableにデータの発行先として登録する(=購読する)
  2. Observableは登録されたすべての発行先に値を発行する
  3. Observerは受け取った値を使用して任意の処理を実行する

次の項から詳しく説明します。

1. ObserverがObservableを購読する

Observer(値を受信したいクラス)は、欲しい値を発行しているクラス(Observable)に対して、「僕に値を発行・通知してください」と自分自身を通知先として登録します
この「配信登録」することを、「購読(Subscribe)する」と言います。

現実世界で「AさんはS社の雑誌を定期購読する」といった表現はよく使われます。
この場合も、AさんはS社に対して「僕に雑誌を定期的に送ってください」と自分自身を送付先として登録しますね。それと同じような意味になります。

Observerパターン.png

そして、一つの値の発行元(Observable)に対して、複数のObserverが値の発行先として登録することができることも、Observerパターンの特徴の一つです。

Observerパターン_複数登録.png

2.Observableは登録されたすべての発行先に値を発行する

Observable(値を発行するクラス)は、発行する値ができると、値の発行先として登録されたすべてのObserverに対して一斉に値を発行します
こうして、ObserverはObservableからの通知(値)を受け取ることができます。
Observerパターン_値の発行.png

3.Observerは受け取った値を使用して任意の処理を実行する

Observableから発行された値を受け取ったObserverは、受け取った値を使用して任意の処理を実行することができます。

このようにして、ObservableからObserverへとデータの受け渡しが実現されます。

Observerパターンを実装してみる

Observerパターンの基本的な仕組みの大枠は理解頂けたかと思います。
それでは、これらの仕組みを実際にどのように実装するかを説明していきます。

IObserver<T>インターフェイスとIObservable<T>インターフェイス

Observerパターンは、「データを受け取るクラス」と「データを発行するクラス」を明確に分けて考えるデザインパターンと書きました。
実は、.NET Framework4.0以降には、Observerパターンで使えるIObserver<T>インターフェイスとIObservable<T>インターフェイスがSystem名前空間に標準で用意されています
Observerパターンを実装したい場合、これらのインターフェイスをデータの受信側、データの発行側のクラスにそれぞれ実装すれば良いことになります。

これらのインターフェイスの定義は次のようになっています。

public interface IObserver<in T>
{
    //データの発行が完了したことを通知する
    void OnCompleted();
    //データの発行元でエラーが発生したことを通知する
    void OnError(Exception error);
    //データを通知する
    void OnNext(T value);
}

public interface IObservable<out T>
{
    //データの発行を購読する
    IDisposable Subscribe(IObserver<T> observer);
}

IObserver<T>インターフェイスのOnNextメソッドは、データを新しく通知する際に呼び出すメソッドです。
OnCompletedメソッドは、データの発行がすべて完了し、これ以上通知するものがない場合に呼び出します。
万が一、データの発行元で何らかの例外が発生してしまった場合は、OnErrorメソッドを呼び出して、エラーが発生したことを通知します。

IObservable<T>インターフェイスのSubscibeメソッドは、データの発行元からの通知を受け取りたいときに呼び出します。
引数には、通知の発行先となるIObserver<T>オブジェクトを指定します。

ここで、以下のような違和感を覚える方がいらっしゃるかもしれません。

IObserver<T>はデータを受け取るクラスが実装するのに、なぜデータを発行する系のメソッドを実装するのか?
逆にIObservable<T>はデータを発行するクラスが実装するのに、なぜデータの発行を購読するメソッドを実装するのか?

その答えは、これらのメソッドは、実装するクラス自身が使用するのではなく、お互いに相手が使用するものだからです。

例えば、「データを受け取るクラス(Observer)」が実装したIObserver<T>インターフェイスのOnNextメソッドは、データを発行するクラス(Observable)が呼び出すことで、Observerに対して値を通知することができます。
逆に、「データを発行するクラス(Observable)」が実装したIObservable<T>インターフェイスのSubscribeメソッドは、データを受け取るクラス(Observer)が呼び出すことで、自分自身をデータの発行先として登録することができます。

まだ違和感が拭えないかもしれませんが、実際に実装してみると理解が深まるかもしれません。
それでは、実際にIObserver<T>インターフェイスとIObservable<T>インターフェイスを用いて、Observerパターンを実装してみます。

Observerを実装する

まずはObserver(通知を受け取るクラス)を作成します。

1.クラスを作成する

public class Observer
{

}

2.IObserver<T>インターフェイスを実装する

このクラスはObserverなので、IObserver<T>インターフェイスを実装します。
型引数Tには受信したい値の型を指定します。ここでは、int型とします。

public class Observer : IObserver<int>
{
    public void OnCompleted()
    {
        throw new NotImplementedException();
    }

    public void OnError(Exception error)
    {
        throw new NotImplementedException();
    }

    public void OnNext(int value)
    {
        throw new NotImplementedException();
    }
}

3.値を受け取ったときのコールバック処理を記述する

あとは、各メソッドにそれぞれ通知が来たときに実行したい処理を自由に記述します。
例なので、ここでは次のようにコンソールにメッセージを出力するだけの処理を実装しました。

public class Observer : IObserver<int>
{
    public void OnCompleted()
    {
        Console.WriteLine($"通知の受け取りが完了しました");
    }

    public void OnError(Exception error)
    {
        Console.WriteLine($"次のエラーを受信しました:{error.Message}");
    }

    public void OnNext(int value)
    {
        Console.WriteLine($"{value}を受け取りました");
    }
}

これだけでObserverの実装は完了です。

複数のObserverを用意できるようにする

冒頭にも書きましたが、Observerパターンでは、発行された値を複数のクラス(Observer)が同時に受け取ることができます。
これを試すために、次のようにObserverの名前をコンストラクタで指定するようにして、どのObserverがメッセージを受け取ったかを識別できるようにしておきます。

public class Observer : IObserver<int>
{
    private string m_name;
    public Observer(string name)
    {
        m_name = name;
    }

    public void OnCompleted()
    {
        Console.WriteLine($"{m_name}が通知の受け取りを完了しました");
    }

    public void OnError(Exception error)
    {
        Console.WriteLine($"{m_name}が次のエラーを受信しました:{error.Message}");
    }

    public void OnNext(int value)
    {
        Console.WriteLine($"{m_name}{value}を受け取りました");
    }
}

Observableを実装する

次にObservable(通知を発行するクラス)を作成します。

1.クラスを作成する

public class Observable
{

}

2.IObservable<T>インターフェイスを実装する

このクラスはObservableなので、IObservable<T>インターフェイスを実装します。
型引数Tには発行したい値の型を指定します。ObserverとObservableの型引数は合致している必要があるため、ここでもTintとします。

public class Observable : IObservable<int>
{
    public IDisposable Subscribe(IObserver<int> observer)
    {
        throw new NotImplementedException();
    }
}

3.値の発行先を覚えておく仕組みを作成する

Observableの役目は、値の発行先として登録された(購読された、Subscribeされた)IObserver<T>を記憶しておき、発行する値が生じたときに、そのすべてのIObserver<T>に対して値を発行することです。
※「値を発行する」とは、具体的にはIObserver<T>OnNextOnCompleted、もしくはOnErrorメソッドを呼び出すことです

したがって、ObservableなクラスにSubscribeで指定されたIObserver<int>を覚えておける仕組みを作成する必要があります。
とはいっても、単にList<IObserver<T>>に溜めておくだけで十分です。

public class Observable : IObservable<int>
{
    //購読されたIObserver<int>のリスト
    private List<IObserver<int>> m_observers = new List<IObserver<int>>();

    public IDisposable Subscribe(IObserver<int> observer)
    {
        if(!m_observers.Contains(observer))
            m_observers.Add(observer);
    }
}

これで、Subscribeで指定されたIObserver<int>を覚えておくことができるようになりました。

4.購読解除用のIDisposableなクラスを用意する

ところで、Subscribeメソッドの戻り値はIDisposableになっています。
今まで説明しませんでしたが、Observerパターンは、値の発行先として登録するSubscribeの対となる機能として、値の発行を停止してもらう「購読解除」も可能となっています

Subscribeメソッドの戻り値として返すIDisposableは、SubscribeしたObserverが、「もう値はいらないです」と購読を解除するときに使用するものです
ご存知の通り、IDisposableインターフェイスの中身はDisposeメソッドただ一つのみであり、購読を解除したいObserverはこのIDisposableDisposeすることによって購読を解除することができます。

したがって、Disposeされたときに購読を解除する仕組みを作成する必要があります。
「購読を解除する」とは、Observableなクラス視点で言えば、値の発行先リストであるList<IObserver<T>>から購読を解除したいIObserver<T>Removeすることに他なりません。

では、どのように実装すればよいでしょうか?

まず、IDisposableインターフェイスを返さなければなりませんから、当然、IDisposableインターフェイスを実装したクラスが必要になります。
そこで、以下のように「購読を解除する責務を持ったIDisposableなクラス」を作成します。

class Unsubscriber : IDisposable
{
    public void Dispose()
    {
        throw new NotImplementedException();
    }
}

そして、このDisposeメソッドの中に、購読者リストからRemoveする処理を書けば完成です。
具体的には、発行先リストList<IObserver<T>>DisposeされたときにRemoveするターゲットとなるIObserver<T>を、コンストラクタで引き渡して、以下のように実装します。

class Unsubscriber : IDisposable
{
    //発行先リスト
    private List<IObserver<int>> m_observers;
    //DisposeされたときにRemoveするIObserver<int>
    private IObserver<int> m_observer;

    public Unsubscriber(List<IObserver<int>> observers, IObserver<int> observer)
    {
        m_observers = observers;
        m_observer = observer;
    }

    public void Dispose()
    {
        //Disposeされたら発行先リストから対象の発行先を削除する
        m_observers.Remove(m_observer);
    }
}

そして、この新しく作った購読解除用のIDisposableなクラスのインスタンスを、Subscribeの戻り値として返します。

public class Observable : IObservable<int>
{
    //購読されたIObserver<int>のリスト
    private List<IObserver<int>> m_observers = new List<IObserver<int>>();

    public IDisposable Subscribe(IObserver<int> observer)
    {
        if(!m_observers.Contains(observer))
            m_observers.Add(observer);
        //購読解除用のクラスをIDisposableとして返す
        return new Unsubscriber(m_observers, observer);
    }
}

こうすることで、このIDisposableを受け取ったObserverは、値が要らなくなった時点でDisposeメソッドを呼び出すことで、自分自身を発行先リストから削除することができるようになります。

ところで、このUnsubscriberクラスは、Observableクラス以外から生成されることはありません。
したがって、下記のようにObservableクラスの内部クラスにしてしまいます。


public class Observable : IObservable<int>
{
    //購読されたIObserver<int>のリスト
    private List<IObserver<int>> m_observers = new List<IObserver<int>>();

    public IDisposable Subscribe(IObserver<int> observer)
    {
        if(!m_observers.Contains(observer))
            m_observers.Add(observer);
        //購読解除用のクラスをIDisposableとして返す
        return new Unsubscriber(m_observers, observer);
    }

    //購読解除用内部クラス
    private class Unsubscriber : IDisposable
    {
        //発行先リスト
        private List<IObserver<int>> m_observers;
        //DisposeされたときにRemoveするIObserver<int>
        private IObserver<int> m_observer;

        public Unsubscriber(List<IObserver<int>> observers, IObserver<int> observer)
        {
            m_observers = observers;
            m_observer = observer;
        }

        public void Dispose()
        {
            //Disposeされたら発行先リストから対象の発行先を削除する
            m_observers.Remove(m_observer);
        }
    }
}

5.通知を発行する処理を記述する

最後に、発行したい情報や値があった場合に通知を発行する処理を記述します。
ここでは例として、SendNoticeメソッドが呼ばれたときに“int”型の1,2,3を連続で通知を発行するようにします。

値を発行するには、OnNextメソッドを呼び出します。
すべての発行先に対してOnNextメソッドを呼び出すので、以下のようにforeachを使用すればOKです。

public class Observable : IObservable<int>
{
    //購読されたIObserver<int>のリスト
    private List<IObserver<int>> m_observers = new List<IObserver<int>>();

    public IDisposable Subscribe(IObserver<int> observer)
    {
        if(!m_observers.Contains(observer))
            m_observers.Add(observer);
        //購読解除用のクラスをIDisposableとして返す
        return new Unsubscriber(m_observers, observer);
    }

    public void SendNotice()
    {
        //すべての発行先に対して1,2,3を発行する
        foreach (var observer in m_observers)
        {
            observer.OnNext(1);
            observer.OnNext(2);
            observer.OnNext(3);
        }
    }

    //購読解除用内部クラス
    private class Unsubscriber : IDisposable
    {
        //発行先リスト
        private List<IObserver<int>> m_observers;
        //DisposeされたときにRemoveするIObserver<int>
        private IObserver<int> m_observer;

        public Unsubscriber(List<IObserver<int>> observers, IObserver<int> observer)
        {
            m_observers = observers;
            m_observer = observer;
        }

        public void Dispose()
        {
            //Disposeされたら発行先リストから対象の発行先を削除する
            m_observers.Remove(m_observer);
        }
    }
}

使ってみる

これまで作成したObserverとObservableを使用して、実際に値の購読と発行、受け取りの流れを実演します。

ここでは、以下のようなコンソールアプリケーションを作成しました。

class Program
{
    static void Main(string[] args)
    {
        //値を受け取るクラスを3つ作成
        Observer observerA = new Observer("Aさん");
        Observer observerB = new Observer("Bさん");
        Observer observerC = new Observer("Cさん");

        //値を発行するクラスを作成
        Observable observable = new Observable();

        //3つのObserverが、自分自身を発行先として登録する(=購読)
        IDisposable disposableA = observable.Subscribe(observerA);
        IDisposable disposableB = observable.Subscribe(observerB);
        IDisposable disposableC = observable.Subscribe(observerC);
        Console.WriteLine("Aさん〜Cさんが値を購読しました");

        Console.WriteLine("値を発行させます");
        //Observableに値を発行させる
        observable.SendNotice();

        Console.WriteLine("Aさんが購読解除します");
        //Aさんが購読解除する
        disposableA.Dispose();

        Console.WriteLine("値を発行させます");
        //再び値を発行させる
        observable.SendNotice();

        Console.WriteLine("Bさんが購読解除します");
        //Bさんが購読解除する
        disposableB.Dispose();

        Console.WriteLine("値を発行させます");
        //再び値を発行させる
        observable.SendNotice();

        Console.ReadKey();
    }
}

やっている内容としては、コードを見たとおりですが

  1. 値の購読者(Observer)をAさん、Bさん、Cさんの3つを作成
  2. 値の発行者(Observable)を作成
  3. Aさん〜CさんがObservableを購読
  4. Observableに値を発行させる
  5. Aさん、購読を解除する
  6. Observableに再び値を発行させる
  7. Bさんも購読を解除する
  8. Observableに再び値を発行させる

となります。

これを実行すると、次のような出力が得られます。

Aさん〜Cさんが値を購読しました
値を発行させます
Aさんが1を受け取りました
Aさんが2を受け取りました
Aさんが3を受け取りました
Bさんが1を受け取りました
Bさんが2を受け取りました
Bさんが3を受け取りました
Cさんが1を受け取りました
Cさんが2を受け取りました
Cさんが3を受け取りました
Aさんが購読解除します
値を発行させます
Bさんが1を受け取りました
Bさんが2を受け取りました
Bさんが3を受け取りました
Cさんが1を受け取りました
Cさんが2を受け取りました
Cさんが3を受け取りました
Bさんが購読解除します
値を発行させます
Cさんが1を受け取りました
Cさんが2を受け取りました
Cさんが3を受け取りました

Observerパターンが期待通りに動作していることが確認できます。

さいごに

以上、少し長くなってしまいましたがObserverパターンの解説を行いました。

僕自身としては、Observerパターンをこのようにそのまま利用することは少なく、ReactiveExtensionsというObserverパターンをベースとした非常に強力なライブラリを通じて利用することがほとんどです。
もともとは、このReactiveExtensionsの記事を書いていたのですが、ベースとなるObserverパターンの説明がとても長くなってしまったため、別記事として抜き出しました。
またReactiveExtensionsの記事も書いたらリンクを貼るのでそちらも良ければ御覧ください。

[2020/12/22追記]
ReactiveExtensionに関する記事書きました。よろしければ参照ください。
【C#】なぜReactiveExtensionsを導入すると幸せになれるのか

何か間違いやご指摘、ご質問あればコメントにお願いします。

68
68
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
68