オブザーバーパターンのサンプル

オブザーバーパターンは、オブジェクト間の一対多関係を定義するためのソフトウェア設計パターンです。このパターンでは、1つのオブジェクト(Subject)が状態を変更すると、それを購読している複数のオブジェクト(Observer)に通知することができます。これにより、Subjectの状態が変化したときにObserverに通知することができ、Observerはその変化に応じた処理を行うことができます。このパターンは、状態が変化するオブジェクトと、その変化に応じて処理を行うオブジェクトを分離することができるため、複雑なシステムを効果的に開発するために利用されます。

株価の更新通知サンプルコード

コード内の用語の翻訳一覧

  • 株式 (Stock)
  • 投資家 (Investor)
  • 登録 (Attach)
  • 株価 (Price)
  • 表示 (Display)
  • リスト (List)
  • プロパティ (Property)
  • 変更 (Change)
  • 通知 (Notify)
  • 投資家に通知 (Notify Investors)
  • 作成 (Create)
  • 読み込み (Read)
  • Subject クラス (Subject Class)
  • Observer インターフェース (Observer Interface)
  • ConcreteObserver クラス (Concrete Observer Class)
  • 名前 (Name)
  • 通知された (Notified)
  • 情報 (Information)

Stock クラスが Subject オブジェクト、Investor クラスが Observer オブジェクトを表しています。

使い方コード

// 株式の作成
Stock apple = new Stock("アップル", 120);

// 投資家の作成
Investor a = new Investor("山田");
Investor b = new Investor("成田");

// 投資家を株式に登録
apple.Attach(a);
apple.Attach(b);

// 株価変更
apple.Price = 120;
apple.Price = 121;
apple.Price = 125;
apple.Price = 120;

// 結果を表示するための読み込み
Console.ReadKey();

構成クラス

// Subjectクラス
public class Stock
{
    // 投資家のリスト
    private List<IInvestor>investors = new List<IInvestor>();

    // 株式名
    public string Symbol { get; set; }

    // 株価
    private int price;

    // 株価のプロパティ
    public int Price
    {
        get { return price; }
        set
        {
            // 株価が変更された場合
            if (Price != value)
            {
                price = value;
                Notify(); // 投資家に通知
            }
        }
    }

    // コンストラクタ
    public Stock(string symbol, int price)
    {
        Symbol = symbol;
        Price = price;
    }

    // 投資家を登録するメソッド
    public void Attach(IInvestor investor)
    {
        investors.Add(investor);
    }

    // 投資家を削除するメソッド
    public void Detach(IInvestor investor)
    {
        investors.Remove(investor);
    }

    // 投資家に通知するメソッド
    public void Notify()
    {
        foreach (IInvestor investor in investors)
        {
            investor.Update(this);
        }
        Console.WriteLine("");
    }
}

// Observerインターフェース
public interface IInvestor
{
    // 通知するためのメソッド
    void Update(Stock stock);
}

// ConcreteObserverクラス
public class Investor : IInvestor
{
    // 名前のプロパティ
    public string Name { get; set; }

    // コンストラクタ
    public Investor(string name)
    {
        Name = name;
    }

    // 通知された株式の情報を表示するメソッド
    public void Update(Stock stock)
    {
        Console.WriteLine($"{Name}さんに{stock.Symbol}の株価が{stock.Price:C}に変更されたことを通知");
    }
 }

解説

このコードは、Observer パターンを実装しています。このパターンは、Subject オブジェクトが変更されたときに、Observer オブジェクトに通知することができるようにするものです。
Stock クラスが Subject オブジェクト、Investor クラスが Observer オブジェクトを表しています。
Stock クラスは、Attach メソッドと Detach メソッドを通じて、Observer オブジェクトを登録/削除することができます。また、Subject オブジェクトの状態が変更されたときに、Notify メソッドを通じて、Observer オブジェクトに通知することができます。
Investor クラスは、IInvestor インターフェースを実装しています。このインターフェースには、Subject オブジェクトからの通知を受け取るための Update メソッドが定義されています。
このコードは、Observer パターンの一般的な実装例となっています。改良の余地がありますが、大まかな構造は正しいと思います。

実行結果

山田さんにアップルの株価が¥121に変更されたことを通知
成田さんにアップルの株価が¥121に変更されたことを通知

山田さんにアップルの株価が¥125に変更されたことを通知
成田さんにアップルの株価が¥125に変更されたことを通知

