本記事は READYFOR Advent Calendar 2022 の23日目の記事です。
(※ 本記事の内容は所属会社の公式発表・見解を示すものではありません。)
<< 昨日の記事は @shmokmt さんの MySQL ShellでダンプされたデータをMySQLにロードする。
>> 明日の記事は @pxfnc さんの関数型言語/プログラミング系の記事(予定)です。
■ はじめに
本記事では書籍「セキュア・バイ・デザイン」にて解説されている「ドメイン・プリミティブ」という設計手法について紹介をします。
このドメイン・プリミティブという考え方は、書籍「セキュア・バイ・デザイン」が DDD を下地にしているため DDD の文脈で語られることもあります。
しかし実は DDD の手法を採用するか否かを問わず広く利用可能 な考え方になります。
とても導入しやすく、かつお手軽に効果を得やすい考え方 であるため、是非皆さんも採用を検討してみて頂けると嬉しいです。
□ 注意点
サンプルの言語には C# を利用しますが、どなたにも分かりやすくなるように可能な限りシンプルかつ汎用的なコードにすることを目指します。
また2022年現在、C# であれば record
型 のような値を取り扱うための型が存在していますが、本記事では伝統的な「クラス」を使うこととします。
更に、本記事における「値オブジェクト」の定義は「セキュア・バイ・デザイン」に記載されているもの(後述)を用いるものとします。
本書の下敷きとなる「ドメイン駆動設計(DDD本)」が出版される前からある概念だそうですが、本記事ではその深淵には触れません。
完璧な歴史や定義を求める方は、そのような解説を書いてくださっている人の記事を参照いただけると幸いです🙇
■ ドメイン・プリミティブを使うと得られる可能性のあること
ドメイン・プリミティブを使うと、
- 値をそのドメイン(お仕事の領域) に特化した概念として扱うことができる
- 可読性・保守性が向上する
- 値の取り違えによるバグが減少する(異なる概念が混ざらなくなる)
- 修正漏れが減る
- 不正な値が存在しなくなる
- 初期化していない値によるバグがなくなる
- 上記の結果、生産性が向上する
といった良い効果を見込むことが出来ます。
以下、詳しく説明をしていきます。
ただし 他のテクニックと同様に、ドメイン・プリミティブが銀の弾丸でない事 は予めご承知おきください。
■ Domain Primitive / ドメイン・プリミティブ is 何?
□ ドメイン・プリミティブ is...?
書籍「セキュア・バイ・デザイン」にて紹介された設計/実装手法のひとつ です。
超アバウトに言うと、「値オブジェクト」に、その値が成立するためのルールを課したもの と認識すると良いでしょう。
「セキュア・バイ・デザイン」5章 P.170-171 では以下のように説明されています。
ドメイン・プリミティブとは、ドメインを構築する最小の要素を定義するために、安全性の高い実装テクニックと値オブジェクト(value object) を組み合わせたもののことです。
ドメイン・プリミティブはドメイン駆動設計の値オブジェクトと同じようなものです。しかしながら、ドメイン・プリミティブはその中に不変条件を持ち、オブジェクトの生成時にその不変条件が確認される、という大きな違いがあります。加えて、ドメイン・プリミティブはドメイン・モデルの概念を表すのに、プログラミング言語における基本データ型やStringのような汎用的な型の使用を許しておらず、さらに、null になることも許していません。
また値オブジェクトとは、同 3章 P.103 にて以下のように説明されています。
- 自身と他の値オブジェクトとを識別するための識別性(identity) を持たない代わりに、値で識別できるようになっている。
- 不変(immutable) である。
- 完結した概念(conceptual whole) を形成しなくてはならない。
- エンティティを参照できる。
- 重要な制約が明確に定義されており、その制約を守らせるようになっている。
- エンティティや他の値オブジェクトの属性として使われることがある。
- 生存期間は短いことが多い。
さて、この説明だけを見ても『そうなんだー』という感想しか出てこない方が多いのではないかと思います。
以下、もう少しだけ分かりやすい説明にトライしてみます。
□ ドメイン・プリミティブを3行で説明すると?
ドメイン・プリミティブとは・・・
- あるドメイン(同じ言葉を使う仕事の領域)において使われる 「何らかの値」 を、
- 不変条件と共に概念化 したもので、値オブジェクトの派生版。
- 概念をクラス化するため コードの取り扱いが楽 になり、不正な値は存在すら出来ないため より安全なプログラムを書く ことが可能となる。
□ もうちょっと翻訳すると?
ドメイン・プリミティブとは・・・
- どこか 特定の仕事の範囲でだけ 使われている「何らかの値」を、
- その特定の仕事の範囲でだけ 使う「独自の概念」として「その値が成立する為のルール」と一緒にクラス化します。
- 今まで
integer
やstring
として扱っていたためにコード上に散らばってしまっていた知識を1箇所に集められるようになります。
また その概念が成り立たないような値 を使ってインスタンス化しようとしたら落とすことで、不正な値をもつドメイン・プリミティブは存在すらさせません。
□ もう少し具体的な話にすると?
例えば「バナナ」を題材すると・・・
- バナナ農園の農家さんに向けた受発注システム において使われる バナナの数量を表す「房」という値を、
- 当該バナナ農園の受発注システムに必要な「1房以上1000房まで」というルールを備えた「房クラス」 とします。
-
「房」という概念をクラス化して関連の深いロジックをまとめる ので、「房」に関する計算があちこちに散らばって変更漏れを起こす 可能性が下がります。
また「-1房」や「99999999房」といったルール外の数を持った房クラスは生成すら出来ない ようにします(new()
したときにException
を投げる)。
る
■ まずは... ドメイン・プリミティブを使わないとどうなるのか?
まずはバナナの例に戻りましょう。
冒頭の例で挙げた「1房以上1000房まで」というルールがある時、
もしも旧来の手法のように バナナの房数を Integer
で表現したら どんな不都合が生じるでしょうか?
例えばこんな ↓ 問題が生じそうです。ぜんぜんルールを守っていません。
// ここでは説明を簡素化するためプリミティブ(`integer`) を使ったべた書きとします。
// 初期値としてゼロ房を設定したり(ルール外の数値)
int 房の数 = 0;
// エラーや無効な値としてマイナスを指定してみたり(ルール外の数値)
int 房の数 = -1;
// 誤操作などでとんでもない量の房数が指定されたり(入力値のバリデーションをバグなどですり抜けた。当然ルール外の数値)
int 房の数 = 9999999999;
// 計算の理由が分からないアレ(おまけでもしたのかな??)
房の数 = 房の数 * 1.2;
バナナ農家さんの受発注においてルールは守らねばいけません。
ちょっと画面操作をミスっただけで 9999999房のバナナ を発注できてしまっては困る のです。
『農家Fさんまた誤発注! バナナ買ってください赤字大放出!』とバナナを山積みして捌く技はあまり使いたくないものです(そういうマーケティングもありそうですが)。
そんな災害から身を守るため、きっと様々なところに下記 ↓ のような 身を守るためのコード が生まれていくことでしょう。
if (房の数 <= 0 || 1000 < 房の数) {
throw InvalidArgumentException("そんな数は房とは認めん!");
}
・・・そしてある日、上司から指令が来ました。
上限が2000房から5000房に 増える他、更に 新たなルールを追加 することになりました。
あなたは一生懸命にシステムに改修を加えるため、あちこちに散らばったコードを探して一つずつ直していきます。
しかし現実は非常。どこかをうっかり変更し忘れてしまって爆発する 悲しい事故が発生する訳です。
年末年始休暇返上の徹夜確定です。
まじで辛い。
■ ドメイン・プリミティブを使うとどうより安全になるの?
それではドメイン・プリミティブを使って、あなたのシステムを少しだけ安全にしていきましょう。
書籍「セキュア・バイ・デザイン」5章 P.170-171 で語られている、
ドメイン・プリミティブはドメイン駆動設計の値オブジェクトと同じようなものです。しかしながら、ドメイン・プリミティブはその中に不変条件を持ち、オブジェクトの生成時にその不変条件が確認される、という大きな違いがあります。加えて、ドメイン・プリミティブはドメイン・モデルの概念を表すのに、プログラミング言語における基本データ型やStringのような汎用的な型の使用を許しておらず、さらに、null になることも許していません。
というドメイン・プリミティブが持つ性質が、より安全なシステムに近づけるための パワー! を提供してくれます。
以下、安全となる理由を説明していきます。
□ 安全となる理由1: ドメイン・プリミティブは、インスタンスが存在していれば安全な値であることが確定する。
ドメイン・プリミティブは オブジェクトの生成時に不変条件(その値が存在するためのルール)を確認 し、条件を満たさなければエラー とします。
つまり「ドメイン・プリミティブを生成することが出来れば、その値が正しいものである事が確定する」のです。
「生成された値は正しいものである事が確定する」のですから、
今まであちこちで行っていた 房の数が正しいかを確認するコードはもはや不要 となります。
だって 値が存在している = それは安全な値 なのですから。
実際のサンプルコードで見てみましょう。
// 房数を表現するドメイン・プリミティブ
public class 房数 {
public 房数(int 房の数) {
if (房の数 <= 0 || 1000 < 房の数) {
throw InvalidArgumentException("そんな数は房とは認めん!");
}
Value = 房の数;
}
public int Value { get; init; }
// 値(Value) で比較するメソッドは省略。
}
// ドメイン・プリミティブを使う場所で・・・
new 房数(100); // OK (Value=100)
new 房数(0); // InvalidArgumentException
new 房数(-1); // InvalidArgumentException
new 房数(9999999999); // InvalidArgumentException
new 房数(null); // C# の場合はコンパイルすら通らない
...さて、上記のコードは integer
を使っていたときと比較していかがでしょうか?
integer
では int 房の数 = -1;
のように値に 0
でも -1
でも 9999999999
でも好きな値を入れてしまう事が可能でした。
一方ドメイン・プリミティブを使うと、そもそも ルールを満たす房の数を持った「房数」 しか作れないようになっていることが分かります。
毎回「変な値ではないかな・・?」とチェックしてあげる必要はもうありません。
あちこちに散らばっていた 房の数が安全であることを保証するコード を 削除 できるようになったのです。
影響範囲が減るため 変更漏れの可能性も下がります。
もしも使っている言語が静的型付け言語であれば、同じ integer
である userId
を間違えて渡してしまってバグを生み出す こともなくなります。
□ 安全となる理由2: あちこちに分散しがちな計算ロジックをまとめる場所が出来る
(これは値オブジェクトと変わらない効能です。)
今まで房の数を計算するロジックは、今まで様々なところで直接べた書きになっていました。
// 計算の理由が分からないアレ(おまけでもしたのかな??)
房の数 = 房の数 * 1.2;
そんな中、もしも房の数に関するルールが変わったらどう対応するでしょうか。
房の数に関連するロジックがどこに存在するのか、一生懸命に grep などで検索していた のではないでしょうか?
このような、房の数に関係するロジックを集めるに適した場所 が生まれたのです。
↓ のように、ドメイン・プリミティブ「房数」に、先程の 1.2倍 する太っ腹なメソッドを移動させてあげましょう。
// 房数を表現するドメイン・プリミティブ
public class 房数 {
public 房数(int 房の数) {
if (房の数 <= 0 || 1000 < 房の数) {
throw InvalidArgumentException("そんな数は房とは認めん!");
}
Value = 房の数;
}
public int Value { get; init; }
public 房数 おまけする() {
return new 房数(Value * 1.2);
}
// 値(Value) で比較するメソッドは省略。
}
// ドメイン・プリミティブを使う場所で・・・
new 房数(100); // OK (Value=100)
new 房数(100).おまけする(); // OK (Value=120)
もう房の数にかかわる計算処理がどこにあるかを一生懸命探す必要はありません。
房数クラスさえ見ればそこに房の数に関わる全てがある のですから。
「房数にはおまけという概念があるんだな」という事もクラスを見てあげれば理解することが可能となります。
関連する物が一箇所にまとまっているので、可読性が向上している のです。
■ まとめ
ドメイン・プリミティブを使うと、
- 値をそのドメイン(お仕事の領域) に特化した概念として扱うことができる
- 可読性・保守性が向上する
- 値の取り違えによるバグが減少する(異なる概念が混ざらなくなる)
- 修正漏れが減る
- 不正な値が存在しなくなる
- 初期化していない値によるバグがなくなる
- 上記の結果、生産性が向上する
といった良い効果を見込むことが出来ます。
ただし他のテクニックと同様に、ドメイン・プリミティブが銀の弾丸でない 事はご承知おきください。
以上、長文を読んで頂きありがとうございました。
本記事が皆様の役にほんの少しでも経つことを願っております。
本記事は READYFOR Advent Calendar 2022 の23日目の記事です。
(※ 本記事の内容は所属会社の公式発表・見解を示すものではありません。)
<< 昨日の記事は @shmokmt さんの MySQL ShellでダンプされたデータをMySQLにロードする。
>> 明日の記事は @pxfnc さんの関数型言語/プログラミング系の記事(予定)です。
■ おまけ: 値オブジェクトに由来するドメイン・プリミティブの性質
□ ドメイン・プリミティブは、同じドメイン・プリミティブ同士で比較することが可能。
値オブジェクトは、同じ値オブジェクトと「その値」で比較可能 でなければなりません。
当然、値オブジェクトの派生形である ドメイン・プリミティブも同じドメイン・プリミティブと比較可能 である必要があります。
房数(100)
と 房数(100)
は同じ値として比較できて欲しいですよね。
しかし、実は先の例で挙げた「房数ドメイン・プリミティブ」は 比較に失敗 してしまいます(説明を簡易にする目的で省いたため)。
// 房数を表現するドメイン・プリミティブ。
public class 房数 {
public 房数(int 房の数) {
Value = 房の数;
}
public int Value { get; init; }
}
// ドメイン・プリミティブを同じ値で作って比較すると、値は同じだが異なるものとして扱われてしまう。
var 房1 = new 房数(100);
var 房2 = new 房数(100);
var is同じ = 房1.Equals(房2) // false (異なる)
var is同じ = (房1 == 房2) // false (異なる)
これは、上記のサンプルが 「クラス」を使って実現されている ためです。
クラスのオブジェクト/インスタンスは、その「参照が等しいかどうかで比較」されてしまうため、
別々に new
した、同じ100
という房の数を持つ「房数」 が 異なるもの として扱われてしまっているのです。
したがって、房数同士を比較するための処理を書いてあげる必要があります。
C# で言うと、例えば以下のようなものです。
// あくまでもイメージです。そのまま使わないように!
// ※ IEquatable<房数> を実装することで「房数」同士を Equals() で比較可能とし、更に演算子 == と != を実装して式で比較可能とする。
public class 房数 : IEquatable<房数> {
public 房数(int x) {
Value = x;
}
public int Value { get; init; }
// 房数同士を比較可能とする
public bool Equals(房数 other) {
return this.Value == other.Value;
}
// "==" と "!=" で比較可能とする
public static bool operator ==(房数 left, 房数 right) => left?.Equals(right) ?? (right is null);
public static bool operator !=(房数 left, 房数 right) => !(left == right);
}
// ドメイン・プリミティブを同じ値で作って比較したとき、値の中身で比較するので、同じ値として取り扱われる。
var 房1 = new 房数(100);
var 房2 = new 房数(100);
var is同じ = 房1.Equals(房2) // true (等しい)
var is同じ = (房1 == 房2) // true (等しい)
ただし 2022年現在、様々な言語に「値」として取り扱われる専用の型(C#ならrecord
型) があることも多い ので、そうした型を使えば比較用のメソッドを実装してあげる必要はありません。
特にDDD本が書かれた時代には、このような「値」を便利に取り扱うための機能が用意された言語はあまり存在しませんでした。
書籍はこのような時代的背景も踏まえて読んでいく必要があります。
□ ドメイン・プリミティブは、基本的に不変(immutable) である。
不変だと書き換えられないから安全だよね! というだけでなく、
こちらも値オブジェクトやドメイン・プリミティブが「クラス」として実装されていたことに由来した理由もあるようです。
オブジェクトは複数箇所で共有することが可能です。
メモリ使用量を減らしたり、速度を向上するためのテクニックなどで使われることがあるのですが、
そうした共有されている状況で、どこか1箇所が値を書き換えてしまうと、他で使っている部分にも影響が出てしまいます。
そのため、安全に共有可能とするためには不変である必要があった のです。
可読性の向上や安全な取り扱いなどを踏まえて 可能な限り不変とすべき と考えます、
オブジェクトを作れる数が限定されている場合など、時と場合によっては不変としない方が良い事もあることに注意してください。