Rails: 私の好きなコード(3)"正しく書かれた" concerns(翻訳)
Railsのconcernsは長年に渡って多くの批判にさらされてきました。果たしてconcernsはあらゆる問題を解決するのか、それとも全力で避けるべきなのでしょうか?私の考えとしては、concernsの問題は「いくらでも好き勝手に使えてしまう」ことであり、それならばconcernsで自分の足を撃ち抜くのも無理はありません。concernsは、結局のところRubyのmix-inにシンタックスシュガーを少々加える形で、よくある定型コードを取り除いているだけなのです、
37signalsは、大規模なRailsコードベースでconcernsを扱うときの経験を長年積み重ねているので、私たちが用いている設計原則の一部を本記事で紹介したいと思います。
🔗 concernsはどこに置けばいいのか?
Rubyのmix-inは、多重継承に代わる「クラス間でコードを再利用する」手法として説明されることがよくあります。私たちの場合、concernsをその形で使うこともありますが、最もよく使われるのは、単一のモデル内でコードを整理するためのconcernsです。以下の2つのケースでは、それぞれ異なる規約を使い分けています。
- モデル共通のconcerns: app/models/concerns/ディレクトリに置く
- 特定モデルのconcerns: モデル名と同じフォルダに置く(app/models/<モデル名>/)
たとえば、以下はBasecampで使われている特定モデルのconcernです。
# app/models/recording.rb
class Recording < ApplicationRecord
include Completable
end
# app/models/recording/completable.rb
module Recording::Completable
extend ActiveSupport::Concern
end
この規約によって、このconcernをinclude
するときに名前空間を繰り返し書く必要がなくなります。
コントローラの場合は状況が逆になります。concernsはほとんどの場合controllers/concerns/フォルダに置き、特定のサブシステムにのみ適用するconcernsについては、サブシステム名にちなんだフォルダ(controller/concerns/<サブシステム名>/)に置きます。なお、コントローラでのconcernsの使い方については別記事で解説したいと思います。
🔗 concernsは可読性を改善する
concernsについてよくある批判は「可読性が落ちる」というものです。私はそうは思いません。concernsは、正しく用いれば2つの点で可読性が向上します。
第1に、concernsは複雑さを管理するのに有用です。
複雑なシステムを扱う方法の真髄は、前回の記事↓にも書いたように「小さく分割することを繰り返して、一度に1つの内容に専念できるようにする」ことです。ツールボックスに常備されているconcernsも、まさにこれを実現するツールなのです。
参考: Rails: 私の好きなコード(2)フラクタルな旅(翻訳)
ここでは、どのconcernも、ホストとなるモデルの特徴を凝縮した形でキャプチャすべきであるという点が重要です。言い換えれば、concernsにはそれが属するものだけをまとめるべきです。
逆にconcernsは、巨大なモデルを細かなパーツに分割する目的で、振る舞いや構造を何でもかんでも押し込めておく物置にしてはいけません。
クラスの継承では「is a」リレーションシップを必要としますが、それと同じように、concernsは本物の「has trait」や「act as」セマンティクスが機能する必要があります。そのようになっていないconcernsは、メリットよりも害の方が大きくなります。
参考: Rails: 私の好きなコード(1)大胆かつ的確なドメイン駆動開発(翻訳)
上の過去記事でもお話ししましたが、HEYのスクリーニングシステムの以下のコード例をチェックしてみましょう。HEYのユーザーは、他の連絡先から受け取った、そのユーザーにメールを送信する許可を得るためのクリアランス嘆願書(clearance petition)を検査する検査官(examiner)として振る舞います。
class User < ApplicationRecord
include Examiner
end
module User::Examiner
extend ActiveSupport::Concern
included do
has_many :clearances, foreign_key: "examiner_id", class_name: "Clearance", dependent: :destroy
end
def approve(contacts)
...
end
def has_approved?(contact)
...
end
def has_denied?(contact)
...
end
...
end
このconcernは、そのドメインの「クリアランス嘆願書の検査官」というロールと一致しており、そのロールに関連するコードだけを含んでいます。
このように、管理しなければならない概念を少なくすればするほど理解がたやすくなり、メンテナンスしやすくなります。
第2に、concernsはドメインの概念を反映する抽象化を追加します。
以下はHEYのTopic
モデルにinclude
されているconcernsです。先ほどの検査官の例と同様に、concernsの多くの命名にドメインの概念が適切に取り込まれていて、楽に理解できる点にご注目ください。concernsの命名をドメイン概念に近い形にしていることで、読みやすさの面でもプラスに働きます。
class Topic< ApplicationRecord
include Accessible, Breakoutable, Deletable, Entries, Incineratable, Indexed, Involvable, Journal, Mergeable, Named, Nettable, Notifiable, Postable, Publishable, Preapproved, Collectionable, Recycled, Redeliverable, Replyable, Restorable, Sortable, Spam, Spanning
...
🔗 concernsはリッチなオブジェクトモデルを置き換えるものではなく、強化するものである
Railsのconcernsについてよくある誤解のひとつは、concernsは「クラス継承」や「コンポジション」といった伝統的なオブジェクト指向の手法とは別の手法を表しているというものです。
たとえばこの記事です。
ビジネスロジックのモデリングは、concernsよりも抽象化(クラス)でやる方がよい。Value Object、Service、Repository、Aggregateなどのような、より適したアーティファクトを利用しよう。
About Rails concerns. > TL;DR. Don’t use Rails concerns. | by Carles Climent | Mediumより
この記事もそうです。
コンポジションが望ましい
私は、あらゆるものを1個のファイルに入れなければならないと言っているのではありません。ロジックをカスタムクラスに切り出して、それを呼び出してください。
When To Be Concerned About Concerns | Cloudbees Blogより
私は「concerns vs 他の何か」のような二項対立的な考え方は誤りだと思います。concernsの概念を利用したところで、システム設計が楽になるわけでも不要になるわけでもありません。
特に、構造化されていないファットモデルをうまく整理整頓したいのであれば、condernsをそのような目的に使うべきではありません。代わりに責務を正しく分散したオブジェクトを用いて、適切なシステムにすべきです。
私は初めてconcernsを使ったときに、そうやって痛い目にあった経験があるので、それこそがconcernsの本当のリスクであることを思い知りました。
37signalsは、古き良きオブジェクト指向設計、継承とコンポジション、デザインパターンと実装パターンを大切にしているので、私たちのmodels/フォルダ全体に渡って多くのPORO(Plain Old Ruby Object)が置かれています。concernsは、この方法論と非常に相性がよいのです。これについてシンプルな例で説明してみましょう。
HEYの有料会員は、サブスクリプションを解約してもメールアドレスは永久に予約済みのままになります。そのため、システムがアカウントを終了させる場合は「データを完全に削除する(incination: 焼却)」か「最小限のデータを残す」かを選択することになります。この機能に関連するコードを見てみましょう。
class Account < ApplicationRecord
include Closable
end
module Account::Closable
def terminate
purge_or_incinerate if terminable?
end
private
def purge_or_incinerate
eligible_for_purge? ? purge : incinerate
end
def purge
Account::Closing::Purging.new(self).run
end
def incinerate
Account::Closing::Incineration.new(self).run
end
end
焼却とパージ(purge)という操作は、コードの一部を共有する形で互いに関連しています。では、私たちはこれをどう解決しているでしょうか?私たちの場合は、それらの操作をカプセル化した追加クラスと、昔ながらの継承を用いて共通のコードを再利用しています。
私は、こういう形でconcernsを使う方法が大好きです。すなわち、モデルにドメイン指向のAPIをいい感じに提供し、複雑なサブシステムを呼び出し側の目線から隠蔽するというものです。
あるアカウントを停止したければ、以下を実行するだけでよいのです。
account.terminate
それに比べると、以下はいかにも冗長でぎこちなく感じられます。
AccountTerminationService.new(account).run
ここでご注目いただきたいのは、アカウントをincinate
したりpurge
するロジックを丸ごと扱う責務を抱え込んだファットなAccount
モデルが存在しないということです。ここにはそれを担当する3つのクラスで構成されたサブシステムがあり、Account
モデルはその機能を使う通用口を提供しているだけです。
concernsは、システム設計でできる範囲を犠牲にせずに、モデルのコードを整理整頓し、簡潔で良い感じのAPIを実現します。
🔗 まとめ
concernsは道具の1つです。concernsが、The Rails Doctorinesにあるような"切れ味の良い"刃物なのか、それとも単にオープンすぎるだけなのかは私には何とも言えませんが、使い方を間違えればトラブルの元になります。しかし皆さんがRailsプログラマーであれば、いくつかのシンプルなガイドラインに沿って使うことで素晴らしく輝くリソースになると私は思っています。
concernsは、優れたオブジェクト指向設計と組み合わせることで力を発揮します。当然ながら、concernsを使ったからといってソフトウェアの設計方法を知らないままで済むというものではありません。しかしそれでも、concernsはコードを整理整頓して理解しやすくメンテしやすくするための実用的なメカニズムです。
「素のRailsではここまでしかできない、その上にいろんな構成要素や安全器具や規約を乗せる必要がある」みたいな話をよく耳にします。しかし実際には、BasecampとHEYは伝統あるオブジェクト指向とパターンを用いる素のRailsアプリであり、そこではconcernsを多用しているのです。
関連記事
- 訳注: 私の好きなコードシリーズの(4)に相当するのは『素のRailsは十分に豊かである(翻訳)』です。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。concernsについては以下の記事もどうぞ。