岩本隆史の日記帳(アーカイブ)

はてなダイアリーのサービス終了をうけて移行したものです。更新はしません。

デメテルの法則厨がライブラリを作るとどうなるか

お題:クラス名からそのクラスを得るライブラリ

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