はじめに
本記事は、GoFデザインパターン解説シリーズの1つです。
今回は、構造に関するパターンの中のDecorator(デコレーター)パターンを解説します。
Java標準ライブラリの入出力(I/O)まわりにも使われている、実務でも馴染み深いパターンです。
解決したい課題
「基本機能を持つクラスに、後からオプション機能を追加したい」という場面があります。
素朴な方法として継承(サブクラス化)を使うと、追加したい機能の組み合わせの数だけサブクラスが必要になってしまいます。
例えば、コーヒー(Beverage)に「ミルクを追加」「シロップを追加」「ホイップクリームを追加」というオプションを、自由に組み合わせて追加したいとします。
継承だけで実現しようとすると、CoffeeWithMilk・CoffeeWithSyrup・CoffeeWithMilkAndSyrup・CoffeeWithMilkAndWhip...と、オプションの組み合わせの数だけクラスが爆発的に増えてしまいます。
Decoratorパターンは、元のオブジェクトを「包み込む(ラップする)」オブジェクトを重ねていくことで、実行時に動的に機能を追加することで、この問題を解決します。
クラス構成
- Component(コンポーネント): 基本機能・追加機能の両方に共通するインターフェース
- ConcreteComponent(具体的なコンポーネント): 基本機能を持つ、装飾される前の元のオブジェクト
- Decorator(デコレーター): Componentを実装しつつ、内部に別のComponentを保持する抽象クラス
- ConcreteDecorator(具体的なデコレーター): 実際に追加機能を提供する、具体的なDecoratorクラス
Javaでの実装例
コーヒーの例で実装してみます。
Component:コーヒー・オプション共通のインターフェース
public interface Beverage {
String getDescription();
double getCost();
}
ConcreteComponent:基本となるコーヒー
public class SimpleCoffee implements Beverage {
@Override
public String getDescription() {
return "コーヒー";
}
@Override
public double getCost() {
return 300;
}
}
Decorator:オプションの抽象基底クラス
public abstract class BeverageDecorator implements Beverage {
protected final Beverage wrapped; // 装飾対象(元のBeverage)を保持する
protected BeverageDecorator(Beverage wrapped) {
this.wrapped = wrapped;
}
}
ConcreteDecorator:具体的なオプション群
public class MilkDecorator extends BeverageDecorator {
public MilkDecorator(Beverage wrapped) {
super(wrapped);
}
@Override
public String getDescription() {
return wrapped.getDescription() + " + ミルク";
}
@Override
public double getCost() {
return wrapped.getCost() + 50;
}
}
public class SyrupDecorator extends BeverageDecorator {
public SyrupDecorator(Beverage wrapped) {
super(wrapped);
}
@Override
public String getDescription() {
return wrapped.getDescription() + " + シロップ";
}
@Override
public double getCost() {
return wrapped.getCost() + 30;
}
}
public class WhipDecorator extends BeverageDecorator {
public WhipDecorator(Beverage wrapped) {
super(wrapped);
}
@Override
public String getDescription() {
return wrapped.getDescription() + " + ホイップクリーム";
}
@Override
public double getCost() {
return wrapped.getCost() + 80;
}
}
利用側のコード
public class Main {
public static void main(String[] args) {
// 基本のコーヒー
Beverage coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " : " + coffee.getCost() + "円");
// ミルクだけ追加
Beverage coffeeWithMilk = new MilkDecorator(new SimpleCoffee());
System.out.println(coffeeWithMilk.getDescription() + " : " + coffeeWithMilk.getCost() + "円");
// ミルク + シロップ + ホイップクリームを、自由な順番で重ねて追加
Beverage fullyLoaded = new WhipDecorator(
new SyrupDecorator(
new MilkDecorator(
new SimpleCoffee()
)
)
);
System.out.println(fullyLoaded.getDescription() + " : " + fullyLoaded.getCost() + "円");
}
}
実行結果:
コーヒー : 300.0円
コーヒー + ミルク : 350.0円
コーヒー + ミルク + シロップ + ホイップクリーム : 460.0円
WhipDecoratorはSyrupDecoratorを、SyrupDecoratorはMilkDecoratorを、MilkDecoratorはSimpleCoffeeを、それぞれ内部に保持しています。
getCost()を呼び出すと、この入れ子構造を内側から外側へ順番に辿りながら値段が加算されていきます。
オプションを追加するたびにクラスを新しく作る必要はなく、既存のDecoratorクラスを自由な数・自由な順序で組み合わせるだけで、あらゆるバリエーションに対応できます。
Java標準ライブラリでの実例
Decoratorパターンは、Java標準のjava.ioパッケージで広く使われています。
// FileInputStreamという基本のストリームを、BufferedInputStreamでラップしてバッファリング機能を追加
InputStream input = new BufferedInputStream(new FileInputStream("data.txt"));
BufferedInputStreamはInputStream(Component)を実装しつつ、内部に別のInputStreamを保持し、バッファリングという機能を追加でラップしています。
これはまさに本記事のコーヒーの例と同じ構造です。
Proxyパターンとの違い
構造だけを見るとDecoratorは、後述するProxyパターンとよく似ています(どちらも「同じインターフェースを実装したクラスで、元のオブジェクトをラップする」構造です)。
違いは目的にあります。
Decoratorは「機能を追加する」ことが目的であるのに対し、Proxyは「アクセス制御や遅延生成など、元のオブジェクトへのアクセスを管理する」ことが目的です。
Proxyパターンの詳細は、別記事で解説します。
使用場面
- 基本機能に対して、複数のオプション機能を自由に組み合わせて追加したい場合
- 継承によるサブクラスの組み合わせ爆発を避けたい場合
- 実行時に動的に(実行してみるまで分からない条件に応じて)機能を追加・変更したい場合
- 入出力ストリームの機能拡張(バッファリング、暗号化、圧縮など)
まとめ
| 項目 | 内容 |
|---|---|
| 解決する課題 | 継承でオプション機能を組み合わせようとすると、サブクラスが組み合わせ爆発してしまう |
| 実現方法 | 元のオブジェクトを、同じインターフェースを持つDecoratorで何重にもラップし、実行時に機能を積み重ねる |
| メリット | オプションを自由な数・順序で組み合わせられ、新機能の追加も既存コードを変更せずに行える |
| 実例 | Java標準のjava.io(BufferedInputStream等)が代表例 |
おすすめ書籍
GoFの23パターンをより深く学びたい方には、以下の書籍がおすすめです。
Java言語のサンプルコードとUMLを使い、初心者にもわかりやすく解説されています。
Java言語で学ぶデザインパターン入門 第3版 [ 結城 浩 ]
※本リンクはアフィリエイトリンクを含みます。


