zackey推し

IT系のこと書くぞい

【Ruby on Rails】デメテルの法則 と Moduleのinclude / forwardable / delegate を用いたメソッドの委譲について咀嚼した考えまとめ

f:id:kic-yuuki:20190818105606p:plain

現在、会社からオブジェクト指向設計実践ガイドを借りて読んでます。

オブジェクト指向設計実践ガイドを自分なりにざっくり説明すると、

オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方

AmazonでSandi Metz, 髙山 泰基のオブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方。アマゾンならポイント還元本が多数。Sandi Metz, 髙山 泰基作品ほか、お急ぎ便対象商品は当日お届けも可能。またオブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方もアマゾン配送商品なら通常配送無料。

「第4章 - 柔軟なインターフェースをつくる」という章で、デルメルの法則というキーワードが出てきました。

「耳にしたことあるけどなんだっけ?」という状態かつ、この言葉にまつわるメソッドの委譲の話が非常に参考になったため、学習&咀嚼した知識をこの記事にメモしておきたいと思います。

デメテルの法則ってなんぞ? 🤔

Wikiに書いてありました。

解釈したイメージ

オブジェクト指向における適用から、ざっくり以下のように解釈しました。

  • そのクラスが保持しているインスタンスのメンバーやメソッドにアクセスすることはNG
    • 極論ドット(.)は1つまで
      • OK: instance.child
      • NG: instance.child.child

コードに落とし込むと以下のようなイメージです。

class DemeterTest
  attr_reader :child, :value
  def initialize(args)
    @child = args[:child]
    @value = args[:value]
  end

  def print
    puts value
  end

  def child_print
    child&.print
  end
end

test = DemeterTest.new(
  value: 'test1',
  child: DemeterTest.new(value: 'test2')
)

# OK
test.print
test.child_print

# NG
test.child.print

参考になった記事

実際のコードで説明されている、以下の記事が参考になりました。

Moduleのincludeによって考えられるデメリット

先ほど示した解釈したイメージのコードでは、ラッパーしたメソッド(child_print)を実装することによって、デメテルの法則を適用することができました。

しかし、DemeterTestクラスに機能が追加されたいった場合、たくさんのラッパーメソッドを実装しなくてはいけなくなり、冗長なコードになってしまいます。

そうなった場合、

  • 親クラスを定義、継承する
  • Moduleに処理を切り出して部分的にincludeする

といった対処方法が考えられますが、

class DemeterTest
  include SubModule1
  include SubModule2
  include SubModule3
  # 他たくさんのincludeが...
end

と、いずれ、上記のように大量にincludeする未来が伺えます。
(その前にクラス自体分けたら?という理論もあると思いますがそこはおいといて...)

大量のincludeが発生すると、
呼び出しているメソッドがどのModuleで実装されているか、一目でわからなくなるというデメリットが生じます。

解決方法

呼び出しているメソッドがどのModuleで実装されているか、一目でわからなくなる
を解決する方法として、メソッドの委譲があります。

Ruby / Railsそれぞれでデフォルトで提供されている委譲方法があったため、それらを記載していきます。

forwardable

forwardableは、Ruby自体に実装されているライブラリです。

docs.ruby-lang.org

以下のように、Alias(別名)のメソッド(DemeterTestchild_print)を定義、委譲先のインスタンスchild)とメソッド(childインスタンスprint)を指定するようなイメージで実装します。

(細かい内容は上記リファレンスをご参照ください)

require 'forwardable'

class DemeterTest
  extend Forwardable

  attr_reader :child
  def initialize(args)
    @child = args[:child]
    @value = args[:value]
  end

  def print
    puts @value
  end

  def_delegator :child, :print, :child_print
  # def child_print
  #   child&.print
  # end
end

test = DemeterTest.new(
  value: 'test1',
  child: DemeterTest.new(value: 'test2')
)

# OK def_delegatorで委譲
test.child_print

delegate

delegateは、RailsのActive Supportに実装されている機能の1つです。

railsguides.jp

以下のように、特定のメソッド(DemeterTestインスタンスchild_print)が呼ばれたら、移譲先のインスタンスのメソッド(@child.print)を呼び出すイメージで実装します。

(細かい内容は上記リファレンスをご参照ください)

require 'active_support'

class DemeterTest
  attr_reader :value
  delegate :child_print, to: :@child

  def initialize(args)
    @child = args[:child]
    @value = args[:value]
  end

  def print
    puts @value
  end
end

# Child
class Child
  def initialize(args)
    @value = args[:value]
  end

  def child_print
    puts @value
  end
end

test = DemeterTest.new(
  value: 'test1',
  child: Child.new(value: 'child1')
)

# OK delegateで委譲
test.child_print

所感

Moduleのincludeで処理切り出しはお手軽ですが、その分コードが追いにくくなります。

forwardabledelegateを使うと、移譲先が定義されているため、どのメソッドを呼び出しているかわかりやすくなり、コードが追いやすくなります。

その分、委譲の定義をしないといけないというデメリットは発生しますが、誰でもメンテしやすいコードを保つ必要があるという状況だったら、forwardabledelegateを使った方が良いと感じました。

おわり

一読ありがとうございました、何かありましたらフィードバックいただけると助かります!🙏

オブジェクト指向設計実践ガイド、読み出すと面白くて止まらない、いいですね。(本を開けるまでがハードル)

こちらの記事も参考になりましたので、合わせてお読みください。