Javaのデコレータパターン:クラスを動的に拡張する
別名
- スマートプロキシ
- ラッパー
デコレータデザインパターンの意図
デコレータパターンは、既存のコードを変更することなく、オブジェクトに動的に責務を追加することを可能にします。これは、同様のインターフェースを持つオブジェクト内でオブジェクトを「ラップ」する方法を提供することで実現され、Javaデザインパターンの柔軟性を高めます。
現実世界の例を用いたデコレータパターンの詳細な説明
現実世界の例
コーヒーショップでコーヒーの注文をカスタマイズできると想像してください。基本的なコーヒーから始め、ミルク、砂糖、ホイップクリームなどのさまざまな材料を追加できます。それぞれの追加は、デコレータデザインパターンにおけるデコレータのようなものです。基本となるコーヒーオブジェクトは、追加機能(フレーバー、トッピング)で動的に装飾できます。たとえば、プレーンコーヒーオブジェクトから始め、ミルクデコレータでラップし、次に砂糖デコレータ、最後にホイップクリームデコレータでラップすることができます。各デコレータは、ソフトウェア設計におけるデコレータパターンの動作と同様に、コーヒーオブジェクトに新しい機能を追加したり、動作を変更したりします。
簡単な言葉で
デコレータパターンを使用すると、デコレータクラスのオブジェクトでラップすることにより、実行時にオブジェクトの動作を動的に変更できます。
Wikipediaによると
オブジェクト指向プログラミングでは、デコレータパターンは、同じクラスの他のオブジェクトの動作に影響を与えることなく、静的または動的に、個々のオブジェクトに動作を追加できるデザインパターンです。デコレータパターンは、単一責任原則の遵守に役立つことがよくあります。これは、機能を、関心領域がそれぞれ異なるクラスに分割できるためです。また、クラスの機能を修正することなく拡張できるため、開閉原則にも役立ちます。
Javaにおけるデコレータパターンのプログラム例
近くの丘には怒っているトロールが住んでいます。通常は素手ですが、武器を持っていることもあります。トロールに武器を持たせるために、新しいトロールを作成する必要はなく、適切な武器で動的に装飾するだけです。
まず、`Troll`インターフェースを実装する`SimpleTroll`があります。
public interface Troll {
void attack();
int getAttackPower();
void fleeBattle();
}
@Slf4j
public class SimpleTroll implements Troll {
@Override
public void attack() {
LOGGER.info("The troll tries to grab you!");
}
@Override
public int getAttackPower() {
return 10;
}
@Override
public void fleeBattle() {
LOGGER.info("The troll shrieks in horror and runs away!");
}
}
次に、トロールに棍棒を追加したいとします。デコレータを使用することで、動的に追加できます。
@Slf4j
@RequiredArgsConstructor
public class ClubbedTroll implements Troll {
private final Troll decorated;
@Override
public void attack() {
decorated.attack();
LOGGER.info("The troll swings at you with a club!");
}
@Override
public int getAttackPower() {
return decorated.getAttackPower() + 10;
}
@Override
public void fleeBattle() {
decorated.fleeBattle();
}
}
トロールの動作例です
public static void main(String[] args) {
// simple troll
LOGGER.info("A simple looking troll approaches.");
var troll = new SimpleTroll();
troll.attack();
troll.fleeBattle();
LOGGER.info("Simple troll power: {}.\n", troll.getAttackPower());
// change the behavior of the simple troll by adding a decorator
LOGGER.info("A troll with huge club surprises you.");
var clubbedTroll = new ClubbedTroll(troll);
clubbedTroll.attack();
clubbedTroll.fleeBattle();
LOGGER.info("Clubbed troll power: {}.\n", clubbedTroll.getAttackPower());
}
プログラム出力
11:34:18.098 [main] INFO com.iluwatar.decorator.App -- A simple looking troll approaches.
11:34:18.100 [main] INFO com.iluwatar.decorator.SimpleTroll -- The troll tries to grab you!
11:34:18.100 [main] INFO com.iluwatar.decorator.SimpleTroll -- The troll shrieks in horror and runs away!
11:34:18.100 [main] INFO com.iluwatar.decorator.App -- Simple troll power: 10.
11:34:18.100 [main] INFO com.iluwatar.decorator.App -- A troll with huge club surprises you.
11:34:18.101 [main] INFO com.iluwatar.decorator.SimpleTroll -- The troll tries to grab you!
11:34:18.101 [main] INFO com.iluwatar.decorator.ClubbedTroll -- The troll swings at you with a club!
11:34:18.101 [main] INFO com.iluwatar.decorator.SimpleTroll -- The troll shrieks in horror and runs away!
11:34:18.101 [main] INFO com.iluwatar.decorator.App -- Clubbed troll power: 20.
Javaでデコレータパターンを使用する場合
デコレータは、以下の目的で使用されます。
- 個々のオブジェクトに、動的かつ透過的に、つまり他のオブジェクトに影響を与えることなく責務を追加します。これは、Javaデザインパターンの重要な機能です。
- 取り消し可能な責務の場合。
- 結果として生じる可能性のあるサブクラスの増加により、クラスの拡張が現実的でない場合。
- クラス定義が隠されているか、サブクラス化に使用できない場合。
デコレータパターン Javaチュートリアル
Javaにおけるデコレータパターンの実際のアプリケーション
- GUIツールキットは、スクロール、境界線、レイアウト管理などの動作をコンポーネントに動的に追加するために、デコレータをよく使用します。
- Javaのjava.io.InputStream、java.io.OutputStream、java.io.Reader、java.io.Writerクラスは、デコレータパターンを利用した有名な例です。
- java.util.Collections#synchronizedXXX()
- java.util.Collections#unmodifiableXXX()
- java.util.Collections#checkedXXX()
デコレータパターンの利点と欠点
利点
- 静的継承よりも柔軟性が高い。
- 階層の上位にある機能満載のクラスを避け、Javaデザインパターンの威力を発揮します。
- デコレータとそのコンポーネントは同一ではありません。
- 責務は実行時に追加または削除できます。
欠点
- デコレータとそのコンポーネントは同一ではないため、オブジェクトタイプのテストは失敗します。
- デコレータは、プログラマにとって同じように見える小さなオブジェクトがたくさんあるシステムにつながる可能性があり、目的の構成を実現するのが難しくなります。
- 使いすぎると、多数の小さなクラスが導入されるため、コード構造が複雑になる可能性があります。
関連するJavaデザインパターン
- アダプター:デコレータはオブジェクトの責務を変更しますが、アダプターはオブジェクトのインターフェースを変更します。
- コンポジット:デコレータは、コンポーネントが1つだけの縮退コンポジットと見なすことができます。ただし、デコレータは追加の責務を追加します。オブジェクトの集約を目的としたものではありません。
- ストラテジー:デコレータはオブジェクトの外観を変更できますが、ストラテジーはオブジェクトの内部を変更できます。