Java : Decorator パターン (図解/デザインパターン)
Decorator パターンとは、GoF によって定義されたデザインパターンの1つです。
機能を入れ子構造で持つことにより、複数の機能を動的に追加できます。
本記事では、Decorator パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
Decorator パターンとは、
- 機能を 入れ子 構造で持つことにより
- 複数の機能 を動的に追加する
という設計です。
Decorator は、日本語的に発音すると「デコレータ」となります。
意味は「装飾者」ですね。「デコレーション」ケーキなど、日本語でもそのまま使われていたりします。
【Decorator パターンのイメージ】
例えば、3つの機能
- 機能(A)
- 機能(B)
- 機能(C)
があるとします。
これらは共通のインタフェースを持ちます。
ただし、それぞれの機能は単独でもよいし、組み合わせても使いたい、そんな要求があるとしましょう。
単独を含めた機能の組み合わせは次のようになります。
- 機能(A)
- 機能(B)
- 機能(C)
- 機能(A + B)
- 機能(A + C)
- 機能(B + C)
- 機能(A + B + C)
全部で7とおりにもなりますね。
これだけのクラスを、共通インタフェースからそれぞれ実装するのは大変でしょう。
A, B, C の3つでこれなのですから、さらに4つ、5つとなると組み合わせは膨大になります。
この問題を解決するのが Decorator パターンです。
繰り返しになりますが … Decorator パターンを使うと
- 機能を 入れ子 構造で持つことにより
- 複数の機能 を動的に追加
することができます。
機能を組み合わせた新しいクラスを作る必要はありません。
Java 標準API を例にすると、InputStream や OutputStream が、まさに Decorator パターンを使っています。
ファイルの入出力にも使われるため、ご存じのかたも多いのではないでしょうか。
InputStream を使った例については 後ほど解説 します。
関連記事:
クラス図とシーケンス図
上の図は、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 クラスでは、
- ベースクラス(super) の operation メソッドの呼び出し
- 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 メソッドが順々に実行されました。
具体的な例
もう少し具体的な例も見てみましょう。
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 オブジェクトを受け取ります。
getName と getPrice メソッドでは、その 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);
}
}
getName と getPrice メソッドでは…
- ベースクラス(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 もありますが、ここでは割愛します)
さっそくコード例を見てみましょう。
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 パターンを検討してみましょう。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン