💎

デメテルの法則を根拠にdelegateを乱用するのは間違い

2023/07/30に公開1

ではないか

Ruby、Railsにおけるdelegate

とあるclassがあって全ての要件を一つに詰めるとclassが肥大化しすぎるので、分割したい時があります。Railsのおいてモデルのレイヤーならテーブル設計の際に正規化してテーブルを分割することもありますし、concerns、serviceなどに切り出すこともあるでしょう。

委譲はそういった切り分けのパターンの一つでRubyのdefault gemならdelegateやforwardableなどがあります。ただRubyは動的に色々書き換えられるのが言語的特徴なので、静的型付け言語で見られるようなかっちりとした委譲はあまり使われない印象があります。 自身をカジュアルに書き換えてしまうことの方が多いかなと。

default gemの委譲よりRails開発で使われることが多いものにActive Supportのdelegateがあります。このdelegate、使った方がいい根拠としてデメテルの法則があげられることがあるのですが、乱用しすぎて解読困難に陥ったコードに遭遇したことがあり、使わない方がいいのではと思う時も個人的には多いです。

デメテルの法則が言いたかったことは何なのか、Active Supportのdelegateはそのパターンに当てはまっているのか、それを踏まえて思考停止でdelegateを使うことが正しいのかについて論じてみたいと思います。

Active Supportのdelegateとは

Active Supportのdelegateの簡単な使用例は以下です。通常なら @category.blog_article.title と書かねばいけないところをより短く @category.title と書くことができます。

class BlogArticle < ApplicationRecord
  def title
    'awesome blog title'
  end
end

class BlogArticleCategory < ApplicationRecord
  belongs_to :blog_article
  delegate :title, to: :blog_article
end

@category = BlogArticleCategory.find(1)
@category.title

公式レファレンスやWebの記事ではActive Recordの使用例が多いですが、それ以外にも使えます。Discourseのコードベースだと、libやserializerにも使われていますね。

## 公式docより
class Foo
  CONSTANT_ARRAY = [0,1,2,3]
  @@class_array  = [4,5,6,7]

  def initialize
    @instance_array = [8,9,10,11]
  end
  delegate :sum, to: :CONSTANT_ARRAY
  delegate :min, to: :@@class_array
  delegate :max, to: :@instance_array
end

Foo.new.sum # => 6
Foo.new.min # => 4
Foo.new.max # => 11

このdelegateを使った方がいい根拠としてデメテルの法則がしばしば紹介されます。本当でしょうか?

デメテルの法則とは

デメテルの法則は1987年にIan Holland氏によって提案された理論です。デメテルさんが提唱したからデメテルの法則ではないです。アスペクト指向プログラミングを研究していたデメテルプロジェクトに由来。

当時としてはおそらく革新的だったアジャイルに近い思想で、ソフトウェアはビルドするものではなく育てるものとして豊穣の女神の名をとったようです。

デメテルの法則は簡単にOnly talk to your friends、最も近しい友とのみ会話せよ であらわせます。オブジェクト指向の言語だとドットで関連概念を辿っていくことが多いですが、use only one dot、使えるドットは一つまで がルール。

簡単にa.b().c()はルール違反でa.b()はOK。aからbを辿ってcにアクセスするのではなく、Aを呼ぶ側からは内部の実装は(ラッパーメソッドを作るなどして)隠蔽するようにすべしということです。

一見するとActive Supportのdelegateはショートカットを作ることでルールを満たしているように見えます。

デメテルの法則が言いたかったことは何か

理解を深めるために元論文を読んでみます。Wikipeidaを信じるなら1987年が提唱年のようなのですが、入手できた最古の論文が1988年のObject-oriented programming: an objective sense of styleだったのでその内容ベースに以下進めます。

論文の構成は以下

  1. Introduction
  2. Notation
  3. The Law of Demeter
  4. The Motivation and Explanation
  5. Example
  6. The Trade-off
  7. The Interface
  8. The Weak and Strong Law of Demeter
  9. Conforming to the Law
  10. Compile Time Checking of the Law
  11. Minimum Documentation
  12. Formulations of the Law
  13. Conclusion

まず目につくのがNotation(記法)の部分。プログラミング言語自体を作る際BNF記法などで構文を定義することがあると思うのですが、EBNF記法をベースにデメテル記法を生み出しています。デメテル記法は構文ではなくclassの階層を表現しています。TypeScriptの型に近い概念を当時のやり方であらわすとこうなるのかなと。

Reference-Books-Sec =
  <ref-books> List-of-Books
  <ref-catalog> Catalog.
List-of-Books ~ {Book}.
Catalog ~ {Catalog-Entry}.
Book =
  <title> String
  <author> String
  <id> Book-identifiier

継承があるオブジェクト指向の言語をベースに主に親子関係を語っているのがポイント

デメテルの法則はかたく書くとこうです。

