👋

Rubyで学ぶデコレータパターン (Decorator Pattern)

2025/01/13に公開

1. どんなもの?

デコレータパターンは、既存のオブジェクトに新しい機能を動的に追加するデザインパターンです。
オブジェクトの構造を変更することなく、柔軟に振る舞いを拡張できます。

2. 通常の実装方法と比べてどこがすごいの?

通常の方法

新しい振る舞いを持つオブジェクトを作成するには、通常サブクラスを作成します。

class BaseComponent
  def operation
    "Base"
  end
end

class ExtendedComponent < BaseComponent
  def operation
    "#{super} + Extended"
  end
end

component = ExtendedComponent.new
puts component.operation # "Base + Extended"
  • サブクラスが増えると管理が煩雑になる。
    • オブジェクトに新しい振る舞いを追加する際、サブクラスを作る方法は一般的ですが、複数の振る舞いを組み合わせる必要が出てくると、サブクラスの数が爆発的に増える可能性があります。
    • 例: 1つの基本クラスに対して、次のような拡張が必要だとします:
      • 振る舞いA
      • 振る舞いB
      • 振る舞いAとBの両方
    • この場合、サブクラスは3つ必要になります。
      • class BaseComponent
          def operation
            "Base"
          end
        end
        
        class ComponentWithBehaviorA < BaseComponent
          def operation
            "#{super} + A"
          end
        end
        
        class ComponentWithBehaviorB < BaseComponent
          def operation
            "#{super} + B"
          end
        end
        
        class ComponentWithBothBehaviors < BaseComponent
          def operation
            "#{super} + A + B"
          end
        end
        
      • この場合、振る舞いが増えるたびに新しいサブクラスを作る必要があり、 組み合わせが増えるほどクラスの数が爆発的に増える(クラスの爆発) 問題が発生します。
      • また、上記理由により動的に振る舞いを切り替えることが難しい。

デコレータパターンの場合

デコレータを使うと、振る舞いを1つのサブクラスにまとめ、それを動的に組み合わせることができます。

  • class BaseComponent
      def operation
        "Base"
      end
    end
    
    class BehaviorADecorator
      def initialize(component)
        @component = component
      end
    
      def operation
        "#{@component.operation} + A"
      end
    end
    
    class BehaviorBDecorator
      def initialize(component)
        @component = component
      end
    
      def operation
        "#{@component.operation} + B"
      end
    end
    
    # 動的に組み合わせ
    component = BaseComponent.new
    component_with_a = BehaviorADecorator.new(component)
    component_with_a_and_b = BehaviorBDecorator.new(component_with_a)
    
    puts component.operation # => "Base"
    puts component_with_a.operation # => "Base + A"
    puts component_with_a_and_b.operation # => "Base + A + B"
    
  • 振る舞いの追加や切り替えが柔軟になり、サブクラスが増える問題を解消できる。

3. 技術や手法の”キモ”はどこにある?

1. オブジェクトの委譲:

  • デコレータは、元のオブジェクトに機能を委譲します。
  • これにより、元のクラスの振る舞いを変更せずに拡張が可能。

2. 動的な振る舞いの追加:

  • インスタンス生成時にデコレータを適用するため、動的に機能を切り替えられる。

3. 柔軟性の向上:

  • サブクラスを増やさずに振る舞いを拡張できるため、クラス設計が簡潔になります。

4. 実装例

例: メールのフォーマット

メール送信の際、デコレータを使ってコンテンツを装飾。

class PlainTextEmail
  def content
    "This is a plain email."
  end
end

class SignatureDecorator
  def initialize(email)
    @email = email
  end

  def content
    "#{@email.content}\n\n-- Ruby App"
  end
end

# 利用例
email = SignatureDecorator.new(PlainTextEmail.new)
puts email.content # "This is a plain email.\n\n-- Ruby App"
  • 実装ポイント: メール本文の装飾が柔軟に追加可能。

例3: サービス層のロギング

デコレータを使ってログを追加。

class Service
  def execute
    "Executing service"
  end
end

class LoggingDecorator
  def initialize(service)
    @service = service
  end

  def execute
    puts "#{Time.now} Log: Service started"
    result = @service.execute
    puts "#{Time.now} Log: Service ended"
    result
  end
end

# 利用例
service = LoggingDecorator.new(Service.new)
puts service.execute
# 2025-01-13 09:19:07 +0900 Log: Service started
# 2025-01-13 09:19:07 +0900 Log: Service ended
# "Executing service"
  • 実装ポイント: サービスロジックを変更せずにログ機能を追加。

5. 議論はあるか?

メリット

  • 動的に機能を追加できる。
  • サブクラスの増加を防ぎ、コードのメンテナンス性を向上。

デメリット

  • デコレータの多重適用が増えると、トラブルシューティングが難しくなる。
  • 委譲の仕組みがコードを追いにくくする場合がある。
    • デコレータパターンでは、元のオブジェクトに機能を追加するために「委譲」を使います。
    • デコレータは、他のオブジェクト(元のオブジェクトや別のデコレータ)を保持し、そのオブジェクトに対して操作を委譲します。
    • 例: シンプルな委譲。
      • この仕組みでは、DecoratorがBaseComponentに操作を委譲しています。
      • class BaseComponent
          def operation
            "Base"
          end
        end
        
        class Decorator
          def initialize(component)
            @component = component
          end
        
          def operation
            "#{@component.operation} + Decorated"
          end
        end
        
        component = BaseComponent.new
        decorated_component = Decorator.new(component)
        puts decorated_component.operation # => "Base + Decorated"
        
    • 例: デコレータが多層化した場合。
      • この場合、decorated_componentがどのデコレータを持っていて、最終的な出力がどのように構成されているかを把握するのが難しくなります。
      • class BaseComponent
          def operation
            "Base"
          end
        end
        
        class BehaviorADecorator
          def initialize(component)
            @component = component
          end
        
          def operation
            "#{@component.operation} + A"
          end
        end
        
        class BehaviorBDecorator
          def initialize(component)
            @component = component
          end
        
          def operation
            "#{@component.operation} + B"
          end
        end
        
        # 元のオブジェクト
        component = BaseComponent.new
        
        # デコレータを適用
        decorated_component = BehaviorBDecorator.new(BehaviorADecorator.new(component))
        puts decorated_component.operation # => "Base + A + B"
        

議論

  • デコレータパターンは、「柔軟性を優先したい場合」に最適です。
  • ただし、使用箇所を明確にしないと、オーバーヘッドや可読性の低下につながる可能性があります。

6. まとめ

デコレータパターンは、既存のクラスに動的に振る舞いを追加するための強力なデザインパターンです。

その柔軟性を活かして、メンテナンス性が高く拡張可能なコードを実現しましょう。
ただし、過度な使用は可読性を損なうリスクがあるため、適切な範囲での利用を心がけることが重要です。

GitHubで編集を提案

Discussion