0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

全30回:静的と動的でどう違うのか、JavaとPythonで学ぶデザインパターン - Day 21 Observerパターン:状態変化を通知する

Posted at

はじめに

皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第21回目です。
今回は、オブジェクトの状態変化を、それに依存する複数のオブジェクトに自動的に通知するためのObserver(オブザーバー)パターンについて解説します。


Observerパターンとは?

Observerパターンは、**主題(Subject)と呼ばれるオブジェクトが状態を変化させたときに、それに依存している観察者(Observer)**と呼ばれる複数のオブジェクトに自動的に通知する振る舞いパターンです。これにより、主題と観察者間の結合度を低く保ち、システムの柔軟性を高めます。

例えるなら、YouTubeのチャンネル登録システムです。
YouTuber(主題)が新しい動画をアップロードすると、そのチャンネルを登録している視聴者(観察者)全員に自動的に通知が送られます。視聴者は新しい動画がアップロードされたかどうかを常にチェックする必要はなく、YouTuberも個別に各視聴者に連絡する必要はありません。

このパターンの主な目的は以下の通りです:

  • 疎結合: 主題と観察者を直接結びつけず、インターフェースを介して通信することで、互いの依存関係を最小限に抑える
  • 自動通知: 主題の状態変化が、それに依存する全てのオブジェクトに自動的に反映される
  • 動的な追加・削除: 実行時に新しい観察者を簡単に追加したり、既存の観察者を削除したりできる
  • 一対多の関係: 一つの主題が複数の観察者に対して効率的に通知を行える

パターンの構成要素

Observerパターンでは、以下の4つのコンポーネントが登場します:

  1. 主題インターフェース(Subject): attach(), detach(), notify() といったメソッドを定義
  2. 具象主題クラス(ConcreteSubject): 主題インターフェースを実装し、観察者のリストを保持し、状態変化時に通知
  3. 観察者インターフェース(Observer): update() のような通知を受け取るメソッドを定義
  4. 具象観察者クラス(ConcreteObserver): 観察者インターフェースを実装し、通知を受け取って特定の処理を実行

Javaでの実装:厳格なインターフェースとタイプセーフティ

Javaは、かつてjava.util.Observerjava.util.Observableを提供していましたが、現在は非推奨となっています。現代的なJavaでは、カスタムインターフェースを定義して実装することが推奨されています。

以下に、ニュースメディアシステムを例に実装を示します:

// JavaでのObserverパターンの実装例

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

// ニュース記事を表すデータクラス
class NewsArticle {
    private final String title;
    private final String content;
    private final String category;

    public NewsArticle(String title, String content, String category) {
        this.title = title;
        this.content = content;
        this.category = category;
    }

    public String getTitle() { return title; }
    public String getContent() { return content; }
    public String getCategory() { return category; }

    @Override
    public String toString() {
        return String.format("[%s] %s", category, title);
    }
}

// 観察者インターフェース
interface NewsObserver {
    void update(NewsArticle article);
    String getName();
}

// 主題インターフェース
interface NewsPublisher {
    void subscribe(NewsObserver observer);
    void unsubscribe(NewsObserver observer);
    void notifyObservers(NewsArticle article);
}

// 具象主題クラス
class NewsAgency implements NewsPublisher {
    // スレッドセーフなリストを使用
    private final List<NewsObserver> observers = new CopyOnWriteArrayList<>();
    private String agencyName;

    public NewsAgency(String agencyName) {
        this.agencyName = agencyName;
    }

    @Override
    public void subscribe(NewsObserver observer) {
        observers.add(observer);
        System.out.println(observer.getName() + " subscribed to " + agencyName);
    }

    @Override
    public void unsubscribe(NewsObserver observer) {
        observers.remove(observer);
        System.out.println(observer.getName() + " unsubscribed from " + agencyName);
    }

    @Override
    public void notifyObservers(NewsArticle article) {
        System.out.println("\n" + agencyName + " publishing: " + article);
        for (NewsObserver observer : observers) {
            try {
                observer.update(article);
            } catch (Exception e) {
                System.err.println("Error notifying observer " + observer.getName() + ": " + e.getMessage());
            }
        }
    }

    public void publishNews(String title, String content, String category) {
        NewsArticle article = new NewsArticle(title, content, category);
        notifyObservers(article);
    }
}

// 具象観察者クラス1: 一般読者
class GeneralReader implements NewsObserver {
    private final String name;

    public GeneralReader(String name) {
        this.name = name;
    }

    @Override
    public void update(NewsArticle article) {
        System.out.println("📰 " + name + " received news: " + article.getTitle());
    }

    @Override
    public String getName() {
        return name;
    }
}

// 具象観察者クラス2: 特定カテゴリの専門読者
class CategorySpecialist implements NewsObserver {
    private final String name;
    private final String interestedCategory;

    public CategorySpecialist(String name, String interestedCategory) {
        this.name = name;
        this.interestedCategory = interestedCategory;
    }

