Rubyのdelegateとは?委譲の基本から応用、使い方を徹底解説
Rubyにおけるdelegate
は、オブジェクト指向プログラミングの重要な概念である「委譲」を実現するための強力なツールです。委譲とは、あるオブジェクトが別のオブジェクトに処理を依頼することを指します。delegate
を使用することで、コードの再利用性、可読性、保守性を向上させることができます。
この記事では、delegate
の基本的な概念から、具体的な使い方、そして応用的なテクニックまでを網羅的に解説します。
目次
-
委譲(Delegation)とは?
- 委譲の定義とメリット
- 継承との違い
- 委譲の適用例
-
Rubyの
delegate
ライブラリdelegate
ライブラリの概要require 'delegate'
Delegator
クラスとSimpleDelegator
クラスforwardable
ライブラリとの比較
-
Delegator
クラスの使い方Delegator
クラスの基本構造_get_obj()
メソッドと委譲先のオブジェクトDelegator
クラスの継承- メソッドのオーバーライド
method_missing
とrespond_to?
-
SimpleDelegator
クラスの使い方SimpleDelegator
クラスの基本構造__getobj__()
メソッドと委譲先のオブジェクト- 委譲先のオブジェクトの変更
method_missing
とrespond_to?
-
delegate
マクロ (gem ‘delegate’) の使い方delegate
マクロの概要delegate
マクロの基本的な使い方- オプションとカスタマイズ
:to
オプション:prefix
オプション:allow_nil
オプション:private
オプション:as
オプション
- ブロックを使ったカスタマイズ
-
委譲の応用的なテクニック
- 複数のオブジェクトへの委譲
- 動的な委譲先の変更
- 委譲とデザインパターン(Facade, Adapter)
- パフォーマンスを考慮した委譲の実装
-
委譲を使う上での注意点
- 委譲の濫用を避ける
- 委譲先のオブジェクトのライフサイクル
- デバッグの難しさ
-
まとめ
1. 委譲(Delegation)とは?
委譲の定義とメリット
委譲とは、オブジェクト指向プログラミングにおけるデザインパターンの一つで、あるオブジェクトが別のオブジェクトに特定の処理の実行を依頼することを意味します。つまり、オブジェクトは、直接処理を行うのではなく、他のオブジェクトにその責任を「委譲」します。
委譲の主なメリットは以下の通りです。
- コードの再利用性: 既存のオブジェクトの機能を再利用できます。新しい機能を実装する代わりに、既存のオブジェクトに処理を委譲することで、コードの重複を減らし、DRY (Don’t Repeat Yourself) 原則を遵守できます。
- 疎結合: オブジェクト間の依存関係を弱めることができます。委譲を行うオブジェクトは、委譲先のオブジェクトの具体的な実装を知る必要がありません。インターフェースを通じて疎結合に連携できます。
- 単一責任原則: 各オブジェクトが特定の責任を持つように設計できます。オブジェクトの責務を明確に分離することで、コードの理解、変更、テストが容易になります。
- 柔軟性: 委譲先のオブジェクトを動的に変更することで、実行時の振る舞いを柔軟に変更できます。
- 保守性: コードが整理され、各オブジェクトの責務が明確になるため、保守が容易になります。
継承との違い
委譲とよく比較されるのが継承です。どちらもコードの再利用を目的としたものですが、そのアプローチは大きく異なります。
- 継承: あるクラスが別のクラスの特性(属性とメソッド)を受け継ぎ、その特性を拡張または変更します。is-aの関係を表すのに適しています。(例:犬は動物である)
- 委譲: あるオブジェクトが別のオブジェクトに処理を依頼します。has-aの関係を表すのに適しています。(例:車はエンジンを持っている)
特徴 | 継承 | 委譲 |
---|---|---|
関係性 | is-a | has-a |
結合度 | 強い | 弱い |
柔軟性 | 低い(実行時に変更が難しい) | 高い(実行時に委譲先を変更可能) |
コードの再利用 | 親クラスのコードを再利用 | 委譲先のオブジェクトの機能を再利用 |
ポリモーフィズム | サブクラスは親クラスの型として扱える | インターフェースによってポリモーフィズムを実現 |
継承は、コードの再利用には強力な手段ですが、クラス間の結合度が高くなり、柔軟性が低いというデメリットがあります。また、継承の濫用は、クラス階層が複雑化し、保守性を損なう原因となります。
一方、委譲は、オブジェクト間の結合度が低く、柔軟性が高いというメリットがあります。委譲先のオブジェクトを動的に変更することで、実行時の振る舞いを柔軟に変更できます。
委譲の適用例
委譲は、さまざまな場面で活用できます。以下にいくつかの例を示します。
- Facadeパターン: 複雑なサブシステムのインターフェースを簡素化するために、Facadeオブジェクトを作成し、実際の処理をサブシステムのオブジェクトに委譲します。
- Adapterパターン: 互換性のないインターフェースを持つオブジェクトを連携させるために、Adapterオブジェクトを作成し、クライアントからのリクエストを委譲先のオブジェクトが理解できる形式に変換します。
- 状態パターン: オブジェクトの状態に応じて振る舞いを変更するために、状態オブジェクトを作成し、オブジェクトの状態に応じて処理を委譲する状態オブジェクトを切り替えます。
- Decoratorパターン: 既存のオブジェクトに新しい機能を追加するために、Decoratorオブジェクトを作成し、基本機能を委譲先のオブジェクトに実行させ、追加機能をDecoratorオブジェクト自身が実行します。
2. Rubyのdelegate
ライブラリ
delegate
ライブラリの概要
Rubyには、委譲を容易にするためのdelegate
ライブラリが標準で付属しています。delegate
ライブラリは、Delegator
クラスとSimpleDelegator
クラスを提供し、これらを活用することで、簡単に委譲を実装できます。
require 'delegate'
delegate
ライブラリを使用するには、最初にrequire 'delegate'
を記述する必要があります。これにより、Delegator
クラスとSimpleDelegator
クラスが利用可能になります。
ruby
require 'delegate'
Delegator
クラスとSimpleDelegator
クラス
delegate
ライブラリには、主にDelegator
クラスとSimpleDelegator
クラスの2つのクラスが用意されています。
-
Delegator
クラス: より柔軟な委譲を実現するためのクラスです。Delegator
クラスを継承し、_get_obj()
メソッドをオーバーライドすることで、委譲先のオブジェクトを指定します。また、method_missing
メソッドをオーバーライドすることで、委譲先のオブジェクトに存在しないメソッドが呼び出された場合の処理をカスタマイズできます。 -
SimpleDelegator
クラス: よりシンプルな委譲を実現するためのクラスです。SimpleDelegator
クラスは、コンストラクタで委譲先のオブジェクトを受け取り、すべてのメソッド呼び出しをそのオブジェクトに委譲します。委譲先のオブジェクトを動的に変更することも可能です。
forwardable
ライブラリとの比較
forwardable
ライブラリも、委譲を実現するためのライブラリですが、delegate
ライブラリとは異なるアプローチを採用しています。
delegate
ライブラリ:Delegator
クラスまたはSimpleDelegator
クラスを継承し、委譲の仕組みを自分で制御します。forwardable
ライブラリ:Forwardable
モジュールをincludeし、delegate
マクロを使用して委譲を設定します。
forwardable
ライブラリは、より宣言的な方法で委譲を定義できますが、カスタマイズ性はdelegate
ライブラリの方が高いです。
3. Delegator
クラスの使い方
Delegator
クラスの基本構造
Delegator
クラスは、委譲の基底クラスとして機能します。Delegator
クラスを継承したクラスは、_get_obj()
メソッドをオーバーライドすることで、委譲先のオブジェクトを指定する必要があります。
“`ruby
require ‘delegate’
class MyDelegator < Delegator
def initialize(obj)
super(obj) # Delegatorクラスのinitializeメソッドを呼び出す必要がある
end
def _get_obj()
# 委譲先のオブジェクトを返す
# @delegated_objectなどのインスタンス変数を返すことが多い
@delegated_object
end
private
def delegated_object=(obj)
@delegated_object = obj
end
end
“`
initialize
メソッドでは、super(obj)
を呼び出す必要があります。これは、Delegator
クラスのinitialize
メソッドが、委譲先のオブジェクトを内部的に管理するために必要な処理を行っているためです。
_get_obj()
メソッドは、委譲先のオブジェクトを返す役割を持ちます。通常、インスタンス変数などを返します。
_get_obj()
メソッドと委譲先のオブジェクト
_get_obj()
メソッドは、Delegator
クラスの中核となるメソッドです。このメソッドが返すオブジェクトに対して、メソッド呼び出しが委譲されます。
_get_obj()
メソッドは、protectedメソッドとして定義されているため、Delegator
クラスを継承したクラス内からのみアクセスできます。
Delegator
クラスの継承
Delegator
クラスを継承することで、委譲の振る舞いをカスタマイズできます。例えば、特定のメソッドの呼び出しをインターセプトしたり、委譲先のオブジェクトに存在しないメソッドが呼び出された場合の処理を定義したりできます。
“`ruby
require ‘delegate’
class UppercaseString < Delegator
def initialize(str)
super(str)
end
def upcase
_get_obj().upcase # 明示的に委譲先オブジェクトのメソッドを呼び出す
end
def to_s
_get_obj().to_s # 明示的に委譲先オブジェクトのメソッドを呼び出す
end
def inspect
“UppercaseString: #{_get_obj().inspect}”
end
private
def _get_obj()
@delegate_sd_obj
end
end
str = UppercaseString.new(“hello”)
puts str.upcase #=> HELLO
puts str.to_s #=> hello
puts str.inspect #=> UppercaseString: “hello”
“`
メソッドのオーバーライド
Delegator
クラスを継承したクラスでは、委譲先のオブジェクトのメソッドをオーバーライドすることができます。オーバーライドされたメソッドは、委譲先のオブジェクトではなく、Delegator
クラスを継承したクラス自身で処理されます。
“`ruby
require ‘delegate’
class MyArray < Delegator
def initialize(array)
super(array)
end
def push(element)
puts “要素を追加します: #{element}”
_get_obj().push(element) # 委譲先オブジェクトのpushメソッドを呼び出す
end
private
def _get_obj()
@delegate_sd_obj
end
end
arr = MyArray.new([1, 2, 3])
arr.push(4) #=> 要素を追加します: 4
puts arr #=> [1, 2, 3, 4]
“`
method_missing
とrespond_to?
Delegator
クラスでは、委譲先のオブジェクトに存在しないメソッドが呼び出された場合、method_missing
メソッドが呼び出されます。method_missing
メソッドをオーバーライドすることで、そのような場合の処理をカスタマイズできます。
また、respond_to?
メソッドもオーバーライドすることで、委譲先のオブジェクトが特定のメソッドに応答できるかどうかを制御できます。
“`ruby
require ‘delegate’
class MyDelegator < Delegator
def initialize(obj)
super(obj)
end
def method_missing(method_name, *args, &block)
puts “メソッドが見つかりません: #{method_name}”
super # 親クラスのmethod_missingを呼び出す(例外を発生させる)
end
def respond_to_missing?(method_name, include_private = false)
# 委譲先オブジェクトに存在するかどうかを返す
_get_obj().respond_to?(method_name, include_private) || super
end
private
def _get_obj()
@delegate_sd_obj
end
end
obj = MyDelegator.new(“hello”)
obj.non_existent_method #=> メソッドが見つかりません: non_existent_method
NoMethodError: undefined method `non_existent_method’ for “hello”:String
puts obj.respond_to?(:upcase) #=> true
puts obj.respond_to?(:non_existent_method) #=> false
“`
4. SimpleDelegator
クラスの使い方
SimpleDelegator
クラスの基本構造
SimpleDelegator
クラスは、よりシンプルに委譲を実現するためのクラスです。SimpleDelegator
クラスは、コンストラクタで委譲先のオブジェクトを受け取り、すべてのメソッド呼び出しをそのオブジェクトに委譲します。
“`ruby
require ‘delegate’
class MySimpleDelegator < SimpleDelegator
def initialize(obj)
super(obj)
end
end
obj = MySimpleDelegator.new(“hello”)
puts obj.upcase #=> HELLO
“`
SimpleDelegator
クラスは、Delegator
クラスのように_get_obj()
メソッドをオーバーライドする必要はありません。委譲先のオブジェクトは、コンストラクタで渡されたオブジェクトが使用されます。
__getobj__()
メソッドと委譲先のオブジェクト
SimpleDelegator
クラスでは、__getobj__()
メソッドを使用して、委譲先のオブジェクトを取得できます。このメソッドは、protectedメソッドとして定義されているため、SimpleDelegator
クラスを継承したクラス内からのみアクセスできます。
委譲先のオブジェクトの変更
SimpleDelegator
クラスでは、__setobj__(obj)
メソッドを使用して、委譲先のオブジェクトを動的に変更できます。
“`ruby
require ‘delegate’
class MySimpleDelegator < SimpleDelegator
def initialize(obj)
super(obj)
end
def change_object(obj)
setobj(obj)
end
end
str1 = “hello”
str2 = “world”
obj = MySimpleDelegator.new(str1)
puts obj.upcase #=> HELLO
obj.change_object(str2)
puts obj.upcase #=> WORLD
“`
method_missing
とrespond_to?
SimpleDelegator
クラスでも、Delegator
クラスと同様に、委譲先のオブジェクトに存在しないメソッドが呼び出された場合、method_missing
メソッドが呼び出されます。また、respond_to?
メソッドもオーバーライドすることで、委譲先のオブジェクトが特定のメソッドに応答できるかどうかを制御できます。
“`ruby
require ‘delegate’
class MySimpleDelegator < SimpleDelegator
def initialize(obj)
super(obj)
end
def method_missing(method_name, *args, &block)
puts “メソッドが見つかりません: #{method_name}”
super # 親クラスのmethod_missingを呼び出す(例外を発生させる)
end
def respond_to_missing?(method_name, include_private = false)
# 委譲先オブジェクトに存在するかどうかを返す
getobj().respond_to?(method_name, include_private) || super
end
end
obj = MySimpleDelegator.new(“hello”)
obj.non_existent_method #=> メソッドが見つかりません: non_existent_method
NoMethodError: undefined method `non_existent_method’ for “hello”:String
puts obj.respond_to?(:upcase) #=> true
puts obj.respond_to?(:non_existent_method) #=> false
“`
5. delegate
マクロ (gem ‘delegate’) の使い方
delegate
マクロの概要
delegate
マクロは、delegate
gemによって提供される機能で、クラス内で簡単にメソッドの委譲を定義できます。 delegate
マクロを使用すると、Delegator
クラスやSimpleDelegator
クラスを直接使用するよりも、簡潔で読みやすいコードを記述できます。
まず、delegate
gemをインストールする必要があります。
bash
gem install delegate
そして、以下のようにrequire
します。
ruby
require 'delegate' # Delegate gemを明示的に require しなくても動作する場合もあります。
delegate
マクロの基本的な使い方
delegate
マクロの基本的な使い方は以下の通りです。
“`ruby
require ‘delegate’
class User
attr_accessor :profile
end
class Profile
attr_accessor :name, :age
end
class UserPresenter
extend Forwardable # delegateマクロを使うために必要
def initialize(user)
@user = user
end
delegate :name, :age, to: :profile
end
user = User.new
user.profile = Profile.new
user.profile.name = “Taro”
user.profile.age = 20
presenter = UserPresenter.new(user)
puts presenter.name #=> Taro
puts presenter.age #=> 20
“`
この例では、UserPresenter
クラスが、User
オブジェクトのprofile
属性にアクセスし、profile
オブジェクトのname
とage
属性を委譲しています。
オプションとカスタマイズ
delegate
マクロには、委譲の振る舞いをカスタマイズするためのさまざまなオプションが用意されています。
:to
オプション
:to
オプションは、委譲先のオブジェクトを指定するために使用します。委譲先のオブジェクトは、シンボル、文字列、またはProcオブジェクトとして指定できます。
“`ruby
require ‘delegate’
class Order
attr_accessor :customer
def initialize(customer)
@customer = customer
end
end
class Customer
attr_accessor :name
end
class OrderPresenter
extend Forwardable
def initialize(order)
@order = order
end
delegate :name, to: :customer # customerオブジェクトのnameを委譲
end
customer = Customer.new
customer.name = “Hanako”
order = Order.new(customer)
presenter = OrderPresenter.new(order)
puts presenter.name #=> Hanako
“`
:prefix
オプション
:prefix
オプションは、委譲されたメソッドにプレフィックスを追加するために使用します。これにより、メソッド名が衝突するのを防ぎ、コードの可読性を向上させることができます。
“`ruby
require ‘delegate’
class Article
attr_accessor :title, :body
end
class Comment
attr_accessor :body
end
class CommentPresenter
extend Forwardable
def initialize(comment)
@comment = comment
end
delegate :body, to: :@comment, prefix: :comment # comment_bodyメソッドが定義される
end
article = Article.new
article.title = “Ruby Delegate”
article.body = “Delegate is powerful tool.”
comment = Comment.new
comment.body = “Great article!”
presenter = CommentPresenter.new(comment)
puts presenter.comment_body #=> Great article!
“`
:prefix
オプションにtrue
を指定すると、プレフィックスとしてクラス名(小文字)が使用されます。
“`ruby
require ‘delegate’
class CommentPresenter
extend Forwardable
def initialize(comment)
@comment = comment
end
delegate :body, to: :@comment, prefix: true # comment_bodyメソッドが定義される
end
“`
:allow_nil
オプション
:allow_nil
オプションは、委譲先のオブジェクトがnil
の場合でも、例外が発生しないようにするために使用します。allow_nil: true
を指定すると、委譲先のオブジェクトがnil
の場合、nil
が返されます。
“`ruby
require ‘delegate’
class User
attr_accessor :profile
end
class UserPresenter
extend Forwardable
def initialize(user)
@user = user
end
delegate :name, to: :profile, allow_nil: true # profileがnilでも例外が発生しない
end
user = User.new
presenter = UserPresenter.new(user) # user.profileはnil
puts presenter.name #=> nil
“`
:private
オプション
:private
オプションは、委譲されたメソッドをprivateメソッドとして定義するために使用します。
“`ruby
require ‘delegate’
class MyClass
extend Forwardable
def initialize(obj)
@obj = obj
end
delegate :my_method, to: :@obj, private: true
end
my_methodはprivateメソッドとして定義される
“`
:as
オプション
:as
オプションは、委譲されたメソッドの名前を変更するために使用します。
“`ruby
require ‘delegate’
class MyClass
extend Forwardable
def initialize(obj)
@obj = obj
end
delegate :original_method, to: :@obj, as: :new_method
end
original_methodはnew_methodという名前で委譲される
“`
ブロックを使ったカスタマイズ
delegate
マクロでは、ブロックを使用して、委譲されたメソッドの処理をカスタマイズすることもできます。
“`ruby
require ‘delegate’
class MyClass
extend Forwardable
def initialize(obj)
@obj = obj
end
delegate :calculate, to: :@obj do |result|
“Result: #{result}”
end
end
calculateメソッドの処理をブロックでカスタマイズ
“`
6. 委譲の応用的なテクニック
複数のオブジェクトへの委譲
delegate
は、複数のオブジェクトに委譲することも可能です。
“`ruby
require ‘delegate’
class MyClass
extend Forwardable
def initialize(obj1, obj2)
@obj1 = obj1
@obj2 = obj2
end
delegate :method1, to: :@obj1
delegate :method2, to: :@obj2
end
“`
動的な委譲先の変更
SimpleDelegator
クラスを使用すると、委譲先のオブジェクトを動的に変更できます。これは、オブジェクトの状態に応じて振る舞いを変更したい場合に役立ちます。
“`ruby
require ‘delegate’
class MyClass < SimpleDelegator
def initialize(obj)
super(obj)
end
def change_object(obj)
setobj(obj)
end
end
オブジェクトの状態に応じて委譲先を変更する
“`
委譲とデザインパターン(Facade, Adapter)
委譲は、さまざまなデザインパターンを実装するために活用できます。
- Facadeパターン: 複雑なサブシステムのインターフェースを簡素化するために、Facadeオブジェクトを作成し、実際の処理をサブシステムのオブジェクトに委譲します。
- Adapterパターン: 互換性のないインターフェースを持つオブジェクトを連携させるために、Adapterオブジェクトを作成し、クライアントからのリクエストを委譲先のオブジェクトが理解できる形式に変換します。
パフォーマンスを考慮した委譲の実装
委譲は、メソッド呼び出しを転送するため、パフォーマンスに影響を与える可能性があります。特に、頻繁に呼び出されるメソッドを委譲する場合は、パフォーマンスを考慮した実装が必要です。
- キャッシュ: 委譲先のオブジェクトをキャッシュすることで、メソッド呼び出しのオーバーヘッドを削減できます。
- インライン化: 短いメソッドを委譲する場合、メソッド呼び出しをインライン化することで、オーバーヘッドを削減できます。
7. 委譲を使う上での注意点
委譲の濫用を避ける
委譲は、コードの再利用性、可読性、保守性を向上させる強力なツールですが、濫用すると、コードが複雑化し、理解しにくくなる可能性があります。委譲は、必要な場合にのみ使用し、オブジェクトの責務を明確に分離するように心がけましょう。
委譲先のオブジェクトのライフサイクル
委譲先のオブジェクトのライフサイクルに注意する必要があります。委譲元のオブジェクトが、委譲先のオブジェクトよりも長く生存する場合、委譲先のオブジェクトが無効になった時点で、委譲元のオブジェクトがエラーを引き起こす可能性があります。
デバッグの難しさ
委譲を使用すると、メソッド呼び出しのチェーンが長くなるため、デバッグが難しくなる可能性があります。デバッガを使用したり、ログを出力したりすることで、問題を特定しやすくすることができます。
8. まとめ
この記事では、Rubyにおけるdelegate
の基本的な概念から、具体的な使い方、そして応用的なテクニックまでを網羅的に解説しました。delegate
は、オブジェクト指向プログラミングにおいて、コードの再利用性、可読性、保守性を向上させるための強力なツールです。
Delegator
クラス、SimpleDelegator
クラス、そしてdelegate
マクロを適切に使いこなすことで、より洗練されたRubyコードを書くことができるでしょう。委譲を理解し、積極的に活用することで、より優れたソフトウェア開発を目指しましょう。