山田さんにアップルの株価が¥120に変更されたことを通知
成田さんにアップルの株価が¥120に変更されたことを通知

クラス図

ニュースの更新通知サンプルコード

株価更新通知サンプルをベースにニュースサイトでニュースが更新されたら購読者に通知する仕組みを考えてみましょう

コード内の用語の翻訳一覧

NewsSite: ニュースサイト
Subscriber: 購読者
ISubscriber: Observerインターフェース
Notify: 通知
Attach: 登録
Detach: 削除
Update: 通知する

使い方コード

// ニュースサイトの作成
NewsSite scienceNewsSite = new NewsSite("科学ニュース");

// 購読者の作成
Investor a = new Investor("山田");
Investor b = new Investor("成田");

// 購読者をニュースサイトに登録
scienceNewsSite.Attach(a);
scienceNewsSite.Attach(b);

// ニュース発生
scienceNewsSite.News = "月面旅行達成";
scienceNewsSite.News = "反重力物質の発見";

構成クラス

// ニュースサイトの作成
NewsSite scienceNewsSite = new NewsSite("科学ニュース");

// 購読者の作成
Subscriber a = new Subscriber("山田");
Subscriber b = new Subscriber("成田");

// 購読者をニュースサイトに登録
scienceNewsSite.Attach(a);
scienceNewsSite.Attach(b);

// ニュース発生
scienceNewsSite.News = "月面旅行達成";
scienceNewsSite.News = "反重力物質の発見";

// 結果を表示するための読み込み
Console.ReadKey();

// Subjectクラス
public class NewsSite
{
    // 購読者のリスト
    private List<ISubscriber> subscribers = new List<ISubscriber>();

    // ニュース
    private string news;

    // ニュースのプロパティ
    public string News
    {
        get { return news; }
        set
        {
            // ニュースが変更された場合
            if (News != value)
            {
                news = value;
                Notify(); // 購読者に通知
            }
        }
    }

    // コンストラクタ
    public NewsSite(string news)
    {
        News = news;
    }

    // 購読者を登録するメソッド
    public void Attach(ISubscriber subscriber)
    {
        subscribers.Add(subscriber);
    }

    // 購読者を削除するメソッド
    public void Detach(ISubscriber subscriber)
    {
        subscribers.Remove(subscriber);
    }

    // 購読者に通知するメソッド
    public void Notify()
    {
        foreach (ISubscriber subscriber in subscribers)
        {
            subscriber.Update(this);
        }
        Console.WriteLine("");
    }
}

// Observerインターフェース
public interface ISubscriber
{
    // 通知するためのメソッド
    void Update(NewsSite stock);
}

// ConcreteObserverクラス
public class Subscriber : ISubscriber
{
    // 名前のプロパティ
    public string Name { get; set; }

    // コンストラクタ
    public Subscriber(string name)
    {
        Name = name;
    }

    // 通知された株式の情報を表示するメソッド
    public void Update(NewsSite netNews)
    {
        Console.WriteLine($"{Name}さんに{netNews.News}ニュースを通知");
    }
}

解説

このコードは、Observerパターンを実装したものです。NewsSiteクラスはSubjectクラスで、ISubscriberインターフェースを実装したSubscriberクラスがObserverクラスです。SubscriberクラスはNewsSiteクラスに登録され、NewsSiteのニュースが変更された時に、Subscriberに通知されます。そして、Subscriberクラスは受け取ったニュースを表示します。

実行結果

山田さんに月面旅行達成ニュースを通知
成田さんに月面旅行達成ニュースを通知

山田さんに反重力物質の発見ニュースを通知
成田さんに反重力物質の発見ニュースを通知

クラス図

Eventに変換

オブザーバーパターンを採用しているものにイベントシステムがあります
最後のサンプルをイベントに変換してみましょう

eventキーワードを使う

コード

// ニュースサイトの作成
NewsSite scienceNewsSite = new NewsSite("科学ニュース");

// 購読者の作成
Subscriber a = new Subscriber("山田");
Subscriber b = new Subscriber("成田");

// 購読者をニュースサイトに登録
scienceNewsSite.NewsChanged += a.Update;
scienceNewsSite.NewsChanged += b.Update;

// ニュース発生
scienceNewsSite.News = "月面旅行達成";
scienceNewsSite.News = "反重力物質の発見";

// 結果を表示するための読み込み
Console.ReadKey();
// Subjectクラス
public class NewsSite
{
    // ニュース変更イベント
    public event Action<string> NewsChanged;

