かとじゅんの技術日誌

技術の話をするところ

ドメイン固有型(値オブジェクト含む)を再考する

Value Objectが盛り上がっているらしい。

Value Objectについて整理しよう - Software Transactional Memo

Value Objectの説明に異論がないものの、主題はValue Object Obsessionのほうですよね。

こちらも聞いてみた。

fukabori.fm

よい機会なので、よくわかっているつもりの、値オブジェクトというかドメイン固有型について再考してみよう。

それは値か属性か

それはエンティティの全メンバーやデータベースの全列のために「顧客郵便番号」「送付先郵便番号」「事業所郵便番号」「契約日」などのクラス(メンバではなくクラス!)を定義して、immutableな振る舞いを強制する事を以てValue Objectであると言い張り、ドメイン知識の断片をそれぞれのクラスに書き散らして「高凝集になった」「型システムが守ってくれる」と喜ぶ奇行に走る。

これは妥当な指摘だと思います。Twitterの一部で話題になったのですが、この論点は「値(attribute)」と「属性(property)」を混同するなということかと。「その値オブジェクトは値じゃなくて属性のほうじゃないの?」ってやつ。(過去に属性を値オブジェクト化したことがあったかもしれません。やらかしてたらすみません)

たとえば「住所(PostalAddress)」は値オブジェクトになりうるとしても「出荷先住所(ShippingPostalAddress)」はいずれかのオブジェクトの属性であってこれ自体が値オブジェクトではない。

コードにしてみるとものすごい違和感がある…。

// 出荷
public class Shipping {
   // 出荷先住所
   private ShippingPostalAddress postalAddress;
   // あるとしても、こっちでは? PostalAddress postalAddress;
   // 以下 略
}

仮に、出荷先住所(ShippingPostalAddress)を是とするならば、住所(PostalAddress)からサブタイプを作るのか。それとも継承ではなく委譲を使って特化した型を作るのか。いずれにしても、出荷先住所(ShippingPostalAddress)は、一般的な住所(PostalAddress)より特化した責務を担うのかどうか。なかなか想像しにくい…。通常なら住所(PostalAddress)で十分ではないかと。

しかし、こういう要求が絶対にないとはいいにくい…。特化した要求があるとしても、出荷(Shipping)クラスの中で閉じるのであれば、住所(PostalAddress)で十分かもしれない。他でも再利用されるならば、独自の型が必要になるかもしれない。状況により判断されるのでどちらがいいとも悪いとも言えない。

値か属性かを考えること自体は全く妥当なことだと思う。しかし、値か属性かを明確に判断する基準はつくれるのか…。

属性というのは文脈に紐付く値といえる。そもそも文脈に紐付かない値というのはあり得るのか。『実践ドメイン駆動設計』の「アカウント」モデルの話を思い出そう。「アカウント」モデルのコンテキストは、銀行口座コンテキストなのか、文学コンテキストなのか。「アカウント」モデルは境界づけられたコンテキストの属性的な一面を担っているのではないか。単に値と見ているものでも、暗黙的な文脈があるかもしれない。どのように判断すべきか。

値か属性か、はっきりする場合はよい。しかし、たなかさんがいうように簡単に線引きできる問題ではないかもしれない。グレーゾーンの中にいたら、僕らはどうするべきか。実験の失敗から学ぶしかないのではないか。

振る舞いはPofEAAの値オブジェクトの本質ではないのか

もちろん郵便番号クラスをValue Objectとして実装するのは順当な話であるが、それをValue Object足らしめているのは「比較のために内部の郵便番号の数値を使うオブジェクトとした事」であって「プリミティブ型を包んで振る舞いを足した事」ではない。

こちらについて。不変条件(invariant)を維持するための振る舞いがあったとしても、値オブジェクト由来の概念ではなく、ドメイン固有型(ドメインオブジェクト)が持つ制約でしょうと。つまり、振る舞いの方ではなく、不変と等価判定にこそ存在意義があると読めました。うーん、僕は少し違う解釈をしています。

そして、なんでもかんでも値をラップして、別名割当て問題を回避するために不変にする。さらに等価判定も実装する…。余計な複雑さを取り込んでいるよね…と。まぁこれはわかります。さらに、前述したように属性を誤ってクラス化したりしないように…。

ドメイン固有型はモデリングツールでもある