全てのクラスCの全てのメソッドMについて、Mが送るメッセージの全てのオブジェクトは以下のクラスのインスタンスでなければならない。

  • Mの引数クラス(Cを含む)
  • Cのインスタンス変数

ムズカシイ。。。要約すると先ほどの 最も近しい友とのみ会話せよ になるようです。

デメテルの法則の利点と欠点

この法則を使うことで以下の恩恵を受けます。

  • 結合性のコントロール
  • 情報の隠蔽化
  • 情報の制限
  • 情報の局所化
  • インターフェースの最小化
  • 構造の推測が容易に

逆に欠点としては孫以下の部分を隠蔽するので子クラスのメソッドが増えてしまいます。

強いデメテルの法則と弱いデメテルの法則についても語られており、他のクラスを継承したクラスにおいてインスタンス変数を使った場合、元クラスのデメテルの法則の規定上OKかどうかで判断で分かれる模様。

Active Supportのdelegateはデメテルの法則のパターンに当てはまっているのか

以上を踏まえてActive Supportのdelegateはデメテルの法則の想定パターンに当てはまっているでしょうか? Active Supportのdelegateはデメテルの法則の想定パターンに必ずしも当てはまっていないのではが自分の見解です。

確かにActive Supportのdelegate を使うとa.b().c()a.b()にする"使えるドットは一つまで"のルールを遵守する方向にはなります。一方でデメテルの法則の元論文ではclassの階層や親子関係を意識しているように感じました。a.b().c()のaはより親であることが多い想定ではないかと。

一方でActive Supportのdelegateはレファレンスからしてbelongs_toの子から親のメソッドを参照するのに使われています。依存の方向が逆ですね。

また一番使用例の多いActive Record間同士のdelegateはClean Architecture風のレイヤー構造でいうとモデル同士であり、親子関係というより隣近所での使用となっています。

個人的な経験でも、テーブルのカラム名をそのまま関連モデルのdelegateに渡してしまうケースにおいて、いろんなテーブルから双方向にdelegate依存ができてしまうとコードの見通しが悪くリファクタリングが困難に陥ったことがあります。

どういう時にdelegateを使うといいのか

ではどういう時にdelegateを使うといいのかというと、Active Recordでは基本delegateの使用は避けつつ、使うケースでもドメインの主たるテーブルから子への参照にとどめるのがいいのではと思いました。

プログラムの規模が大きくなってくるとActive RecordというかモデルのレイヤーのコードがFatになりすぎると読みにくくなることがあります。そういった際にForm Objectなり、ViewModelなり、モデルに書いていた処理をより上位のラッパー概念に移すと見通しが良くなることがあります。

RailsのFormで使われるaccepts_nested_attributes_forも魔法が強すぎて使わない方がいいと言われることがありますが、delegateについても次にRailsの魔法が強い部分だと思っており、そういったリファクタリングをする際の足枷になる場合もあるが自分の見解です。

  • Active Recordでのdelegateの使用は基本避ける

モデルの関係や機能が複雑になってくるとモジュール化が進み、モデルの上位概念を発明したくなります。

複数モデルを統合して処理するような概念を産んだ方が見通しが良くなる。ショートハンドの記法はリファクタリングと相性が悪いので避ける。

  • Active Recordでdelegateを使う際はドメインのメインテーブルのみにする

デメテルの法則の元論文で成し遂げたかったことは親子関係の子部分をラップしたいであり、子から親へはdelegateをはらず、BlogArticleで言えばBlogArticleのメソッドがBlogArticleCategoryの複雑性をラップする方向に倒す。

そうしておくことで、依存の方向が単方向に絞れ、将来的にBlogArticleのコードが肥大化しすぎた際に、より上位の概念への移し替えが簡単になる。

また、ユーザー系テーブルについても機能テーブルからユーザー系テーブルへの参照は疎結合にしたいのでdelegateは使わない。機能軸とユーザー軸は疎結合にした方がコードの見通しが上がり、将来的にユーザーDBが分かれるような場合にも対応できるため。

別解としてデコレーター系のgemを使っている場合に、ショートハンド部分はデコレーターに寄せることで将来のリファクタリングがしやすくなったりします。

最後に

以上が個人的な見解です。自分はPythonもよく書くのでRubyistの中では明示的な書き方が好きな方かもしれません。

delegate使用禁止というと便利なのにと言われてあまりメンバーの理解が得られたことが少ないのでつらいところ。そういった場合、使用を親から子への参照にとどめるくらいが落とし所かもしれません。

Discussion

Naoki FujitaNaoki Fujita

同感です。
動的型付け言語の場合、API呼び出しがどのメソッド実体を指しているのかが不鮮明になりがちで、
.method_aみたいなインタフェースがいろんなクラスにdelegateを通じて実装されると、そのメソッドをリネームしたり削除する文脈において、困ったことになりがちだと思います。

rubyはmixinとかメタプロとかも多用する文化であることが多いですが、結局保守を考えた時にみごとに諸刃の剣になるなと思っています。(作って捨てるならそれでいいんですけどね)