🎰

ドメイン駆動で大胆に行こう(翻訳)

2023/02/01に公開

英語記事: 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 と共に保存する。こうすることで、エントリーが作成される前に対処することができる。

AnalysisInsightAnalysisInsightDecision というコードを持つ。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 JordanUnsplash

脚注
  1. その他翻訳記事:
    第二弾: Fractal journeys
    第三弾: Good concerns
    第四弾(TechRachoさん): Vanilla Rails is plenty
    第五弾: Active Record, nice and blended ↩︎

Discussion