デメテルの法則厨がライブラリを作るとどうなるか
お題:クラス名からそのクラスを得るライブラリ
Webアプリのフレームワークを作っていて、クラス名からそのクラスのインスタンスを生成したくなりました。リクエストURIによって呼び出したいコントローラが変わるためです。
そこで、下記のような ClassGetter ライブラリがあると便利だと思いました。
class_name = "MyFramework::Controller::#{controller_name}" controller_class = ClassGetter.new.get(class_name) controller_class.new.run
普通に実装してみる
Rubyには、クラス名からインスタンスを得るためのイディオムがあります。
c = classname.split(/::/).inject(Object) {|c,name| c.const_get(name) } c.new
これを私好みのコーディングスタイルでクラス化すると:
class ClassGetter def get(class_name) class_name.split('::').inject(::Object) do |klass, const| klass.const_get(const) end end end
となります。はい、ライブラリ完成。普通はこれでおしまいですね。
必要ないオブジェクトを受け取らざるをえなければ「ずるい方法」を使う
さて、そうはいかないのがデメテルの法則厨です。法則違反が気になってしょうがない。お察しのとおり、ここから病的になります。
まず気になるのが、class_name を split してできた配列のメソッドを呼んでいるところです。前回の記事に書いたように「必要のないオブジェクトは受け取るな」がデメテル厨の金科玉条です。したがって、本来は split 後の配列を受け取るのが理想的なのですが、それではライブラリのインターフェースが変わってしまいます。ちょっと使いづらい。
そこで、前回の「ずるい方法」登場です。まず、ClassName クラスをでっちあげてしまいましょう。
class ClassGetter def get(class_name) class_name_parts = ClassName.new(class_name).split class_name_parts.inject(::Object) do |klass, const| klass.const_get(const) end end end class ClassName def initialize(class_name) @class_name = class_name end def split @class_name.split('::') end end
配列を受け取るメソッドをでっちあげる
これだけでは解決にはなりません。結局、配列(class_name_parts)のメソッド(inject)を呼び出しているからです。そこで、配列を受け取るメソッドをでっちあげます。
class ClassGetter def get(class_name) class_name_parts = ClassName.new(class_name).split self.get_by_parts(class_name_parts) end def get_by_parts(class_name_parts) class_name_parts.inject(::Object) do |klass, const| klass.const_get(const) end end end # ClassNameクラスの定義は省略
はい、これで解決。get メソッドの法則違反がなくなりました。なくなりました。
ブロック内でも「ずるい方法」を使う
でもですよ、get_by_parts メソッドの中で klass のメソッドを呼ぶのは法則違反にはならないのでしょうか。klass が無名関数の引数だと思えば違反ではないのかもしれませんが、ClassGetter#get_by_parts にとって、klass は直接の知り合いとは言いがたいですよね。
ならば、こちらも「ずるい方法」で解決してしまいましょう。Klass クラスをでっちあげます。
class ClassGetter def get(class_name) class_name_parts = ClassName.new(class_name).split self.get_by_parts(class_name_parts) end def get_by_parts(class_name_parts) class_name_parts.inject(::Object) do |klass, const| Klass.new(klass).get_constant(const) end end end class Klass def initialize(klass) @klass = klass end def get_constant(const) @klass.const_get(const) end end # ClassNameクラスの定義は省略
これでもう大丈夫でしょう。真似するかどうかはあなた次第です。私は結構いけてると思っています(病気なので)。
より病的な別解
ちなみに、より病的な別解もあります。DIを使うあたりに狂気すら感じられて、好感がもてます。
class ClassGetter def initializer(class_name_splitter, constant_getter) @class_name_splitter = class_name_splitter @constant_getter = constant_getter end def get(class_name) class_name_parts = @class_name_splitter.split(class_name) self.get_by_parts(class_name_parts) end def get_by_parts(class_name_parts) class_name_parts.inject(::Object) do |klass, const| @constant_getter.get(klass, const) end end end class ClassNameSplitter def split(class_name) class_name.split('::') end end class ConstantGetter def get(klass, const) klass.const_get(const) end end class_name = "MyFramework::Controller::#{controller_name}" class_getter = ClassGetter.new(ClassNameSplitter.new, ConstantGetter.new) controller_class = class_getter.get(class_name) controller_class.new.run