広告

Java : Decorator パターン (図解/デザインパターン)

Decorator パターンとは、GoF によって定義されたデザインパターンの1つです。
機能を入れ子構造で持つことにより、複数の機能を動的に追加できます。

本記事では、Decorator パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

図解/デザインパターン一覧

概要

Decorator パターン(デコレータ・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定義されたデザインパターンの1つである。 このパターンは、既存のオブジェクトに新しい機能や振る舞いを動的に追加することを可能にする。

Decorator パターンとは、

  • 機能を 入れ子 構造で持つことにより
  • 複数の機能 を動的に追加する

という設計です。

Decorator は、日本語的に発音すると「デコレータ」となります。
意味は「装飾者」ですね。「デコレーション」ケーキなど、日本語でもそのまま使われていたりします。

【Decorator パターンのイメージ】

例えば、3つの機能

  • 機能(A)
  • 機能(B)
  • 機能(C)

があるとします。
これらは共通のインタフェースを持ちます。

イメージ図1

ただし、それぞれの機能は単独でもよいし、組み合わせても使いたい、そんな要求があるとしましょう。
単独を含めた機能の組み合わせは次のようになります。

  • 機能(A)
  • 機能(B)
  • 機能(C)
  • 機能(A + B)
  • 機能(A + C)
  • 機能(B + C)
  • 機能(A + B + C)

全部で7とおりにもなりますね。

イメージ図2

これだけのクラスを、共通インタフェースからそれぞれ実装するのは大変でしょう。
A, B, C の3つでこれなのですから、さらに4つ、5つとなると組み合わせは膨大になります。

この問題を解決するのが Decorator パターンです。

イメージ図3

繰り返しになりますが … Decorator パターンを使うと

  • 機能を 入れ子 構造で持つことにより
  • 複数の機能 を動的に追加

することができます。
機能を組み合わせた新しいクラスを作る必要はありません。

Java 標準API を例にすると、InputStreamOutputStream が、まさに Decorator パターンを使っています。
ファイルの入出力にも使われるため、ご存じのかたも多いのではないでしょうか。

InputStream を使った例については 後ほど解説 します。

関連記事:


クラス図とシーケンス図

クラス図1

シーケンス図1

上の図は、Decorator パターンの一般的なクラス図とシーケンス図です。

構造は Composite パターン に少し似ているかもしれません。
しかし、その目的はだいぶ違います。

それでは、このクラス図をそのままコードにしてみましょう。

まずは共通となる Component インタフェースです。

public interface Component {
    void operation();
}

次に Decorator クラスです。

Decorator クラスは、フィールド変数に Component オブジェクトを持つのがポイントですね。
これにより、自分自身を含めて Component オブジェクトを 入れ子 構造で保持することができます。

operation メソッドでは、処理をそのまま Component オブジェクトへ 委譲 します。

public class Decorator implements Component {
    private final Component component;

    public Decorator(Component component) {
        this.component = component;
    }

    @Override
    public void operation() {
        component.operation();
    }
}

ConcreteDecorator クラスでは、

  1. ベースクラス(super) の operation メソッドの呼び出し
  2. extra メソッドで固有の処理

を実行します。

public class ConcreteDecorator1 extends Decorator {
    public ConcreteDecorator1(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        extra();
    }

    private void extra() {
        System.out.print(" -> Decorator1");
    }
}

public class ConcreteDecorator2 extends Decorator {
    public ConcreteDecorator2(Component component) {
        super(component);
    }

    @Override
    public void operation() {
        super.operation();
        extra();
    }

    private void extra() {
        System.out.print(" -> Decorator2");
    }
}

最後に ConcreteComponent クラス です。

ConcreteComponent クラスでは Decorator クラスを継承しません。
Component インタフェースを直接実装します。

通常、Decorate しないクラスは 入れ子 構造の末端になります。

public class ConcreteComponent implements Component {
    @Override
    public void operation() {
        System.out.print("ConcreteComponent");
    }
}

それでは、これらのクラスを使ってみましょう。
入れ子 構造にするために、コンストラクタで Component オブジェクトを渡していきます。

final var component =
        new ConcreteDecorator2(
                new ConcreteDecorator1(
                        new ConcreteComponent()
                )
        );

component.operation();

// 結果
// ↓
//ConcreteComponent -> Decorator1 -> Decorator2

結果も問題なしですね。
入れ子構造になった Component オブジェクト、その operation メソッドが順々に実行されました。


具体的な例

もう少し具体的な例も見てみましょう。

クラス図2

Product は商品を表すインタフェースです。

  • getName : 商品名
  • getPrice : 商品の値段

というメソッドを持ちます。

public interface Product {
    String getName();

    int getPrice();
}

ProductImpl クラスは、単純に Product インタフェースを実装したクラスです。

public class ProductImpl implements Product {
    private final String name;
    private final int price;

    public ProductImpl(String name, int price) {
        this.name = name;
        this.price = price;
    }

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

    @Override
    public int getPrice() {
        return price;
    }
}

ProductImpl クラスを使う例です。
商品として「メロンパン」を作成してみましょう。

final var product = new ProductImpl("メロンパン", 100);
System.out.println("商品名 : " + product.getName());
System.out.println("値段  : " + product.getPrice() + " 円");

// 結果
// ↓
//商品名 : メロンパン
//値段  : 100 円

使い方もシンプルなので問題ないかと思います。

さて、メロンパンですが、期間限定で半額セールをすることになりました。
Decorator パターンを使って実現してみましょう。

まずは ProductDecorator クラスです。

public class ProductDecorator implements Product {
    private final Product product;

    public ProductDecorator(Product product) {
        this.product = product;
    }

    @Override
    public String getName() {
        return product.getName();
    }

    @Override
    public int getPrice() {
        return product.getPrice();
    }
}

コンストラクタで Product オブジェクトを受け取ります。
getNamegetPrice メソッドでは、その Product オブジェクトに処理を 委譲 するだけです。

次に HalfPrice クラスです。
半額セール用ですね。

HalfPrice クラスは ProductDecorator クラスを継承します。

public class HalfPrice extends ProductDecorator {
    public HalfPrice(Product product) {
        super(product);
    }

    @Override
    public String getName() {
        final var name = super.getName();
        return name + "【半額セール!】";
    }

    @Override
    public int getPrice() {
        final var price = super.getPrice();
        return (int) (price * 0.5);
    }
}

getNamegetPrice メソッドでは…

  • ベースクラス(super) の値を取得
  • その値を半額セール用に加工

という処理をします。

それでは、ProductDecorator クラスを使ってみましょう。

final var product =
        new HalfPrice(
                new ProductImpl("メロンパン", 100)
        );
System.out.println("商品名 : " + product.getName());
System.out.println("値段  : " + product.getPrice() + " 円");

// 結果
// ↓
//商品名 : メロンパン【半額セール!】
//値段  : 50 円

無事に、半額セール用の商品名と値段が表示できました。

やれやれ…と思っていたところに、もう1つの要望がきました。
今度は「メロンパン」に 10% の消費税を考慮してほしい、と。

さっそく対応してみましょう。
Tax クラスでは、10% の消費税の処理を行います。

public class Tax extends ProductDecorator {
    public Tax(Product product) {
        super(product);
    }

    @Override
    public String getName() {
        final var name = super.getName();
        return name + "(10%税込)";
    }

    @Override
    public int getPrice() {
        final var price = super.getPrice();
        return (int) (price + price * 0.1);
    }
}

それでは Tax クラスを使ってみましょう。

final var product =
        new Tax(
                new HalfPrice(
                        new ProductImpl("メロンパン", 100)
                )
        );
System.out.println("商品名 : " + product.getName());
System.out.println("値段  : " + product.getPrice() + " 円");

// 結果
// ↓
//商品名 : メロンパン【半額セール!】(10%税込)
//値段  : 55 円

無事に、半額セールと消費税に対応できました。

さて、しばらくして半額セールの期間が終わりました。

Decorator パターンを使っているので対応は簡単ですね。
Product の生成処理から、HalfPrice を抜くだけです。

final var product =
        new Tax(
                new ProductImpl("メロンパン", 100)
        );
System.out.println("商品名 : " + product.getName());
System.out.println("値段  : " + product.getPrice() + " 円");

// 結果
// ↓
//商品名 : メロンパン(10%税込)
//値段  : 110 円

Java 標準API で使われている例

この抽象クラスは、バイト入力ストリームを表現するすべてのクラスのスーパー・クラスです。

Java 標準API で Decorator パターンが使われている例を見てみましょう。

InputStream は、Java の IOストリーム API の中心となるクラスです。
データをストリームとして読み込むことができます。

(データ書き込み用の OutputStream もありますが、ここでは割愛します)

クラス図3

さっそくコード例を見てみましょう。
Files.newInputStream メソッドは、指定したファイルからデータを読み込むための InputStream オブジェクトを返します。

また、読み込むファイルには事前にバイナリデータ(10, 20, 30) を書き込んでおきます。

final var path = Path.of("R:", "java-work", "a.txt");
Files.write(path, new byte[]{10, 20, 30});

try (final var inputStream = Files.newInputStream(path)) {
    System.out.println(inputStream.read()); // 10
    System.out.println(inputStream.read()); // 20
    System.out.println(inputStream.read()); // 30
}

InputStream の read メソッドで、バイナリデータを1つずつ読み込むことができました。

しかし、メモリバッファを使っていないので、少しパフォーマンスが心配かもしれません。
ということで、バッファ機能を備えた BufferedInputStream を使ってみましょう。

final var path = Path.of("R:", "java-work", "a.txt");
Files.write(path, new byte[]{10, 20, 30});

try (final var inputStream =
             new BufferedInputStream(
                     Files.newInputStream(path)
             )
) {
    System.out.println(inputStream.read()); // 10
    System.out.println(inputStream.read()); // 20
    System.out.println(inputStream.read()); // 30
}

BufferedInputStream クラスのコンストラクタで、InputStream オブジェクトを渡します。
入れ子 構造で保持するためですね。

これで InputStream にバッファ機能が追加されました。

それでは、さらに PushbackInputStream クラスを使ってみましょう。
このクラスでは、読み込むストリームにデータを書き戻す(プッシュバックする) ことができます。

final var path = Path.of("R:", "java-work", "a.txt");
Files.write(path, new byte[]{10, 20, 30});

try (final var inputStream =
             new PushbackInputStream(
                     new BufferedInputStream(
                             Files.newInputStream(path)
                     )
             )
) {

    System.out.println(inputStream.read()); // 10
    System.out.println(inputStream.read()); // 20

    // 99 をプッシュバック
    inputStream.unread(99);

    System.out.println(inputStream.read()); // 99
    System.out.println(inputStream.read()); // 30
}

PushbackInputStream クラスでも、コンストラクタで InputStream オブジェクトを渡します。
これで、バッファ機能とプッシュバック機能を持った InputStream ができました。

今回、InputStream はファイルデータを対象にしましたが、ネットワークデータを対象にすることもできます。
もちろん、ネットワークデータに対しても BufferedInputStream を使いバッファリングできます。

とても柔軟な API ですね。

まとめ

Decorator パターンとは、

  • 機能を 入れ子 構造で持つことにより
  • 複数の機能 を動的に追加する

という設計です。

複数の機能を組み合わせて使いたいケースでは、Decorator パターンを検討してみましょう。


関連記事

ページの先頭へ