はじめに
皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第12回目です。
今回は、オブジェクトの機能を継承を使わずに動的に拡張するためのDecorator(デコレーター)パターンについて解説します。
Decoratorパターンとは?
Decoratorパターンは、既存のオブジェクトをラップ(包装)し、新しい機能を追加することで、そのオブジェクトの振る舞いを変更する構造パターンです。これは、継承による機能追加が困難な場合に非常に有効です。
身近な例
シンプルなコーヒーに、ミルク、シロップ、ホイップクリームといった追加トッピングを加えていくことを考えてみましょう。それぞれのトッピング(デコレーター)は、コーヒー(元のオブジェクト)の味(機能)を個別に変更し、それらを自由に組み合わせることができます。
Decoratorパターンが解決する問題
継承による機能追加では、以下のような問題が発生します:
// 問題のあるアプローチ:継承による組み合わせ爆発
class SimpleCoffee { ... }
class MilkCoffee extends SimpleCoffee { ... }
class WhipCoffee extends SimpleCoffee { ... }
class MilkAndWhipCoffee extends SimpleCoffee { ... }
class SugarMilkAndWhipCoffee extends SimpleCoffee { ... }
// トッピングの組み合わせ数だけクラスが必要...
Decoratorパターンでは、この問題を合成(コンポジション)によって解決します。
パターンの構成要素
- コンポーネント(Component): 共通の機能を定義するインターフェース
- 具象コンポーネント(ConcreteComponent): 基本的な機能を提供するクラス
- デコレーター(Decorator): コンポーネントインターフェースを実装し、内部でコンポーネントへの参照を保持する抽象クラス
- 具象デコレーター(ConcreteDecorator): 新しい機能を追加するクラス
Javaでの実装:厳格なインターフェースとクラスの合成
Javaは厳格な型システムを持つため、Decoratorパターンは共通のインターフェースを介して実装されます。
以下に、コーヒーにトッピングを追加する例を示します。
// 1. コンポーネントインターフェース
interface Coffee {
String getDescription();
double getCost();
int getCalories(); // カロリー情報も追加
}
// 2. 具象コンポーネント
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double getCost() {
return 5.0;
}
@Override
public int getCalories() {
return 5;
}
}
class Espresso implements Coffee {
@Override
public String getDescription() {
return "Espresso";
}
@Override
public double getCost() {
return 4.0;
}
@Override
public int getCalories() {
return 3;
}
}
// 3. デコレーターの抽象クラス
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}
// デフォルトの委譲実装
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
@Override
public int getCalories() {
return decoratedCoffee.getCalories();
}
}
// 4. 具象デコレーター
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public String getDescription() {
return super.getDescription() + ", Milk";
}
@Override
public double getCost() {
return super.getCost() + 2.0;
}
@Override
public int getCalories() {
return super.getCalories() + 50;
}
}
class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public String getDescription() {
return super.getDescription() + ", Whip Cream";
}
@Override
public double getCost() {
return super.getCost() + 3.0;
}
@Override
public int getCalories() {
return super.getCalories() + 80;
}
}
class SugarDecorator extends CoffeeDecorator {
private int spoons;
public SugarDecorator(Coffee decoratedCoffee, int spoons) {
super(decoratedCoffee);
this.spoons = spoons;
}
@Override
public String getDescription() {
return super.getDescription() + ", Sugar(" + spoons + " spoons)";
}
@Override
public double getCost() {
return super.getCost() + (0.5 * spoons);
}
@Override
public int getCalories() {
return super.getCalories() + (16 * spoons);
}
}
// 使用例とユーティリティクラス
class CoffeeShop {
public static void printCoffeeInfo(Coffee coffee) {
System.out.printf("%-30s - Cost: $%.2f, Calories: %d%n",
coffee.getDescription(), coffee.getCost(), coffee.getCalories());
}
public static void main(String[] args) {
System.out.println("=== Coffee Shop Menu ===");
// シンプルなコーヒー
Coffee simpleCoffee = new SimpleCoffee();
printCoffeeInfo(simpleCoffee);
// ミルクを追加
Coffee milkCoffee = new MilkDecorator(simpleCoffee);
printCoffeeInfo(milkCoffee);
// さらにホイップクリームを追加
Coffee whipMilkCoffee = new WhipDecorator(milkCoffee);
printCoffeeInfo(whipMilkCoffee);
// エスプレッソベースの複雑な組み合わせ
Coffee specialCoffee = new WhipDecorator(
new SugarDecorator(
new MilkDecorator(new Espresso()), 2));
printCoffeeInfo(specialCoffee);
// 動的にデコレーターを追加
Coffee dynamicCoffee = new SimpleCoffee();
String[] toppings = {"milk", "whip", "sugar"};
for (String topping : toppings) {
switch (topping) {
case "milk":
dynamicCoffee = new MilkDecorator(dynamicCoffee);
break;
case "whip":
dynamicCoffee = new WhipDecorator(dynamicCoffee);
break;
case "sugar":
dynamicCoffee = new SugarDecorator(dynamicCoffee, 1);
break;
}
}
System.out.println("\n=== Dynamically Created Coffee ===");
printCoffeeInfo(dynamicCoffee);
}
}
Javaでは、コンポーネントをラップするという概念を、クラスの継承とコンポジション(合成)を組み合わせて厳密に表現します。
Pythonでの実装:オブジェクト指向と関数デコレータの両方
Pythonでは、オブジェクト指向のDecoratorパターンと、言語機能としての関数デコレータの両方を活用できます。
オブジェクト指向のDecoratorパターン
まず、Javaと同様のオブジェクト指向アプローチを見てみましょう:
from abc import ABC, abstractmethod
# 1. コンポーネントインターフェース
class Coffee(ABC):
@abstractmethod
def get_description(self) -> str:
pass
@abstractmethod
def get_cost(self) -> float:
pass
@abstractmethod
def get_calories(self) -> int:
pass
# 2. 具象コンポーネント
class SimpleCoffee(Coffee):
def get_description(self) -> str:
return "Simple Coffee"
def get_cost(self) -> float:
return 5.0
def get_calories(self) -> int:
return 5
class Espresso(Coffee):
def get_description(self) -> str:
return "Espresso"
def get_cost(self) -> float:
return 4.0
def get_calories(self) -> int:
return 3
# 3. デコレーター基底クラス
class CoffeeDecorator(Coffee):
def __init__(self, coffee: Coffee):
self._coffee = coffee
def get_description(self) -> str:
return self._coffee.get_description()
def get_cost(self) -> float:
return self._coffee.get_cost()
def get_calories(self) -> int:
return self._coffee.get_calories()
# 4. 具象デコレーター
class MilkDecorator(CoffeeDecorator):
def get_description(self) -> str:
return f"{super().get_description()}, Milk"
def get_cost(self) -> float:
return super().get_cost() + 2.0
def get_calories(self) -> int:
return super().get_calories() + 50
class WhipDecorator(CoffeeDecorator):
def get_description(self) -> str:
return f"{super().get_description()}, Whip Cream"
def get_cost(self) -> float:
return super().get_cost() + 3.0
def get_calories(self) -> int:
return super().get_calories() + 80
class SugarDecorator(CoffeeDecorator):
def __init__(self, coffee: Coffee, spoons: int = 1):
super().__init__(coffee)
self.spoons = spoons
def get_description(self) -> str:
return f"{super().get_description()}, Sugar({self.spoons} spoons)"
def get_cost(self) -> float:
return super().get_cost() + (0.5 * self.spoons)
def get_calories(self) -> int:
return super().get_calories() + (16 * self.spoons)
# 使用例
def print_coffee_info(coffee: Coffee):
print(f"{coffee.get_description():<30} - Cost: ${coffee.get_cost():.2f}, Calories: {coffee.get_calories()}")
# 基本的な使用
simple_coffee = SimpleCoffee()
print_coffee_info(simple_coffee)
# デコレートされたコーヒー
special_coffee = WhipDecorator(
SugarDecorator(
MilkDecorator(Espresso()), 2))
print_coffee_info(special_coffee)
Pythonの関数デコレータ
Pythonには、Decoratorパターンを言語機能として組み込んだ@構文があります:
import functools
import time
from typing import Any, Callable
# ログ出力デコレータ
def log_calls(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
print(f"🔍 Calling function: {func.__name__}")
result = func(*args, **kwargs)
print(f"✅ Finished function: {func.__name__}")
return result
return wrapper
# 実行時間測定デコレータ
def measure_time(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"⏱️ {func.__name__} executed in {end_time - start_time:.4f} seconds")
return result
return wrapper
# キャッシュデコレータ
def simple_cache(func: Callable) -> Callable:
cache = {}
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
# 簡単なキャッシュキーの作成
key = str(args) + str(sorted(kwargs.items()))
if key in cache:
print(f"💾 Cache hit for {func.__name__}")
return cache[key]
result = func(*args, **kwargs)
cache[key] = result
print(f"💿 Cache miss for {func.__name__}, storing result")
return result
return wrapper
# 複数のデコレータを組み合わせて使用
@log_calls
@measure_time
@simple_cache
def fibonacci(n: int) -> int:
"""フィボナッチ数を計算する関数"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@log_calls
def say_hello(name: str) -> str:
"""挨拶する関数"""
time.sleep(1) # 処理時間をシミュレート
return f"Hello, {name}!"
# 使用例
def demo_function_decorators():
print("=== Function Decorator Demo ===")
# シンプルな関数の実行
result = say_hello("Alice")
print(f"Result: {result}")
print("\n=== Fibonacci with Multiple Decorators ===")
# フィボナッチ数の計算(キャッシュ効果を確認)
fib_result1 = fibonacci(10)
print(f"fibonacci(10) = {fib_result1}")
fib_result2 = fibonacci(10) # キャッシュからの取得
print(f"fibonacci(10) = {fib_result2}")
# クラスデコレータの例
def add_string_representation(cls):
"""クラスに__str__メソッドを動的に追加するデコレータ"""
def __str__(self):
attrs = ', '.join(f"{k}={v}" for k, v in self.__dict__.items())
return f"{cls.__name__}({attrs})"
cls.__str__ = __str__
return cls
@add_string_representation
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
# 実行例
def main():
print("=== Object-Oriented Decorator Pattern ===")
# オブジェクト指向のDecoratorパターン
simple_coffee = SimpleCoffee()
print_coffee_info(simple_coffee)
decorated_coffee = WhipDecorator(MilkDecorator(simple_coffee))
print_coffee_info(decorated_coffee)
print()
demo_function_decorators()
print("\n=== Class Decorator Demo ===")
person = Person("Bob", 30)
print(person) # デコレータによって追加された__str__メソッドが使用される
if __name__ == "__main__":
main()
Decoratorパターンの利点と注意点
利点
- 実行時の機能追加: オブジェクト作成後でも機能を追加・削除できる
- 柔軟な組み合わせ: 複数のデコレーターを自由に組み合わせられる
- 単一責任の原則: 各デコレーターは単一の機能に集中できる
- 継承の代替: 継承による組み合わせ爆発を回避できる
注意点
- 複雑性の増加: 多重のデコレーションは理解が困難になる場合がある
- デバッグの困難さ: 複数層のラッピングによりスタックトレースが複雑になる
- パフォーマンスオーバーヘッド: 委譲の連鎖により若干のパフォーマンス低下
- 型の問題: 動的な機能追加により、元の型では利用できないメソッドが追加される場合がある
実践的な使用例
Decoratorパターンは以下のような場面で特に有効です:
- ログ記録: メソッドの実行ログを追加
- キャッシュ: 計算結果のキャッシュ機能を追加
- 認証・認可: セキュリティチェックを追加
- トランザクション管理: データベース操作のトランザクション制御
- GUI コンポーネント: ウィジェットにスクロールバーや境界線を追加
- ストリーム処理: データ圧縮、暗号化、バッファリング機能を追加
まとめ:本質は「機能の合成」
| 特性 | Java | Python (OOP) | Python (Function) |
|---|---|---|---|
| 実装方法 | インターフェースと抽象クラス | ABCとクラス継承 | 関数とクロージャ |
| 設計思想 | 継承の代わりにコンポジション | 柔軟なオブジェクト合成 | 関数の動的変更 |
| コードの複雑さ | 比較的多い | 中程度 | 非常に簡潔 |
| 型安全性 | コンパイル時チェック | 実行時チェック | 実行時チェック |
| 使用場面 | 大規模システムの構造設計 | オブジェクト指向設計 | 関数の横断的関心事 |
Decoratorパターンは、言語によって実装方法が大きく異なりますが、 「オブジェクトの機能を動的に追加する」 という本質は共通です。Javaは厳密なクラス設計でこれを実現し、Pythonはオブジェクト指向アプローチと言語組み込みのデコレータ機能の両方を提供しています。
重要なのは、「継承よりも合成を優先する」という原則を理解し、柔軟で保守しやすいコードを書くことです。
次回は、複雑なサブシステムをシンプルにするFacadeパターンについて解説します。お楽しみに!
次回のテーマは、「Day 13 Facadeパターン:複雑なサブシステムを単純化する」です。