組み込みライブラリもしくは既存のクラスclassやモジュールmoduleを, 特定の用途向けに一部だけ改変したいことがある. その際, できればモンキーパッチはしたくない.
このような場合, クラスならば
class DerivedKlass < OriginalKlass
として継承を使える. 派生クラスを作ることで, 元のクラスのコピーを作り, それを改変すればいい.
ではモジュールの場合は, どうしたらよいだろう? ここでは, Mathを取り上げて考えてみる.
最初に, include/extend/refineという既存の方法の検討をおこない問題点を明らかにする. 次に, これらの問題点を解決する
Module#module_compose
の導入を提案する.include (Module#include) を使った場合
Module#include
は, 特定のモジュール/クラスに対して, 指定したモジュールの定義 (メソッド, 定数) を受け継ぐ. 多重継承の代わりに用いられることを意図している.
これを使った場合:
module MyMath
include Math
def some_function; ...; end
end
MyMath.sin(0.5)
とすることは……できない (
NoMethodError: undefined method
sin’ for MyMath:Module`).
Mathにあった各種モジュール関数は, MyMathの外部から使うことは出来ない.
Module#prepend
を使った場合も同様である.extend (Object#extend) を使った場合
Object#extend
は, 特定のインスタンスに対して, 指定したモジュールで拡張する.module MyMath
extend Math
def some_function; ...; end
end
こうしたときも
MyMath.sin()
のような使い方はできない (NoMethodError: private method
sin’ called for MyMath:Module`): モジュール関数として用意される2つのメソッドのうちのひとつのインスタンスメソッドは, privateだからだ.refine-using (Module#refine)
refineならどうか? 目的には, 合いそうな気がするが…
module RefineMath
refine Math do
def some_function; 1; end
end
end
# =>TypeError: wrong argument type Module (expected Class)
うぅ……. そもそもrefineはクラスを扱う のだった. また仮に出来たとしてもincludeのケースと同じことになるだろう. using をしたモジュール/クラスの外側からは使えないからだ.
Module#module_compose
の導入
「なんらかのオブジェクトをモジュールで拡張したい」のではなく, 既存のモジュールそのものを拡張したい場合は, どうしたらいいだろう(モンキーパッチはしない前提で). もともとのモジュールの使い勝手を変えずに, かつそのモジュールの機能を新たなモジュールにコピーする方法はないだろうか? モジュールも, クラスのように派生できたら…….
具体的には:
- 元のモジュール (たとえばMath) のクラスメソッドは, 新モジュールのクラスメソッドへと
- 元のモジュールのインスタンスメソッドは, 新モジュールのインスタンスメソッドへと
マッピングするような仕組みがあればよい.
これらのことを実現するために, 以下に示す
Module#module_compose
を導入する:#
# module macro Module#module_compose
#
# copy module(s) into a module.
# ==== Args
# mods :: module(s)
# ==== Return
# self
# ==== Usage
# The usage is same as `include'.
#
# Ex.
# module My::Math
# module_compose Math
# module_function
# def foo; ...; end
# :
# end
#
class Module
public
def module_compose( *mods )
#
mods.each do |mod|
# store ancestors info.
include mod
# define delegation methods of mod.methods.
methods_class = mod.singleton_methods
# $stderr.puts methods_class
methods_class.each do |cmeth|
self.singleton_class.send( :define_method, cmeth ) do |*args|
mod.send( cmeth, *args )
end
end
# instance methods (public/protected/private) of mod can be used
# because we did `include mode`.
#
end
self
end
end
#
処理の中身としては, 新モジュール内で, 拡張の対象となる元のモジュールをincludeをし, 元のモジュールのクラスメソッドへのチェーンを定義する. だけだ.
以下のように使う. 元のモジュールと同じ感覚で使えるモジュールMyMathを作れるのだ:
require 'module_compose'
# => true
module MyMath
module_compose Math
def my_method; 1; end
end
# => :some_method
MyMath.sin(0.5)
# => 0.479425538604203
include MyMath
# => Object
sin(0.5)
# => 0.479425538604203
my_method
# => 1
Mathモジュールから派生(module_compose)したMyMathで, 継承元のクラスメソッドもインスタンスメソッドも使える.
まとめ
- Rubyでモジュールを継承/派生させる試みとして, 新たに
Module#module_compose
を導入した Module#module_compose
により, モンキーパッチなしで既存モジュールを変更することが可能となった
今後の課題など
- 変数については考慮していない
forwardable
,delegate
の検討は行っていない- Rubyの想定するモジュールの使用法から逸脱している可能性はある. MatzによるMix-inの解説, 2005. 一般に, クラスメソッドやインスタンスメソッドの定義の自動化には, ClassMethods, InstanceMethodsという名のサブモジュールを用意しておき, フックメソッドを使ってextendやincludeする方式(
self.included(base)
イディオム)が, よく使われている
0 件のコメント:
コメントを投稿
何かありましたら、どうぞ: