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

Decorator:一覧表示への期限超過バッジ等の後付け装飾

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

📋 この記事の目次
  1. Decoratorは何のためにあるか
  2. Before:装飾のたびに元のクラスにif文が増える
  3. After:バッジをDecoratorとして重ねる
  4. こんな場面で使う/使わない
  5. まとめ
  6. 🎓 理解度テスト

TicketCraftの一覧画面には、「期限超過」「緊急」といったバッジを表示する機能があります。今のコードは、TicketRowクラスの中にすべてのバッジ表示ロジックが詰め込まれています。

public class TicketRow {
    private String title;
    private boolean overdue;
    private boolean urgent;

    public TicketRow(String title, boolean overdue, boolean urgent) {
        this.title = title;
        this.overdue = overdue;
        this.urgent = urgent;
    }

    // バッジを追加するたびに、このメソッドの中にif文が増えていく
    public String render() {
        String result = title;
        if (overdue) {
            result = "⏰[期限超過] " + result;
        }
        if (urgent) {
            result = "🔥[緊急] " + result;
        }
        return result;
    }
}

新しいバッジ(例えば「担当者未割り当て」)を追加するたびに、booleanフィールドとif文のペアが増えていきます。

Decoratorは何のためにあるか

Decoratorは、「既存のクラスを変更せずに、機能を後から重ね着させる」 パターンです。無地のTシャツにワッペンを縫い付けたり、上着を重ね着したりするように、元のオブジェクトを包む形で機能を追加していきます。

Before:装飾のたびに元のクラスにif文が増える

public class Main {
    public static void main(String[] args) {
        TicketRow row = new TicketRow("ログイン画面が真っ白になる", true, true);
        System.out.println(row.render());
    }
}

実行結果:

🔥[緊急] ⏰[期限超過] ログイン画面が真っ白になる

このやり方だと、TicketRowクラス自身が「どんなバッジがあるか」を全部知っていなければならず、バッジの種類が増えるほどクラスが肥大化していきます。

After:バッジをDecoratorとして重ねる

まず、共通のインターフェースを定義します。

public interface Renderable {
    String render();
}

public class BasicTicketRow implements Renderable {
    private String title;

    public BasicTicketRow(String title) {
        this.title = title;
    }

    @Override
    public String render() {
        return title;
    }
}

各バッジを、Renderableを包み込むDecoratorとして実装します。

public abstract class TicketRowDecorator implements Renderable {
    protected final Renderable wrapped;

    protected TicketRowDecorator(Renderable wrapped) {
        this.wrapped = wrapped;
    }
}

public class OverdueBadgeDecorator extends TicketRowDecorator {
    public OverdueBadgeDecorator(Renderable wrapped) {
        super(wrapped);
    }

    @Override
    public String render() {
        return "⏰[期限超過] " + wrapped.render();
    }
}

public class UrgentBadgeDecorator extends TicketRowDecorator {
    public UrgentBadgeDecorator(Renderable wrapped) {
        super(wrapped);
    }

    @Override
    public String render() {
        return "🔥[緊急] " + wrapped.render();
    }
}
public class Main {
    public static void main(String[] args) {
        Renderable row = new UrgentBadgeDecorator(
            new OverdueBadgeDecorator(
                new BasicTicketRow("ログイン画面が真っ白になる")));
        System.out.println(row.render());
    }
}

実行結果:

🔥[緊急] ⏰[期限超過] ログイン画面が真っ白になる

結果はBeforeと同じですが、BasicTicketRow自身はバッジについて何も知りません。新しいバッジ(「担当者未割り当て」)を追加したくなったら、TicketRowDecoratorを継承した新しいクラスを1つ足すだけで済み、BasicTicketRowは一切変更する必要がありません。バッジを重ねる順番も、呼び出し側で自由に組み替えられます。

🧓 ベテランの現場コラム Decoratorは、Java標準ライブラリの入出力クラス(ストリームをラップして機能を追加していく仕組み)でも実際に使われている、実務での登場頻度が高いパターンです。「オプション機能を後から柔軟に組み合わせたい」と感じたら候補に挙がりますが、包む層が何重にもなると、今度は「結局どのバッジがどんな順番で適用されているか」が読みにくくなることもあります。重ねる数が多くなりすぎないか、要所要所で見直しましょう。

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

  • 使う: 基本機能に対して、複数のオプション機能を自由な組み合わせで後付けしたい場合
  • 使わない: 追加したい機能が1〜2種類で固定しており、組み合わせを増やす予定がない場合。素直にif文で書いた方が読みやすいこともある

まとめ

  • Decoratorは「既存のクラスを変更せず、機能を後から重ね着させる」パターン
  • 新しい装飾を追加しても、既存のクラスやDecoratorには手を入れずに済む
  • 重ねすぎると読みにくくなるため、組み合わせの数と可読性のバランスを見る

🎓 理解度テスト

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