現在、会社からオブジェクト指向設計実践ガイドを借りて読んでます。
オブジェクト指向設計実践ガイドを自分なりにざっくり説明すると、
- Practical Object-Oriented Design in Rubyという本の翻訳版
- Ruby on Railsにおけるオブジェクト指向設計を「なんとなく」で学んでいた知識を補完できる
- 細かい魅力はこちらをチェック
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方
AmazonでSandi Metz, 髙山 泰基のオブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方。アマゾンならポイント還元本が多数。Sandi Metz, 髙山 泰基作品ほか、お急ぎ便対象商品は当日お届けも可能。またオブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方もアマゾン配送商品なら通常配送無料。
「第4章 - 柔軟なインターフェースをつくる」という章で、デルメルの法則というキーワードが出てきました。
「耳にしたことあるけどなんだっけ?」という状態かつ、この言葉にまつわるメソッドの委譲の話が非常に参考になったため、学習&咀嚼した知識をこの記事にメモしておきたいと思います。
デメテルの法則ってなんぞ? 🤔
Wikiに書いてありました。
解釈したイメージ
オブジェクト指向における適用から、ざっくり以下のように解釈しました。
- そのクラスが保持しているインスタンスのメンバーやメソッドにアクセスすることはNG
- 極論ドット(.)は1つまで
- OK:
instance.child
- NG:
instance.child.child
- OK:
- 極論ドット(.)は1つまで
コードに落とし込むと以下のようなイメージです。
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自体に実装されているライブラリです。
以下のように、Alias(別名)のメソッド(DemeterTest
のchild_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つです。
以下のように、特定のメソッド(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で処理切り出しはお手軽ですが、その分コードが追いにくくなります。
forwardable
やdelegate
を使うと、移譲先が定義されているため、どのメソッドを呼び出しているかわかりやすくなり、コードが追いやすくなります。
その分、委譲の定義をしないといけないというデメリットは発生しますが、誰でもメンテしやすいコードを保つ必要があるという状況だったら、forwardable
やdelegate
を使った方が良いと感じました。
おわり
一読ありがとうございました、何かありましたらフィードバックいただけると助かります!🙏
オブジェクト指向設計実践ガイド、読み出すと面白くて止まらない、いいですね。(本を開けるまでがハードル)
こちらの記事も参考になりましたので、合わせてお読みください。