本文へスキップ
BecomeCoder

Javaデザインパターンコース · 第3章 構造に関するパターン ― オブジェクトの「組み合わせ方」を工夫する · レッスン7

Decorator ― 機能を後付けで積み重ねる

ブラウザで完結

導入

コーヒーに「ミルクを追加」「砂糖を追加」「ホイップを追加」……こうしたオプションの組み合わせを、継承MilkAndSugarCoffee のようなクラス)で全パターン分作るのは非現実的です。Decorator(デコレータ) は、同じインタフェースを実装した「包み紙」を何重にも重ねることで、機能を柔軟に追加する型です。

説明

DecoratorはAdapterと似た「ラップする」形をしていますが、目的が違います。Adapterは「インタフェースを変換する」ため、Decoratorは「同じインタフェースのまま機能を上乗せする」ためです。

public class Main {
    public static void main(String[] args) {
        Coffee coffee = new Milk(new Sugar(new SimpleCoffee()));
        System.out.println(coffee.getDescription() + " " + coffee.getCost() + "円");
    }
}

interface Coffee {
    String getDescription();
    int getCost();
}

class SimpleCoffee implements Coffee {
    public String getDescription() {
        return "コーヒー";
    }
    public int getCost() {
        return 300;
    }
}

abstract class CoffeeDecorator implements Coffee {
    protected Coffee wrapped;

    CoffeeDecorator(Coffee wrapped) {
        this.wrapped = wrapped;
    }
}

class Milk extends CoffeeDecorator {
    Milk(Coffee wrapped) {
        super(wrapped);
    }
    public String getDescription() {
        return wrapped.getDescription() + " + ミルク";
    }
    public int getCost() {
        return wrapped.getCost() + 50;
    }
}

class Sugar extends CoffeeDecorator {
    Sugar(Coffee wrapped) {
        super(wrapped);
    }
    public String getDescription() {
        return wrapped.getDescription() + " + 砂糖";
    }
    public int getCost() {
        return wrapped.getCost() + 20;
    }
}
classDiagram
  Coffee <|.. SimpleCoffee
  Coffee <|.. CoffeeDecorator
  CoffeeDecorator <|-- Milk
  CoffeeDecorator <|-- Sugar
  CoffeeDecorator --> Coffee : wraps
  class Coffee {
    <<interface>>
    +getDescription() String
    +getCost() int
  }
  • new Milk(new Sugar(new SimpleCoffee())) … いちばん内側の SimpleCoffee(素のコーヒー、300円)を Sugar が包み、それをさらに Milk が包みます。
  • CoffeeDecoratorCoffee を実装しつつ protected Coffee wrapped; で「包んでいる相手」を保持する共通の抽象クラスです。MilkSugar もこれを継承します。
  • Milk.getCost()wrapped.getCost() + 50 と、内側の結果に自分の分を足して返すだけです。何重に包んでも同じ仕組みで金額が積み上がります(300 + 20 + 50 = 370円)。
  • 包む順番や個数を自由に変えられるのが継承だけの設計との違いです。「ミルクだけ」「砂糖だけ」「両方」も全部同じクラスの組み合わせで表現できます。

やってみよう

下のエディタを実行し、370円になることと、getDescription() が包んだ順に説明を積み上げていくことを確認しましょう。包む順序を new Sugar(new Milk(new SimpleCoffee())) に変えると説明文の順番がどう変わるか試してみてください。

演習

Whip という CoffeeDecorator を追加してください(+80円、説明に " + ホイップ" を追加)。mainnew Whip(new Milk(new SimpleCoffee())) を作り、説明と金額を表示してください。

ヒント1を見る

Milk をまねて class Whip extends CoffeeDecorator { Whip(Coffee wrapped) { super(wrapped); } public String getDescription() { return wrapped.getDescription() + " + ホイップ"; } public int getCost() { return wrapped.getCost() + 80; } }

ヒント2を見る

300(コーヒー) + 50(ミルク) + 80(ホイップ) = 430円になります。

実際に動かしてみよう

下のエディタにJavaを書いて「実行」を押すと、学習用シミュレータが System.out.println の出力を表示します(本物のJVMではなく、教材の範囲を再現した軽量エンジンです)。クラス・継承・コレクションなども動きます。Scanner で入力を読む例は「標準入力」欄に値を入れてください。本文の例を書き換えて動かしてみましょう(Stream・ラムダ・スレッド・ファイル入出力など一部の機能は対象外です)。

Java — ブラウザ内で実行(学習用シミュレータ)

Javaの教材サブセットを動かす学習用シミュレータを読み込みます(本物のJVMではなく、動きを再現した軽量な自作エンジンです)。
スクロールして表示された時点でも自動で読み込まれます。