delegate と向き合おう
はじめに
ラブグラフで 3 月から開発インターンをしている arawi です!
この記事では Rails の delegate の話をしていきます。
TL;DR
- DB アクセスを隠蔽する delegate は危険
- デメテルの法則を達成するために、delegate を利用して形式的にメソッドチェーンを減らせばいいわけではない
そもそも delegate とは
delegate は Active Support によって導入される機能で、delegate マクロを利用することでメソッドを簡単に委譲できるようになります。
詳細は Rails ガイド 等を参照してください。
デメテルの法則(Law of Demeter)
デメテルの法則は、オブジェクト同士を疎結合にするための原則で、「隣のオブジェクトのみに話しかけよう」「自分以外のことは知りすぎないようにしよう」などと表されたりします。具体的な例を挙げると以下のようになります。
# This example is taken from "Practical Object-Oriented Design in Ruby".
# BAD
customer.bicycle.wheel.rotate
# GOOD
customer.ride
これらのコードで表現したいのはどちらも「お客さんが乗り物に乗って進む」ということですが、 BAD のコードは知りすぎています。このコードは、rotate メソッドを持った wheel とそれを持つ bicycle がないとうまく動きません。いわゆる「密結合」なオブジェクトになっていますね。
一方、 GOOD のコードはシンプルです。 ride メソッドを適切に定義すれば、お客さんが自転車で移動しようと車で移動しようと目的を達成することができます。
GOOD のコードを目指すのがデメテルの法則です。形式的には「ドットを減らそう」と言われたりもします。
delegate と デメテル
delegate を使うとデメテルの法則を達成することができるとされています。例を見てみましょう。
class Order < ApplicationRecord
belongs_to :customer
delegate :phone_number, to: :customer
end
# When you want to access phone_number...
# Without delegate
order.customer.phone_number
# With delegate
order.phone_number
delegate を利用することで、お客さんの電話番号を取得するのに customer オブジェクトを経由しなくてよくなります。ドットが減った!デメテルもにっこり!
......本当にそうでしょうか?
delegate を使うとつらいとき
実は、上の例は DB アクセスを伴うような場合にはつらくなります。phone_number は customers テーブルのカラムとして保存されているとします。
<% @orders.each do |order| %>
<p><%= order.phone_number %></p>
<% end %>
Order に delegate が仕込まれていることに気づかず、 phone_number が orders テーブルのカラムのような気持ちで上のようなコードを書いたとき、このコードは忌まわしき N+1 を発生させてしまいます。
正しくは、以下のように書かないといけません。
<% @orders.includes(:customer).each do |order| %>
<p><%= order.phone_number %></p>
<% end %>
これだけ見ると、「なんで customer を includes してるんだろう?」という疑問が湧きませんか?結局、この delegate は可読性を下げてしまっているのです。
対策
ではどうすればよいでしょうか?方法は2つあると思います。
- DBアクセスを隠蔽するような delegate を避ける
- prefix を使う
問題の本質は「delegate を使うことで生えるメソッドがカラム由来のメソッドに見えてしまう」ことです。それを避けるため、結局 delegate を使わず直接書き下すのは選択肢の1つとなると思います。
2つめの prefix は delegate マクロに与えることのできるオプションで、先ほどの例は prefix を使うと次のようになります。
class Order < ApplicationRecord
belongs_to :customer
delegate :phone_number, to: :customer, prefix: true
end
# When you want to access phone_number...
# Without delegate
order.customer.phone_number
# With delegate
order.customer_phone_number
prefix を true にすることで、 Order に生えるメソッドには customer_phone_number
のように、先頭に委譲先のモデル名が付きます。これにより、カラム名と勘違いするリスクはいくつか抑えられるでしょう。
おわりに
以上です。実際業務の中で湧いた疑問を記事にしてみました!
delegate やデメテルと向かい合って開発していきましょう!!
Discussion