これは僕の解釈です。値オブジェクトはドメイン固有型の一種です。なので、不変と等価判定だけではなく、なにかしらのドメイン固有の不変条件(invariant)を維持する責任があると考えます(もちろん型として切り出すわけですからその投資に見合うだけの見返りがないといけません)。

PofEAAの値オブジェクトであるMoney型も不変条件(invariant)を維持する操作を備えていると解釈できます。

Moneyの場合、Moneyオブジェクトを数字のように簡単に使えるような、算術演算が必要である。しかし、Moneyの算術演算と数字のMoney演算との間には、重要な相違点がいくつかある。最も明確なのは、異なる貨幣単位の金銭を合算しようとする場合、加算や減算では常に貨幣単位を認識する必要があることである。シンプルでよく使われる対処方法は、異なる貨幣単位の合算をエラーとして処理することである。

つまるところ、値オブジェクトであっても扱う値に健全性がなければ、それは存在価値や利用価値がなくなってしまうので、値オブジェクトである前にドメイン固有型(ドメインオブジェクト)として責任を果たすことは当然だと考えています。詳しくは述べませんが、こう考える理由は『エリックエヴァンスのドメイン駆動設計』(以下DDD) の 第10章 しなやかな設計 にあります。

こうした表明が記述するのは、すべて状態であって手続きではないため、容易に分析できる。クラスの不変条件によって、クラスの意味が特徴づけられ、オブジェクトをより予測しやすくすることで、クライアント開発者の仕事が単純化される。事後条件による保証を信用するのであれば、メソッドがどう動くかを気にする必要はない。委譲による影響は、表明の中に組み込まれているはずである。

長くなるので書きませんが理由は他にもあります。「解決する問題にあった設計を作り出すために、中心となる概念を捉えるモデルが必要。そのモデルをツールとしてソフトウェアに組み込む 」みたいな考え方があります。DDDの「第3部 より深い洞察へ向かうリファクタリング」あたりに書かれています。『エンタープライズアプリケーションアーキテクチャパターン』(以下 PofEAA), 『UMLモデリングのエッセンス』, 『リファクタリング』ではこのような視点が少なく、DDDとして特徴的な部分だと思います。つまるところ、ドメイン固有型はモデリングツールの側面があるんだと思います。

なので、値をなんでもかんでもラップしてクラス化*1するという杓子定規ではなく、まずこういった目的に合致するかどうかは、当たり前ですが確認する必要があります。一方で前述したようなデメリットも生じますし、値がプリミティブ型でよい場合もあります。これはトレードオフするしかありません。

ドメイン固有型の手段が「とにかくクラス化する」の一辺倒だから問題が生じやすいという意見もあるので、type aliasやopaque type aliasなど実装手段は最適なものをその状況に合わせて考えたいですね。

ドメイン固有型とセキュリティ

「とにかく値オブジェクト化しよう!」は「オブジェクト指向エクササイズの影響ではないか」という話がありました。その説の可能性もありますが、別の説も書いておきます。

「オブジェクト指向エクササイズ」でクセの強いコードを矯正しよう | ALTUS-FIVE

プリミティブ型を避けてドメイン固有型を好む考え方は、『セキュア・バイ・デザイン』の影響もあるのではないかと思います。

安全なアプリケーションを設計するという観点から、ドメイン固有型=ドメイン・プリミティブが注目されています。

ドメイン・プリミティブとは、その存在だけで、その値が有効であることを保証する厳格な定義がなされた値オブジェクトのことである。

XSSやインジェクションなど、既知の攻撃を対策すればセキュリティの対策は十分か?答えはノーです。

とあるオンライン書店の話。あるユーザーから$39の本 -1冊の注文を受け付けてしまった。注文金額が$-39となった。明らかに不具合。本来であれば、その書籍を返品しなければなりませんが、そうはならなかった。技術的な問題ではなく、システムがドメインのルールから逸れてしまった問題です。既知の攻撃対策では防げません。

本書では、多くの開発者が汎用的なデータ型を選択するが、セキュリティの観点からそれは大きな間違いであると指摘しています。例えば、電話番号を文字列型として扱うのは便利と考えるかもしれないが、電話番号以外のいかなる種類の値であっても受け入れることができてしまう。これでは不正な入力や操作を防ぎようがない、と。