    // ニュース
    private string news;

    // ニュースのプロパティ
    public string News
    {
        get { return news; }
        set
        {
            // ニュースが変更された場合
            if (News != value)
            {
                news = value;
                NewsChanged?.Invoke(news); // 購読者に通知
            }
        }
    }

    // コンストラクタ
    public NewsSite(string news)
    {
        News = news;
    }
}

// ConcreteObserverクラス
public class Subscriber
{
    // 名前のプロパティ
    public string Name { get; set; }

    // コンストラクタ
    public Subscriber(string name)
    {
        Name = name;
    }

    // 通知された株式の情報を表示するメソッド
    public void Update(string news)
    {
        Console.WriteLine($"{Name}さんに{news}ニュースを通知");
    }
}
nullチェックの省略について
NewsChanged?.Invoke(news); // 購読者に通知

は次のコードを省略したものになります

if (NewsChanged != null)
{
    NewsChanged(news);
}

クラス図

デリゲートに変換

コード

// ニュースサイトの作成
NewsSite scienceNewsSite = new NewsSite("科学ニュース");

// 購読者の作成
Subscriber a = new Subscriber("山田");
Subscriber b = new Subscriber("成田");

// 購読者をニュースサイトに登録
scienceNewsSite.NewsChanged += a.Update;
scienceNewsSite.NewsChanged += b.Update;

// ニュース発生
scienceNewsSite.News = "月面旅行達成";
scienceNewsSite.News = "反重力物質の発見";

// 結果を表示するための読み込み
Console.ReadKey();
// Subjectクラス
public class NewsSite
{
    // 購読者に通知するためのデリゲート
    public delegate void NewsChangedEventHandler(string news);

    // 購読者に通知するためのイベント
    public event NewsChangedEventHandler NewsChanged;

    // ニュース
    private string news;

    // ニュースのプロパティ
    public string News
    {
        get { return news; }
        set
        {
            // ニュースが変更された場合
            if (News != value)
            {
                news = value;
                OnNewsChanged(news); // 購読者に通知
            }
        }
    }

    // コンストラクタ
    public NewsSite(string news)
    {
        News = news;
    }

    // 購読者に通知するメソッド
    protected void OnNewsChanged(string news)
    {
        NewsChangedEventHandler handler = NewsChanged;
        if (handler != null)
        {
            handler(news);
        }
        Console.WriteLine("");
    }
}

// ConcreteObserverクラス
public class Subscriber
{
    // 名前のプロパティ
    public string Name { get; set; }

    // コンストラクタ
    public Subscriber(string name)
    {
        Name = name;
    }

    // 通知された株式の情報を表示するメソッド
    public void Update(string news)
    {
        Console.WriteLine($"{Name}さんに{news}ニュースを通知");
    }
}

クラス図

デリゲート(イベント)の代入演算子

デリゲートの代入演算子は次のようになります

デリゲートのイベントハンドラを追加する場合: +=
デリゲートのイベントハンドラを削除する場合: -=

サンプル

上記のニュースサイト

NewsSite scienceNewsSite = new NewsSite("科学ニュース");
Subscriber a = new Subscriber("山田");

// 購読者をニュースサイトに登録
scienceNewsSite.NewsChanged += a.Update;

// 購読者をニュースサイトから削除
scienceNewsSite.NewsChanged -= a.Update;

WindowsFormsアプリのボタンクラス

イベントの代入演算子はデリゲートの代入演算子と同様です。

イベントハンドラを追加する場合: +=
イベントハンドラを削除する場合: -=
イベントハンドラは、特定のイベント発生時に呼び出されるメソッドを定義することができます。

用意されたボタンクラス
public class Button
{
    public delegate void ClickEventHandler(object sender, EventArgs e);
    public event ClickEventHandler Click;

    // ボタンがクリックされるとシステムから呼び出されます
    public void OnClick()
    {
        if (Click != null)
        {
            Click(this, EventArgs.Empty);
        }
    }
}

この例では、ButtonクラスはClickイベントを持ち、このイベントが発生した場合には、OnClickメソッドが呼び出されます。

自作のフォームのコード
Button button = new Button();
button.Click += Button_Click;

private void Button_Click(object sender, EventArgs e)
{
    Console.WriteLine("Button clicked");
}

外部のクラスでは、button.ClickイベントハンドラにButton_Clickメソッドを追加することができます。