    @Override
    public void update(NewsArticle article) {
        if (article.getCategory().equals(interestedCategory)) {
            System.out.println("🔍 " + name + " (specialist) received " + interestedCategory + 
                             " news: " + article.getTitle());
            // 専門的な処理をここに追加可能
        }
    }

    @Override
    public String getName() {
        return name + " (" + interestedCategory + " specialist)";
    }
}

// 使用例
public class Main {
    public static void main(String[] args) {
        NewsAgency cnnNews = new NewsAgency("CNN");

        // 様々な観察者を作成
        GeneralReader alice = new GeneralReader("Alice");
        GeneralReader bob = new GeneralReader("Bob");
        CategorySpecialist techExpert = new CategorySpecialist("TechGuru", "Technology");
        CategorySpecialist sportsExpert = new CategorySpecialist("SportsFan", "Sports");

        // 購読登録
        cnnNews.subscribe(alice);
        cnnNews.subscribe(bob);
        cnnNews.subscribe(techExpert);
        cnnNews.subscribe(sportsExpert);

        // ニュース配信
        cnnNews.publishNews("New iPhone Released", "Apple announces...", "Technology");
        cnnNews.publishNews("Olympic Games Update", "Latest results from...", "Sports");
        cnnNews.publishNews("Market Analysis", "Stock market shows...", "Finance");

        // 一部の読者が購読解除
        System.out.println("\n--- Bob unsubscribes ---");
        cnnNews.unsubscribe(bob);
        
        cnnNews.publishNews("Breaking Tech News", "Major breakthrough in AI...", "Technology");
    }
}

Pythonでの実装:柔軟性と複数のアプローチ

Pythonでは、その柔軟性を活かして複数のアプローチでObserverパターンを実装できます。クラスベース、関数ベース、さらにはプロパティを活用した実装など、様々な方法があります。

アプローチ1: クラスベース実装

# PythonでのObserverパターン - クラスベース実装

from abc import ABC, abstractmethod
from typing import List, Protocol
from dataclasses import dataclass

@dataclass
class NewsArticle:
    title: str
    content: str
    category: str
    
    def __str__(self):
        return f"[{self.category}] {self.title}"

# プロトコルを使った観察者インターフェース(Python 3.8+)
class NewsObserver(Protocol):
    def update(self, article: NewsArticle) -> None: ...
    @property
    def name(self) -> str: ...

# または、ABC を使った従来の方法
class NewsObserverABC(ABC):
    @abstractmethod
    def update(self, article: NewsArticle) -> None:
        pass
    
    @property
    @abstractmethod
    def name(self) -> str:
        pass

class NewsAgency:
    def __init__(self, agency_name: str):
        self._observers: List[NewsObserver] = []
        self.agency_name = agency_name

    def subscribe(self, observer: NewsObserver) -> None:
        self._observers.append(observer)
        print(f"{observer.name} subscribed to {self.agency_name}")

    def unsubscribe(self, observer: NewsObserver) -> None:
        if observer in self._observers:
            self._observers.remove(observer)
            print(f"{observer.name} unsubscribed from {self.agency_name}")

    def notify_observers(self, article: NewsArticle) -> None:
        print(f"\n{self.agency_name} publishing: {article}")
        for observer in self._observers:
            try:
                observer.update(article)
            except Exception as e:
                print(f"Error notifying observer {observer.name}: {e}")

    def publish_news(self, title: str, content: str, category: str) -> None:
        article = NewsArticle(title, content, category)
        self.notify_observers(article)

class GeneralReader:
    def __init__(self, name: str):
        self._name = name

    def update(self, article: NewsArticle) -> None:
        print(f"📰 {self._name} received news: {article.title}")

    @property
    def name(self) -> str:
        return self._name

class CategorySpecialist:
    def __init__(self, name: str, interested_category: str):
        self._name = name
        self.interested_category = interested_category

    def update(self, article: NewsArticle) -> None:
        if article.category == self.interested_category:
            print(f"🔍 {self._name} (specialist) received {self.interested_category} "
                  f"news: {article.title}")

    @property
    def name(self) -> str:
        return f"{self._name} ({self.interested_category} specialist)"

アプローチ2: 関数ベース実装(コールバック)

# 関数ベースのアプローチ
from typing import Callable, Dict, Any

CallbackFunction = Callable[[NewsArticle], None]

class SimpleNewsAgency:
    def __init__(self, name: str):
        self.name = name
        self._callbacks: List[CallbackFunction] = []
        self._callback_names: Dict[CallbackFunction, str] = {}

    def subscribe(self, callback: CallbackFunction, name: str = "Anonymous") -> None:
        self._callbacks.append(callback)
        self._callback_names[callback] = name
        print(f"{name} subscribed to {self.name}")

    def unsubscribe(self, callback: CallbackFunction) -> None:
        if callback in self._callbacks:
            name = self._callback_names.get(callback, "Unknown")
            self._callbacks.remove(callback)
            del self._callback_names[callback]
            print(f"{name} unsubscribed from {self.name}")

    def publish_news(self, title: str, content: str, category: str) -> None:
        article = NewsArticle(title, content, category)
        print(f"\n{self.name} publishing: {article}")
        
        for callback in self._callbacks:
            try:
                callback(article)
            except Exception as e:
                name = self._callback_names.get(callback, "Unknown")
                print(f"Error in callback {name}: {e}")