ドメイン・モデルに含まれる概念はどれも基本データ型やStringのような汎用的な型を使って表現されるべきではありません。ドメイン・モデル内の各概念はドメイン・プリミティブとしてモデリングされるべきであり、そうすることで、そのオブジェクトが様々なところに渡されても、そのオブジェクトの意味が伝わり、不変条件を維持できるようになります。

具体的な例として、UserAccountを表示する入力画面での、XSS対策を考えます。

UserAccountに以下のような対策コードを埋め込むことがありますが、問題があります。XSS以外の問題に対応できない可能性があります。

浅いドメイン理解では、ユーザー名を文字列型として捉えてしまうかもしれません。

public class UserAccount {
   public  UserAccount(long id, String userName) {
      validateForXSS(userName); // XSSを防ぐ
      // ...
   }
}

『セキュア・バイ・デザインでは』、前述のコードではなく以下のよう変更することを推奨しています。

ユーザー名の制約に 4から40文字、半角英数とアンダーバーおよびハイフンのみのルールがあると、深いドメイン理解を得られた場合、以下のような表明*2を追加可能です。ドメイン知識を反映して不変条件を維持すれば、結果的にXSS対策も対策可能になるわけです。

public class UserAccount {
   // ...
   public UserAccount(long id, String userName) {
      this.id = assertLength(userName, 4, 40); // 長さが4以上40以内か
      this.userName = assertPatterns(userName, "^[a-zA-Z0-9_-]+$");  // 文字種が適切か
   }
}

userNameの利用箇所がUserAccountだけならよいですが、他にもあった場合このような表明をあちこちに格納するのは馬鹿らしいので、UserNameとしてのドメイン・プリミティブを新設することになります。こうすれば、UserAccountUserNameの型を受け取るだけで、正しいユーザー名を受け入れることができます。

public class UserName {
   // ...
   public  UserName(String value) {
      assertLength(value, 4, 40); // 長さが4以上40以内か
      assertPatterns(value, "^[a-zA-Z0-9_-]+$");  // 文字種が適切か
      this.value = value;
   }
}


public class UserAccount {
   // ...
   public UserAccount(long id, UserName userName) { // UserAccountはUserNameを表明すればよいだけになる
      this.id = id;
      this.userName = userName;
   }
}

他の実装例としては、リスト5.1の1〜200個の制約を持つ数量クラス(Quantity)がありますが、内部属性を一つしか持ちません。compound(合成物)ではありません。数量クラスのどんな操作をやっても、0個や201個以上は許容されません。単にintだけを扱ってしまうと、脆弱性の問題に繋がる可能性があるからドメイン固有型を中心に設計しようという考え方です。これはプリミティブ型を辞める理由になると思います。

もちろん、こういった考え方を採用するかは前述したトレードオフを検討すべきです。 どこまでドメインモデルを厳格にするべきか。プレゼンテーション層でバリデーションが終われば、ドメインロジックでわざわざこんなことしなくてもよい?いやいやバリデーション通過後に矛盾が起きる場合はどうするのかなど、考えることがいろいろあります。あと、クラス化が唯一の選択肢なのかも。使える言語が限られますが、型クラスを使えば既存の値型に振る舞いを追加することも可能です。

とはいえ、セキュリティ対策はドメイン固有型を採用する大きなモチベーションになっているのではないでしょうか。私も実際にドメインプリミティブを採用した結果、脆弱性診断でゼロ件を経験したこともあります。「プリミティブ型よりドメイン固有の型を」のエピソードのように、多くの人が探査機を燃やすようなことは避けたいと考えるのではないでしょうか。

ja.wikisource.org

まぁセキュリティ対策のために他のすべてが犠牲になってよいのか。そういう極端なことは現実的ではないので、全体性を考慮に入れるのは当然ですが…。

まとめ

まとめらしい、まとめにならないな…。

まぁドメイン固有型もプリミティブ型も「用法・用量を守ろう」としかいいようがないですね…。個人の開発ならどうでもいいと思いますが、開発者間の合意が必要なケースでは、問題のコンテキストに合致していないのに方法論だけ都合よく抜き取って対策したつもりになるのは避けたいですね。Howの解決策だけでなく、なぜそれをやるのかWhyも整理したほうがよいですね。例えばADRを書くなど。

