C#のオブザーバーパターンで通知機能を実装!初心者向けデザインパターン解説
生徒
「C#のプログラムで、何かが起きたときに別の場所に自動で通知する仕組みを作りたいのですが、何か良い方法はありますか?」
先生
「それなら『オブザーバーパターン』という設計パターンがぴったりですよ。イベント通知の仕組みをスッキリ書くことができる定番の方法です。」
生徒
「オブザーバーパターンですか?なんだか難しそうな名前ですね。パソコン初心者でも理解できますか?」
先生
「大丈夫です。YouTubeのチャンネル登録や、スマートフォンの通知機能と同じ仕組みだと考えれば、実はとても身近で簡単なんですよ。基本から一緒に見ていきましょう!」
1. オブザーバーパターンとは何か
C#のプログラミングを学んでいると、デザインパターンという言葉を耳にすることがあります。デザインパターンとは、世界中の先輩プログラマーたちが発見した「よくある問題に対する、きれいで効率的な解決策の型」のことです。その中の一つであるオブザーバーパターン(Observer Pattern)は、日本語に直すと「観察者パターン」という意味になります。
この仕組みは、あるオブジェクト(プログラム内のデータや機能のまとまり)の状態が変化したときに、それに関係する他のオブジェクトへ自動的に「状態が変わりましたよ!」と通知を送るための設計図です。プログラミングの学習を進めると、システム同士をどのように繋ぐかで迷うことが多くなりますが、このパターンを使うことで、お互いのプログラムを邪魔することなく、綺麗に繋ぐことができるようになります。
2. 初心者でもわかるオブザーバーパターンの仕組みと例え話
パソコンの操作に慣れていない方でも、インターネットで動画を見たり、スマートフォンのアプリを使ったりしたことはあるかと思います。オブザーバーパターンの仕組みは、まさにYouTubeのチャンネル登録機能と同じです。
動画を配信する側(ユーチューバー)を「通知を発信する側」とします。そして、チャンネル登録をしている視聴者を「通知を受け取る側」とします。もしチャンネル登録の機能がなかったら、視聴者は新しい動画が投稿されたかどうかを確認するために、毎日何度もそのチャンネルのページを見に行かなければなりません。これは非常に面倒ですし、パソコンやスマートフォンの動きも重くなってしまいます。
しかし、チャンネル登録をしておけば、ユーチューバーが新しい動画を投稿した瞬間に、登録者全員のスマートフォンへ「新しい動画が公開されました」という通知が自動的に届きます。このように、発信側が変化したときに、登録者に対して一斉にお知らせを送る仕組みこそがオブザーバーパターンです。プログラミングの世界では、通知を発信する側を「サブジェクト(Subject)」、通知を受け取って観察する側を「オブザーバー(Observer)」と呼びます。
3. なぜオブザーバーパターンを使うのかというメリット
オブジェクト指向プログラミングという考え方では、プログラムを部品化して作ることが大切です。このとき、部品同士がべったりと依存し合っていると、一つの部品を改造したときに他の部品まで壊れてしまうという問題が起きます。これを専門用語で結合度が高いと言います。オブザーバーパターンを使う最大のメリットは、部品同士の結びつきを緩やかにできること、つまり疎結合(そけつごう)にできる点にあります。
通知を送る側は、相手が誰であるかを詳しく知る必要はありません。「自分の通知を受け取りたい人たちのリスト」だけを持っており、何かが起きたらそのリストにある相手に向かって順番に通知の信号を送るだけです。通知を受け取る側も、自分の好きなタイミングでリストに参加したり、リストから抜けたり(チャンネル登録を解除したり)することができます。これにより、プログラムの拡張や修正が驚くほど簡単になります。例えば、新しい通知先を後から追加したくなっても、通知を送る側のプログラムを書き換える必要は一切ありません。
4. 最もシンプルな基本の通知プログラム
それでは、C#を使った具体的なプログラムのコードを見ていきましょう。まずは、最も基礎的な概念を理解するために、インターフェースという仕組みを使わずに、簡単なクラスだけで構築したプログラムを作成します。クラスとは、プログラムの設計図のようなものです。
このプログラムでは、ニュースを配信する新聞社と、それを購読する読者を表現しています。新聞社が新しいニュースを発行すると、登録されている読者に自動でその内容が届きます。
using System;
using System.Collections.Generic;
class Reader
{
public string Name { get; set; }
public Reader(string name)
{
Name = name;
}
public void Update(string message)
{
Console.WriteLine(Name + "さんに通知:「" + message + "」");
}
}
class NewsPublisher
{
private List<Reader> readers = new List<Reader>();
public void RegisterReader(Reader reader)
{
readers.Add(reader);
}
public void Notify(string news)
{
foreach (var reader in readers)
{
reader.Update(news);
}
}
}
class Program
{
static void Main()
{
NewsPublisher publisher = new NewsPublisher();
Reader reader1 = new Reader("田中");
Reader reader2 = new Reader("鈴木");
publisher.RegisterReader(reader1);
publisher.RegisterReader(reader2);
publisher.Notify("本日の天気は晴れです。");
}
}
上記のコードを実行すると、画面には次のように出力されます。
田中さんに通知:「本日の天気は晴れです。」
鈴木さんに通知:「本日の天気は晴れです。」
このプログラムでは、ニュース配信者が読者のリストを持っており、一斉にメッセージを送信していることがわかります。
5. インターフェースを使った本格的な設計方法
基本の形が理解できたら、次はC#の標準的な設計に近づけていきましょう。先ほどのプログラムでは、新聞社は読者という特定の型しか登録できませんでした。しかし実際の開発では、読者だけでなく、テレビ局やWEBサイトなど、様々な形式の相手に通知を送りたい場合があります。ここで登場するのがインターフェース(Interface)です。
インターフェースとは、クラスが実装すべき「約束事」や「ルール」を定めたものです。これを使うことで、通知を受け取る側がどんな種類のクラスであっても、同じ方法で通知を送ることができるようになります。ここでは、通知を受け取るルールを定めたインターフェースを導入したプログラムを見てみましょう。
using System;
using System.Collections.Generic;
interface IObserver
{
void OnReceived(string message);
}
interface ISubject
{
void AddObserver(IObserver observer);
void RemoveObserver(IObserver observer);
void NotifyObservers(string message);
}
class MainSystem : ISubject
{
private List<IObserver> observers = new List<IObserver>();
public void AddObserver(IObserver observer)
{
observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
observers.Remove(observer);
}
public void NotifyObservers(string message)
{
foreach (var observer in observers)
{
observer.OnReceived(message);
}
}
}
class UserObserver : IObserver
{
private string userName;
public UserObserver(string name)
{
userName = name;
}
public void OnReceived(string message)
{
Console.WriteLine("ユーザー [" + userName + "] が受信: " + message);
}
}
class AdminObserver : IObserver
{
public void OnReceived(string message)
{
Console.WriteLine("管理者システムがログに記録: " + message);
}
}
class Program
{
static void Main()
{
MainSystem system = new MainSystem();
IObserver user = new UserObserver("佐藤");
IObserver admin = new AdminObserver();
system.AddObserver(user);
system.AddObserver(admin);
system.NotifyObservers("サーバーのメンテナンスを開始します。");
}
}
上記のプログラムを実行すると、次のような結果が得られます。
ユーザー [佐藤] が受信: サーバーのメンテナンスを開始します。
管理者システムがログに記録: サーバーのメンテナンスを開始します。
インターフェースを使用することで、全く異なる役割を持つクラスであっても、一斉に同じ通知を受け取ることができるようになりました。
6. C#のイベント機能を使ったスマートな実装
実は、C#というプログラミング言語には、このオブザーバーパターンをはじめから簡単に実現するための仕組みが用意されています。それがデリゲート(Delegate)とイベント(Event)という機能です。デリゲートとは「メソッド(処理)を保存して持ち運べる変数」のようなもので、イベントはそのデリゲートを安全に扱うための仕組みです。
これらを使うと、先ほどのようにリストを手動で作成したり、インターフェースをわざわざ定義したりすることなく、わずか数行のコードでオブザーバーパターンと同じ仕組みを実装することができます。よりスマートで、実戦的なC#のコードを確認してみましょう。
using System;
class SensorStation
{
public event Action<double> TemperatureChanged;
public void CheckTemperature(double currentTemperature)
{
Console.WriteLine("現在の温度を測定しました: " + currentTemperature + "度");
if (TemperatureChanged != null)
{
TemperatureChanged(currentTemperature);
}
}
}
class DisplayUnit
{
public void Show(double temp)
{
Console.WriteLine("画面表示ユニット: 温度が " + temp + " 度に変更されました。");
}
}
class AlarmUnit
{
public void CheckAlert(double temp)
{
if (temp > 30.0)
{
Console.WriteLine("警告アラーム: 気温が30度を超えています!危険です!");
}
}
}
class Program
{
static void Main()
{
SensorStation station = new SensorStation();
DisplayUnit display = new DisplayUnit();
AlarmUnit alarm = new AlarmUnit();
station.TemperatureChanged += display.Show;
station.TemperatureChanged += alarm.CheckAlert;
station.CheckTemperature(25.5);
Console.WriteLine("--------------------------------");
station.CheckTemperature(32.8);
}
}
上記のプログラムを実行した結果は以下の通りです。
現在の温度を測定しました: 25.5度
画面表示ユニット: 温度が 25.5 度に変更されました。
--------------------------------
現在の温度を測定しました: 32.8度
画面表示ユニット: 温度が 32.8 度に変更されました。
警告アラーム: 気温が30度を超えています!危険です!
プラス記号とイコール記号を組み合わせた演算子を使うだけで、簡単に処理を登録できるのがC#のイベント機能の大きな特徴です。
7. 実際のアプリケーション開発における具体的な活用事例
オブザーバーパターンが、実際のシステム開発の現場でどのように使われているのかを具体的に紹介します。このパターンは、画面を持つアプリケーションの開発において、なくてはならない重要な技術となっています。
例えば、ネットショッピングのアプリを想像してみてください。買い物かごに商品を入れたとき、画面の右上にあるカートの数字が自動的に増えます。同時に、合計金額を表示している場所の数字も書き換わります。これは、買い物かごというデータが変化したことをきっかけに、画面の様々な部品に対してオブザーバーパターンによって通知が送られ、それぞれの表示が自発的に更新されているからです。他にも、スマートフォンのバッテリー残量が少なくなったときに、画面を暗くしたり、警告ポップアップを出したりする処理にも、このイベント通知の仕組みが応用されています。システムが大きくなればなるほど、このパターンの重要性は増していきます。
8. 実装する際に気をつけるべき注意点とメモリ管理
非常に便利なオブザーバーパターンですが、プログラミング初心者が見落としがちな大きな落とし穴があります。それがメモリリークという問題です。メモリとは、パソコンがプログラムを実行するときに一時的にデータを記憶しておくための場所のことです。このメモリの容量には限りがあります。
通知を受け取るためにオブジェクトを登録すると、通知を送る側はそのオブジェクトへの接続ルートをずっと保持し続けます。もし、通知を受け取る側のプログラムが不要になって画面から消えたとしても、登録を解除し忘れていると、パソコンは「まだこのオブジェクトは使うかもしれない」と勘違いしてしまい、メモリからデータを綺麗に消去することができなくなってしまいます。これが積み重なると、パソコンの動作がどんどん重くなってしまいます。そのため、不要になったタイミングで必ずマイナス記号とイコール記号の演算子などを使って、登録を解除する処理を書く習慣をつけることが極めて大切です。