# 使用例
def tech_enthusiast_callback(article: NewsArticle) -> None:
    if article.category == "Technology":
        print(f"🤖 Tech enthusiast excited about: {article.title}")

def general_reader_callback(article: NewsArticle) -> None:
    print(f"📖 General reader informed: {article.title}")

アプローチ3: プロパティとデコレータを活用

# プロパティを使った自動通知システム
class SmartNewsAgency:
    def __init__(self, name: str):
        self.name = name
        self._observers: List[NewsObserver] = []
        self._latest_article: NewsArticle = None

    def subscribe(self, observer: NewsObserver) -> None:
        self._observers.append(observer)
        print(f"{observer.name} subscribed to {self.name}")

    @property
    def latest_article(self) -> NewsArticle:
        return self._latest_article

    @latest_article.setter
    def latest_article(self, article: NewsArticle) -> None:
        self._latest_article = article
        self._notify_all(article)

    def _notify_all(self, article: NewsArticle) -> None:
        print(f"\n{self.name} auto-publishing: {article}")
        for observer in self._observers:
            observer.update(article)

# 使用例
def main():
    # クラスベース使用例
    print("=== Class-based Implementation ===")
    cnn = NewsAgency("CNN")
    
    alice = GeneralReader("Alice")
    tech_expert = CategorySpecialist("TechGuru", "Technology")
    
    cnn.subscribe(alice)
    cnn.subscribe(tech_expert)
    
    cnn.publish_news("AI Revolution", "New breakthrough in machine learning", "Technology")
    cnn.publish_news("Climate Update", "Latest climate change report", "Environment")

    # 関数ベース使用例
    print("\n=== Function-based Implementation ===")
    bbc = SimpleNewsAgency("BBC")
    
    bbc.subscribe(tech_enthusiast_callback, "Tech Enthusiast")
    bbc.subscribe(general_reader_callback, "General Reader")
    
    bbc.publish_news("Quantum Computing Advance", "New quantum processor unveiled", "Technology")

    # プロパティベース使用例
    print("\n=== Property-based Implementation ===")
    reuters = SmartNewsAgency("Reuters")
    reuters.subscribe(GeneralReader("Smart Reader"))
    
    # プロパティに代入すると自動的に通知される
    reuters.latest_article = NewsArticle("Market Update", "Stock prices surge", "Finance")

if __name__ == "__main__":
    main()

実際の使用場面と応用例

Observerパターンは、以下のような場面で広く使われます:

  1. GUI アプリケーション: ボタンクリック、フォーム入力などのイベント処理
  2. MVC アーキテクチャ: Modelの変更をViewに自動反映
  3. リアルタイム通知システム: チャット、株価更新、ニュース配信
  4. ゲーム開発: プレイヤーアクションに対する複数のゲームオブジェクトの反応
  5. IoT システム: センサーデータの変更を複数のデバイスに通知

パターンの利点と注意点

利点

  • 疎結合: 主題と観察者が独立して変更可能
  • 動的な関係: 実行時に観察者の追加・削除が可能
  • 放送通信: 一つの変更を複数の対象に効率的に通知
  • オープン・クローズの原則: 新しい観察者タイプを簡単に追加可能

注意点

  • メモリリーク: 観察者の参照が残り続ける可能性
  • 通知順序: 観察者への通知順序が保証されない場合がある
  • 複雑性: 過度に使用するとデバッグが困難になる
  • パフォーマンス: 大量の観察者がある場合、通知処理が重くなる可能性

実装の違いと特徴

特性 Java Python
型安全性 コンパイル時の型チェックで安全性確保 実行時の型チェック、型ヒントで補完
インターフェース 明示的なインターフェース定義が必須 Protocol、ABC、またはダックタイピング
実装の柔軟性 厳格なクラス構造 クラス、関数、プロパティなど多様なアプローチ
エラーハンドリング 例外処理が明示的 より柔軟なエラーハンドリング
並行処理 スレッドセーフなコレクション使用 GILにより一部の並行処理が制限

まとめ:本質は「変更の自動伝播」

Observerパターンは、両言語で実装アプローチは大きく異なりますが、 「主題の状態変化を複数の依存オブジェクトに自動的に伝播する」 という本質は共通です。

Javaは型安全性と明確なインターフェース定義により堅牢なシステムを構築し、Pythonは言語の柔軟性を活かして様々なスタイルでの実装を可能にします。どちらのアプローチも、疎結合なシステム設計において非常に有用です。

このパターンは、現代のアプリケーション開発において基礎的でありながら非常に重要な役割を果たしており、特にイベント駆動アーキテクチャやリアクティブプログラミングの基盤となっています。

明日は、オブジェクトの状態によって振る舞いを変化させるStateパターンについて解説します。お楽しみに!

次回のテーマは、「Day 22 Stateパターン:オブジェクトの状態によって振る舞いを変える」です。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?