adr.github.io qiita.com

まぁ、すぐどっちの方法論のよいのか悪いのかと考える癖がありますね。僕もあります。方法論にAとBがあるとして、A=プラス・B=マイナスという固定的な見方はしないほうがいい。こういうのを二項対立の思考といいますが、割と無意識にやっています。

ここには思考停止の罠があります。Bのマイナスだって状況が変わればマイナスとも言い切れないわけですから。たとえば、プリミティブ型がプラス、ドメイン固有型がマイナスとした場合、モデリングをそこまで重視しない・セキュリティ対策もそこまで大げさにしなくてもよいシンプルな要件なら成り立ちます。しかし、逆の状況ならプラス・マイナスが逆転するかもしれません。

さらいうと要件が中間レベルにある場合は、AとBのグレーゾーンを考慮しなくてはならなくなります。たとえば、ある部分ではプリミティブ型を重視、違う部分ではドメイン固有型を重視するなど。

二項のうちどちらかに決めつけると不安を払拭できるけど、変化に対する適応力が乏しくなってしまう。二項対立から離れて、グレーゾーンを思考する能力が設計には必要だと思います。

ということで、みなさん頑張っていきましょう。


(余談) そもそもPofEAAとDDDの値オブジェクトが同じかどうか

そもそもPofEAAとDDDの値オブジェクトを同一視できるのかについては、一部で論争がありました。僕もこれについてはよくわかりません。2009年なので13年も前のことです。まだ和訳本がでていない時期…。

Value と Entity - 感想 - Aufheben - GLAD!! の日記

Value ObjectとDTO - yyamanoの日記

DDDのバリューオブジェクトは不変性が本質ではない - かとじゅんの技術日誌

このときの僕の見解

個人的な感想は、やはりDDDとPofEAAで共に出てくる概念に関連性を求めてしまうと混乱する可能性があるなと思いました。似てるところが多いとは言え、完全に同じかというとそれはわからないですね。そこはFowlerとEvansを呼んで概念の関連性を確認しないと、どうにもはっきりしない領域です。

ともあれ、PofEAAのことはさておき、DDDのバリューオブジェクトは、可変も不変もあるので、複製と共有もありと解釈した。大変参考になるエントリでした。気づきを得ることができました。先人に敬意を払いたいですね。

つまりこういうこと

  1. DDDの値オブジェクトとPofEAAの値オブジェクトは似ているようで、解釈が異なる部分がある
  2. DDDの値オブジェクトは不変は必須ではない。もちろん言語や環境に問題がなければ不変であることが望ましい。

1)のような混乱が生じるときは、無理に同一視しなくてもよいと考えています。

(おまけ) Rustではどうするのか

ハイコンテキストな話題なので興味がある人向けです。

JavaのStringは不変です。たとえば String#concat は状態を破壊せずに、追加後の新しいStringインスタンスを返します。仮に、あまりに変更が頻繁なら可変オブジェクトである StringBuilder を使うことになります。共有するなら不変、変更が頻繁なら局所で可変を使うのが基本的な考え方。

Rustの場合も不変がデフォルトですが、String#concatのような新しいインスタンスを返す設計にはほとんどしません。Stringpush_strは以下のように&mut selfを要求するので、不変参照時では呼びだすことができず、可変参照があるときにしか呼べません。可変を安全に扱えるようになっています。つまりメソッドが可変か不変かを決めるので、わざわざStringBuilderのような型を作る必要がありません。

let orginal = String::from("abc");
println!("orginal = {}", orginal);
// orginal.push_str("def"); コンパイルエラー

let mut modify = orginal.clone();
modify.push_str("def");
println!("modify = {}", modify);

Javaでは可変・不変の型を分ける。Rustではメソッドごとに可変(&mut self)・不変(&self)を分けるだけです。

さらに、Rustであっても、守るべき不変条件(invariant)があれば型として定義することがあるでしょう。たとえば、通貨単位が同一でなければMoney#addできないなど。

github.com

単一の値に制約を課すだけなら、newtypeする以外に、値型のためのtraitの実装を追加する方法もあると思います。

*1:Javaのような言語だとクラスということになりますが、Haskell, Scala, Rustなら型クラスも選択肢になるでしょう

*2:無害なasseretとするか、例外をスローするかは設計判断によるものとします