Javaを鍛える|デザインパターン全23回 #19

Observerパターン|通知先が増えてもチケット本体を変更しない

  • Java
  • デザインパターン
  • Observer
  • Javaを鍛える

📋 この記事の目次
  1. このパターンは何のためにあるか
  2. Before
  3. After
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftでは、ステータス変更時に担当者宛にメールを自動送信する仕組みが組まれていました。しかし運用を始めて数ヶ月ほど経つと、そのチケットの担当者と並行してウォッチ(購読)登録を行う関係者が複数人になるケースが増え、それら全員へ通知が届くようにしたいという要望が上がってきました。

このパターンは何のためにあるか

Observerパターンは、ある対象の状態が変化したときに、それを監視している複数の観察者すべてに自動で通知を飛ばすための設計手法です。日常のたとえ話でいえば、ニュースレターの購読者が登録・解除しても編集チームの記事作成手順が変わらないように、チケット本体のコードはステータス更新ロジックだけを担当し、通知先の増減には影響されない形に分離します。

Before

チケット本体が状態変化と通知を密結合している典型的な実装例です。

public class Ticket {
    private String id;
    private String title;
    private String status;

    public Ticket(String id, String title) {
        this.id = id;
        this.title = title;
        this.status = "OPEN";
    }

    public void setStatus(String newStatus) {
        this.status = newStatus;
        sendEmailNotification();
    }

    private void sendEmailNotification() {
        System.out.println("メール通知: 担当者宛にチケット '" + title + "' が " + status + " に変更されました");
    }

    public String getId() { return id; }
    public String getStatus() { return status; }
}

public class BeforeMain {
    public static void main(String[] args) {
        Ticket ticket = new Ticket("TK-100", "ログイン画面のデザイン修正");
        ticket.setStatus("IN_PROGRESS");
    }
}

実行すると以下のような出力が得られます。

メール通知: 担当者宛にチケット 'ログイン画面のデザイン修正' が IN_PROGRESS に変更されました

このコードの問題点は3つあります。1つ目は、ステータス更新と通知送信の責任がTicketクラスに両方入っており、単一責任の原則を破っていることです。2つ目は、ウォッチャー(購読者)を増やそうとすると、Ticketクラス自身に手を入れる必要があることです。3つ目は、メール以外の通知チャネル(Slack等)が増えたときにTicketクラスを直接変更する必要があり、既存のテストやコード全体に影響が及ぶリスクが高いことです。

After

Observerパターンを使って、チケットの状態変化と通知の仕組みを分離した実装に移行します。

import java.util.List;
import java.util.ArrayList;

interface TicketObserver {
    void update(String ticketId, String title, String newStatus);
}

class EmailNotification implements TicketObserver {
    private String recipient;

    public EmailNotification(String recipient) {
        this.recipient = recipient;
    }

    @Override
    public void update(String ticketId, String title, String newStatus) {
        System.out.println("メール通知[" + recipient + "]: チケット '" + title + "' が " + newStatus + " に変更されました");
    }
}

class SlackNotification implements TicketObserver {
    private String channel;

    public SlackNotification(String channel) {
        this.channel = channel;
    }

    @Override
    public void update(String ticketId, String title, String newStatus) {
        System.out.println("Slack通知[" + channel + "]: チケット '" + title + "' が " + newStatus + " に変更されました");
    }
}

public class Ticket {
    private String id;
    private String title;
    private String status;
    private List<TicketObserver> observers = new ArrayList<>();

    public Ticket(String id, String title) {
        this.id = id;
        this.title = title;
        this.status = "OPEN";
    }

    public void addObserver(TicketObserver observer) {
        if (!observers.contains(observer)) {
            observers.add(observer);
        }
    }

    public void removeObserver(TicketObserver observer) {
        observers.remove(observer);
    }

    public void setStatus(String newStatus) {
        this.status = newStatus;
        notifyObservers();
    }

    private void notifyObservers() {
        for (TicketObserver obs : observers) {
            obs.update(id, title, status);
        }
    }

    public String getId() { return id; }
    public String getStatus() { return status; }
}

public class AfterMain {
    public static void main(String[] args) {
        Ticket ticket = new Ticket("TK-100", "ログイン画面のデザイン修正");

        ticket.addObserver(new EmailNotification("田中"));
        ticket.addObserver(new SlackNotification("#dev-team"));
        ticket.addObserver(new EmailNotification("佐藤"));
        ticket.addObserver(new EmailNotification("鈴木"));

        ticket.setStatus("IN_PROGRESS");
    }
}

実行結果は以下のようになります。

メール通知[田中]: チケット 'ログイン画面のデザイン修正' が IN_PROGRESS に変更されました
Slack通知[#dev-team]: チケット 'ログイン画面のデザイン修正' が IN_PROGRESS に変更されました
メール通知[佐藤]: チケット 'ログイン画面のデザイン修正' が IN_PROGRESS に変更されました
メール通知[鈴木]: チケット 'ログイン画面のデザイン修正' が IN_PROGRESS に変更されました

Ticketクラスは状態管理と通知呼び出しだけを担当し、通知先やフォーマットの詳細はTicketObserverを実装したクラスに委譲しています。新しい通知チャネル(Teams等)が追加されても、既存のTicketコードを一切手直しせず、TicketObserverの新しい実装をaddObserverで登録するだけで済みます。ウォッチャーの解除もremoveObserverを呼べばよく、Ticketの内部状態に依存した変更は発生しません。

🧓 ベテランの現場コラム 最初は「担当者宛メール」だけで十分だったものが、後になって「PMとQAにもSlack通知したい」という要件が並ぶのはよくある話です。Observerパターンは、この手の「後から増える通知先」を許容する設計の核になります。

こんな場面で使う/使わない

  • 使う: チケットステータスの更新のように、イベント発生時に複数の独立したコンポーネントに情報を伝播させたい場合
  • 使わない: 1対1の呼び出しで完結し、通知先が固定で増減しないような単純な処理の場合

まとめ

  • Observerパターンは「状態変化の監視と通知」を分離し、新しい購読者をコード変更なしで追加できるようにする
  • TicketCraftのようなチケット管理ツールでは、担当者だけでなくウォッチャー全体への通知が必要になるため適用価値が高い
  • 同じイベントを複数の観測者が受け取るケースは日常的に発生するため、設計の基盤として押さえておく価値がある

🎓 理解度テスト

学んだ内容を確認しましょう。全3問。