Rails: 私の好きなコード(1)大胆かつ的確なドメイン駆動開発(翻訳)
私がほぼ3年前に37signalsで働き始めたときに手がけた作業のひとつは、Basecamp用のGitリポジトリをcloneすることでした。いろいろやっているうちに、以下のメソッドが目に止まりました。
module Person::Tombstonable
...
def decease
case
when deceasable?
erect_tombstone
remove_administratorships
remove_accesses_later
self
when deceased?
nil
else
raise ArgumentError, "an account owner cannot be removed. You must transfer ownership first"
end
end
end
Basecampアプリにおけるpersonは、UserやClientといった特定の種別を表現するdelegated type属性(#39341)を持ちます。指定のアカウントからpersonを削除すると、Basecampはそれをプレースホルダに置き換えるので、関連データは変更されることなく引き続き機能します。
私はドメイン駆動設計(DDD: Domain-Driven Design)やドメイン概念をコードに反映することの重要性についてはひととおり精通していましたが、これほど意識的に実践している例を見たのはこれが初めてでした。「personを削除するときにpersonをプレースホルダーに置き換える」ぐらいまでなら自分でもやれそうですが、「personを削除するときに墓石を建てる(erect tombstone)」というのは実にうまい方法ですね。
客観的に見れば、この概念は明確かつ簡潔で、間違えようもないほど雄弁です。主観的に見れば、この命名にはまるでpersonality(人格)やsoul(魂)のような大胆不敵な要素があります。こういう命名をコードで使うのはありでしょうか?もちろんありですし、うまくハマりさえすればコードは著しく強化されます。私にとっての「aha!」体験でした。
別の例として、HEYのスクリーニングシステムを紹介します。このシステムは、ユーザーにメールを送信したい連絡先からリクエストされた「クリアランス嘆願書」をユーザーが審査する形を取ります。
ここにも大胆不敵な要素を見ることができます。嘆願書(petition)は形式的であることを示唆するので、要求(request)とは別物です。HEYの仕様におけるスクリーニングは形式張った形で行われます。他の人は、ユーザーの許可なしに受信トレイのメールを取得できないようになっています。
「検査官(examiner)は、請願者(petitioner)からのクリアランス嘆願書(clearance petition)を承認しなければならない」という概念は、このシステムが行っていることを他の人にくっきりと明確に説明する方法になっており、以下のコードもまさしくこの概念を反映したものになっています。
class Contact < ApplicationRecord
include Petitioner
...
end
module Contact::Petitioner
extend ActiveSupport::Concern
included do
has_many :clearance_petitions, foreign_key: "petitioner_id", class_name: "Clearance", dependent: :destroy
end
...
end
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
...
end
ここでのconcernsの使い方を見ていると、DCIアーキテクチャパターンにおける"Role"が思い出されます。DCI(Data, Context, Interaction)は興味深いアイデアに満ち溢れた提案のひとつですが、なかなかうまくコードに反映されません。ここでのconcernsの使い方は、Roleの実装としてかなり実用的です。
私が一筋縄では行かないモデルを構築するときのお気に入りのツールは「プレーンテキストで説明文を書く」というものです。私がHEYでメール分析システムの改良を手がけていたとき、新しいドメインモデルがどのようなものになるかについて自分用のメモを書きました。以下は、そのときのメモ(上)と、システムが構築されたときのプルリクに含めた説明文(下)です。メモ書きは自分のための思考ツールとして書いたものなので、内容とその正確さは無関係です。しかし、複雑なシステムについて考えるときプレーンテキストで書いたのは素晴らしい出発点と言えます。そんなとき、辞書は大事なパートナーとなります。
ドメイン
システムへの入力はAnalysis::InboundEmail
になる。今後より多くの要素を取り入れたくなったら別のエンティティを作成することにする。
Analysis
は一連のAnalysis::Rules
を含む。あるルールが実行されるとき、Analysis
はAnalysis::InboundEmail
を受け取ってAnalysis::Insights
のリストを返す。
Analysis
の実行結果はAnalysis::Result
であり、そこに一連のAnalysis::Rules
を含む。
アナリシスがOKでなかった場合は、それの元となるActionMailbox::InboundEmail
に沿ってアナリシスを保存する。これによって、エンティティが作成される前であってもアクションが動作可能になる。
1個のAnalysisInsights
にはコード(AnalysisInsightDecision
)が含まれる。決定には、種別(type)や大きさ(magnitude)がある。種別はbounce
やspam
になる可能性がある。
ある決定について、一連のルールの集約された大きさが1より大きい場合は、その決定がアナリシスの結果となる。
ドメインモデル
以下の4つのエンティティがある。
- アナリシスのある
Rule
は、指定のメールに対してInsight
を返すInsight
は、アクションの種別(現在はok
、reject
、spam
)や重みといった属性を決定し、アナリシスの結果をトレースするAnalysis
は一連のRule
を含む。アナリシスを実行すると、ルールによるすべてのインサイト(洞察)を収集してResult
を返す。Result
は、指定のアナリシスに関するすべてのインサイトをグループ化する
Result
は、ポリモーフィック関連付けによってActionMailbox::InboundEmail
に関連付けられる。Result
を保存するのは、メールがOKでない場合に限ることにする。なお、代わりにReceipt
レベルに追加することも検討したが、受信メールレベルで行う方が、同じシステムでメールをバウンスできるので理にかなっていると思う。重み付けシステムがあるのでアナリシスの判定にファジーさを加えることが可能だ(今すぐ活用するわけではないが)。アナリシスを実行するときに、指定の
kind
の重みセットが1.0以上にならない限り、すべてのルールは上から順に実行される。現時点ではすべてのインサイトが同じ1.0の重みを持つことになる。
HEYもBasecampも、最初のコミット時点からドメイン駆動設計にがっつり賭けています。もちろん、だからといって隅々まで光り輝くほど完璧になるわけではありませんが、総じてコードベースを楽しみながら読めるようになります。
多くの書籍が、いかにして優れたドメインモデルを作り出すかを主題に据えていますが、私が37signalsのコードベースから学んだのは「潔癖症になるな」、そして「いつもにも増して大胆であれ」ということです。
本記事は、『私の好きなコード』シリーズの記事です。
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。一部の画像は原文と配置を変えています。