ドメイン駆動で大胆に行こう(翻訳)
英語記事: Domain driven boldness
原文公開日: 2022/06/13
原著者: Jorge Manrubia
3年前に37signalsで働き始めたとき、私は最初にBasecampのgitリポジトリをクローンしました。あちこち見て回っていたところ、この#decease
メソッドにたどり着きました。
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の「人」は、その特定の種類を表すdelegate type属性を持っています(例:UserやClient)。アカウントから人を削除する際、Basecampはその人をプレースホルダに置き換えることで、関連データには触れずに機能性を保ちます。
私は、ドメイン駆動設計のことはよく知っており、ドメインの概念が反映されたコードの重要性もよく理解していました。しかし、その考えがこれほど意図的に実践されているのを見るのは初めてでした。「人を削除する時にプレースホルダに置き換える」ようなことは予想していましたが、「人を亡くす時に墓石を建てる」という方法は、遥かに優れていました。
客観的には、雄弁で明快で簡潔でした。主観的には、個性や魂のような大胆な要素を持っていました。コードでそのようなことができるとは思いもしませんでした。さらにそれが正しく行われると、とても強力になるのです。私にとっては、ハッとするような瞬間でした。
もう一つの例として、HEYのスクリーニングシステムを紹介しましょう。このシステムは内部的には次のようなものです。「ユーザ(users)」は、メールを送信する「連絡先(contacts)」から依頼された「(送信)許可申請(clearance petitions)」を審査します。
ここでもまた、その大胆さを感じられます。「申請」は正式なものを意味するため「要求」とは異なります。HEYのスクリーニングは、正式な手続きを取ります。あなたの許可がなければ、誰も受信箱にメールを入れることはできません。「審査官」が承認しなければならない「申請者」による「許可申請」は、システムが行うことを別の人間に説明するための明快な方法であり、それがまさにコードに反映されているのです。
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アーキテクチャパターンにおける役割(Roles)を思い出させるものでした。DCIは興味深いアイデアに満ちた提案の一つですが、コードに上手く反映されることはあまりありません。しかしこのconcernsの使い方は、役割をとても実用的に実装しています。
非自明で複雑なモデルを構築する際の私のお気に入りのツールは、プレーンテキストの説明を書くことです。HEYのメール分析システムを改良したとき、私は新しいドメインモデルがどのように見えるかについて自分用のメモを書きました。下の図は、その自分用のメモ(左)と、システムを構築した後にプルリクエストに記載した説明文(右)です。これは自分自身の思考ツールとして書いたものなので、メモの内容やその正確さはここでは重要ではありません。ですが、プレーンテキストに書くことは、複雑なシステムについて考えるときの素晴らしい出発点になります。そして、辞書はその時の素晴らしい相棒です。
自分用のメモ書き | プルリクエスト記載の文章 |
画像内の文章の翻訳はこちら
ドメイン(画像左)
システムへの入力は Analysis::InboundEmail
になる。将来的にもっと多くの要素を取り込みたい場合に備えて、分離したエンティティを作成する。
Analysis
は一連の Analysis::Rules
を含む。ルールが実行されると、Analysis::InboundEmail
を受け取り、Analysis::Insight
のリストが返される。
Analysis
の実行結果は Analysis::Result
で、一連の Analysis::Insights
が含まれる。
解析が正常でない場合、解析結果をその元となった ActionMailbox::InboundEmail
と共に保存する。こうすることで、エントリーが作成される前に対処することができる。
AnalysisInsight
は AnalysisInsightDecision
というコードを持つ。decisionはtypeとmagnitudeを持つ。typeはbounce
もしくはspam
である。
集約された役割の大きさが、与えられたdecisionに対して > 1 である場合、そのdecisionが分析結果となる。
ドメインモデル(画像右)
4つの基本的なエンティティがあります。
- 解析の
Rule
は与えられたメールに対してInsight
を返します。 -
Insight
には、解析判断を追跡するために、アクションの種類 (現在は:ok
,:reject
,:spam
) や重み、その他の属性が設定されています。 -
Analysis
は一連のRules
を含みます。解析を実行すると、ルールによる全ての洞察を収集してResult
を返します。 -
Result
は、指定された分析に関するすべての洞察をグループ化します。
Result
はポリモーフィック関連によってActionMailbox::InboundEmail
に関連付けられます。メールがOKでない場合のみ、それを保存することになります。代わりにReceipt
レベルで追加することも考えましたが、受信メールの場合は受信メールのレベルで行う方が理にかなっていると考えたため、同じシステムを用いてメールのバウンスもできるようにします。
すぐには活用しませんが、重み付けのシステムによって、分析の判断に曖昧さを加えることができます。分析を行う際、与えられたkind
に対する重みのセットが >=1.0 でない限り、すべてのルールを順番に実行することになります。現状では、すべてのインサイトが1.0の重みを持つことになります。
HEYもBasecampも、最初のコミットからドメイン駆動設計に強く賭けています。もちろん、だからといって隅々まで完璧というわけではありません。ただ、総じて彼らのコードベースを読むのは楽しいことです。良いドメインモデルの作り方については多くの本が主題にしていますが、ここで私が学んだ教訓は次のようなものです。 「潔癖にならず、大胆さに倍賭けしよう。」
この記事は、Code I likeというRailsの設計技法に関する連載記事に属しています。
jorgemanrubia.com
写真:Brett Jordan(Unsplash)
Discussion