0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

全30回:静的と動的でどう違うのか、JavaとPythonで学ぶデザインパターン - Day 12 Decoratorパターン:オブジェクトに動的に機能を追加する

Posted at

はじめに

皆さん、こんにちは!「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パターンでは、この問題を合成(コンポジション)によって解決します。

パターンの構成要素

  1. コンポーネント(Component): 共通の機能を定義するインターフェース
  2. 具象コンポーネント(ConcreteComponent): 基本的な機能を提供するクラス
  3. デコレーター(Decorator): コンポーネントインターフェースを実装し、内部でコンポーネントへの参照を保持する抽象クラス
  4. 具象デコレーター(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パターンの利点と注意点

利点

  1. 実行時の機能追加: オブジェクト作成後でも機能を追加・削除できる
  2. 柔軟な組み合わせ: 複数のデコレーターを自由に組み合わせられる
  3. 単一責任の原則: 各デコレーターは単一の機能に集中できる
  4. 継承の代替: 継承による組み合わせ爆発を回避できる

注意点

  1. 複雑性の増加: 多重のデコレーションは理解が困難になる場合がある
  2. デバッグの困難さ: 複数層のラッピングによりスタックトレースが複雑になる
  3. パフォーマンスオーバーヘッド: 委譲の連鎖により若干のパフォーマンス低下
  4. 型の問題: 動的な機能追加により、元の型では利用できないメソッドが追加される場合がある

実践的な使用例

Decoratorパターンは以下のような場面で特に有効です:

  • ログ記録: メソッドの実行ログを追加
  • キャッシュ: 計算結果のキャッシュ機能を追加
  • 認証・認可: セキュリティチェックを追加
  • トランザクション管理: データベース操作のトランザクション制御
  • GUI コンポーネント: ウィジェットにスクロールバーや境界線を追加
  • ストリーム処理: データ圧縮、暗号化、バッファリング機能を追加

まとめ:本質は「機能の合成」

特性 Java Python (OOP) Python (Function)
実装方法 インターフェースと抽象クラス ABCとクラス継承 関数とクロージャ
設計思想 継承の代わりにコンポジション 柔軟なオブジェクト合成 関数の動的変更
コードの複雑さ 比較的多い 中程度 非常に簡潔
型安全性 コンパイル時チェック 実行時チェック 実行時チェック
使用場面 大規模システムの構造設計 オブジェクト指向設計 関数の横断的関心事

Decoratorパターンは、言語によって実装方法が大きく異なりますが、 「オブジェクトの機能を動的に追加する」 という本質は共通です。Javaは厳密なクラス設計でこれを実現し、Pythonはオブジェクト指向アプローチと言語組み込みのデコレータ機能の両方を提供しています。

重要なのは、「継承よりも合成を優先する」という原則を理解し、柔軟で保守しやすいコードを書くことです。

次回は、複雑なサブシステムをシンプルにするFacadeパターンについて解説します。お楽しみに!

次回のテーマは、「Day 13 Facadeパターン:複雑なサブシステムを単純化する」です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?