はじめに
スマレジにて私が担当しているプロジェクトではドメイン駆動設計を取り入れて日々開発をしています。
ジョインしてからドメイン駆動設計について説明を受けたものの体系的に学べていないので書籍を一つ読みながら足りていない箇所を補っていきます。
今回の記事は書籍の内容に沿っているつもりですが、多分に個人的な見解が含まれているため、書籍の著者の考えをきちんと把握したい場合はぜひ書籍を読んでみてください。
ちなみにスマレジに入社してすぐの頃にドメイン駆動設計について調べて記事にしました。
現在はこの頃より理解は進んでいて、人に教えるというイベントが発生する立場になり新しくジョインしたメンバーにうまく伝えるためにも語彙を増やしておきたいです。
今回はこれからドメイン駆動設計に入門する人を対象に、理解をすすめる順番を意識して記事にしてみます。
対象書籍
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本
ドメイン駆動設計とは
ドメイン駆動設計は、ソフトウェアの開発手法の一つです。
まずはそもそもソフトウェアを開発する目的を考えるところから始めてみましょう。
一般的にソフトウェアは、現実世界に存在するある領域において何らかの問題を解決するために開発されます。
この「ある領域」のことを指してドメインと呼びます。(ドメインは「領域」という意味をもつ単語です)
このドメインをコード上に表現するためにモデルを作成します。
モデルという言葉自体はソフトウェア開発に携わっている方なら頻繁に耳にするでしょうし、関連する文献にも頻出します。
ではこのモデルがドメイン駆動設計という文脈で何を表しているのかについて考えてみましょう。
モデルとは、現実の事象あるいは概念を抽象化した概念です。 ドメイン駆動設計とは、ドメインのモデリングによってソフトウェアの価値を高めるというアプローチの開発手法です。
よく挙げられる例として、例えば日報を書くシステムを考えてみます。紙とペンがあります。 ペンを使用して紙に文字を書いて記録するという一連の流れの中にも現実世界には非常に多くの情報があります。
ペンの値段、形、重さ、インクの色 紙の材質、大きさ、色、形 日報を記入する人の名前、所属、年齢 日報の内容、日付、長さ...etc
こうした無数の情報の中から、日報システムに必要な情報を定義します。
例えば、
- 日報記入者名
- 日付
- 内容
といったものを抽出します。これがモデルとよばれるものです。 現実世界においてドメインという概念は非常に多くの情報を含み、複雑なものです。そのドメインをコード上に表現するために、必要な情報のみを抽出し表現しやすくします。
モデルについて理解したところで、ドメイン駆動開発という手法について着目します。
まず、ドメイン駆動設計は以下の3つの概念を中心に考えます。
先程、
と書きましたが、ドメイン駆動設計は次の2つのステップによって成り立っています。
- ドメインモデルを継続的に改善する
- モデルを継続的にソフトウェアに反映する
このうち、1を実現するために「ユビキタス言語」「境界づけられたコンテキスト」「ドメインエキスパート」といったキーワードとその手法が存在しますが、今回は割愛します。 ざっくりいうと、ドメインに詳しい「ドメインエキスパート」と一緒に共通の言語として「ユビキタス言語」を用いてモデルを改善し続けるのが1つ目のステップです。
今回のゴール
今回は2のモデルを継続的にソフトウェアに反映するについて先に触れたいと思います。
今回参考にしている書籍「ドメイン駆動入門」の中心となるのもモデルをどうソフトウェアに反映するのか、という点です。
ドメイン駆動設計の本質は難しく、なかなか実践に取り入れることのできない概念が多く存在します。まずは具体的なドメイン駆動設計の実装パターンを取り入れることでドメイン駆動設計の本質を理解するための準備をする、というのが今回のゴールです。
ドメインモデルを定義した状態から、具体的にコードに反映するためのパターンについて理解していきます。
ValueObject
さあ、いよいよ具体的なコードの話に入ります。
書籍はC#で記載されていますが、本記事では毎度のことながらPHPでサンプルを書いてみます。
まずは値オブジェクト(ValueObject)パターンです。
プログラミング言語にはプリミティブな値が用意されています。(例: int, string)ちなみにプリミティブには原始や初期のような意味があります。
ValueObjectの例として、システムのユーザー名を例にコードで表現すると以下のようになります。
class UserName { public const MAX_LENGTH = 20; private string $name; public function __construct(string $name){ $this->validate($name); $this->name = $name; } public function __toString(): string { return $this->name; } protected function validate(string $name): void { if (mb_strlen($value) > self::MAX_LENGTH) { throw new InvalidArgumentException('ユーザー名は20文字以下でなければならない'); } } }
このようにクラスを用いてシステム固有の値を表現したものを値オブジェクト(ValueObject)とよびます。
例えば、システムにおける 「ユーザー名」という値の長さはドメインにおける知識です。
適切なモデルをコードに落とし込む際に最適な値は必ずしもプリミティブな値であるとは限らないということになります。
こういった「ドメインモデル」を実装したオブジェクトをドメインオブジェクトと呼びます。
ValueObjectを使用するメリットは、主に以下の3つです。
- 表現力を増す(クラス名による表現が可能になる)
- 不正な値を存在させない(バリデーションロジックをコンストラクタで実行することで不正なインスタンスを生成させない)
- ロジックの散在を防ぐ(関連するバリデーション等のロジックをValueObject内に凝集できる)
ここで発生するであろう疑問として、ValueObjectと他のclassの違いはどこにあるのか?という疑問が考えられます。
その答えに近づくために、そもそも「値」とはどういうものなのかということについて見直す必要があります。
書籍でも紹介されている代表的な「値」の性質には3つあります。
- 不変である
- 交換可能である
- 等価性によって比較される
以下、一つずつ確認していきます。
値の不変性
「値」は不変の性質を持ちます。
private string $greet = 'こんにちは'; $greet = 'Hello'; // Helloが出力される echo $greet;
$greet
という変数が変更されています。どういうことでしょう。
変数というのは中身を変更する際に代入をします。
代入というのは、変数の中身を変更することであり、値自体の変更ではありません。
プログラミング言語の入門書によくある「変数は箱」という例を思い出してみるとわかると思います。
代入という行為は箱の中身を新しい値によって上書きする行為です。
決して値そのものが変更されているわけではありません。
この不変性はソフトウェア開発において大きなメリットになります。生成したインスタンスを知らないところで変更され、意図しない挙動となりバグを引き起こすということは日常的に発生します。変更が原因のバグを発生させないもっともシンプルな対策は不変にすることです。ソフトウェアの世界は複雑で困難なので様々な方法で制約を実現して人間がわかりやすい形にすることが好まれます。(例えば型システム)
デメリットとしては値を変更するたびにインスタンスを生成して代入をしなければならないため、パフォーマンスの面では劣ります(C言語等メモリを意識する言語を触ってみるとわかりやすいかもしれません)。が、現代の特にWeb開発においては明らかにメリットのほうが大きいはずです。
値は交換可能
値というのは「変更」はできない不変性をもっていますが、値の変更自体は必要です。矛盾しているように聞こえますが、プログラミングにおいては私達は常に代入を用いて値の交換を行い、変更を表現しています。
private UserName $name; $name = new UserName('わたし'); $name = new UserName('あなた'); // あなたが出力される echo $name;
このとき、$name
は代入によって変更されています。つまり、どちらもUserName型の値ではありますが、インスタンスは全くの別物であり、最初に代入された値オブジェクト自体が変更されているわけではありません。
値は等価性によって比較される
まずはプリミティブ型の値の比較について確認します。
echo (0 == 0); // true echo (0 == 1); // false
1つ目の式の左辺の0と右辺の0は別のインスタンスですが、同じものとして扱われています。 これは、インスタンス自身ではなく、属性によって比較されているということです。
では、ValueObjectの場合にはどう表現すればよいでしょうか。 1つ目の方法は、値の属性を取り出して比較する方法です。
private UserName $name; $name1 = new UserName('わたし'); $name2 = new UserName('あなた'); $compareResult = $name1.value == $name2.value
もちろん上記のコードは動作するので一見正しくみえます。 ただ、「ValueObjectは値」なので「値の値」にアクセスしているのは不自然な記述になります。 数値を比較する際に
1.value === 2.value
という書き方をしないことからも不自然だということがわかります。
どうすればいいのかというとValueObject同士が比較できるようなメソッドを用意するのが自然な記述となります。
private UserName $name; $name1 = new UserName('わたし'); $name2 = new UserName('あなた'); $compareResult = $name1.equals($name2);
この比較用メソッドを用意することの利点
- 記述が自然になる
- 新たに属性が追加されても利用側に比較処理を追加しなくてもいい(ValueObject内に比較処理を隠蔽できるため)
ではどういった値をValueObjectとして表現して、どういった値をプリミティブ型のまま扱うのか、判断する基準が欲しくなります。
まず前提として、設計段階でドメインモデルとして抽出したものはValueObjectにするべきです。ドメインモデルに存在する属性の場合は頻出する概念であり、関連するロジックを散在させないためにもValueObjectに凝集しましょう。 次の判断基準として、書籍で紹介されているのが
- そこにルールが存在しているか
- それ単体で扱いたいか
の2つです。システム上でルールが存在する場合はルールのチェックロジックを散在させないためにもValueObjectの使用を検討しましょう。 ドメインモデルに存在しない属性で、単体で取り扱いたい概念を発見した場合は、ドメインモデルに追加することを検討し、ValueObject化を検討します。こうした実装中の気づきをドメインモデルに反映することもドメイン駆動設計を支える一つのポイントになります。
エンティティ
こちらもValueObject同様、ドメインモデルを実装したドメインオブジェクトです。
ValueObjectとエンティティの差は同一性によって識別されるか否かです。
ValueObjectはその属性によって識別されるオブジェクトです。
エンティティは同一性(識別子)によって区別されます。
ValueObjectとの違いを意識しながら、エンティティについて理解を進めるとつかみやすい概念かなと思います。
エンティティの性質は次の3つです。
- 可変である
- 同じ属性であっても区別される
- 同一性によって区別される
エンティティの可変性
ValueObjectは不変なオブジェクトでした。変更を表現するためには代入を使用していました。
エンティティは可変なオブジェクトです。エンティティの属性は変化することが許容されています。
例えば、ValueObjectの際にも例に挙げたようにユーザーという概念について考えてみます。
class User { private string $name; public function __construct(string $name) { $this->name = $name; } public function changeName(string $name) { $this->name = $name; } }
上記のように「ユーザー名を変更する」という振る舞いをエンティティに持たせることができます。
ValueObjectと違い、属性が変わったとしてもインスタンスは同じです。
このようにエンティティの属性は変更することが許容されています。
また、書籍ではユーザー名としてのルールをchangeName
というメソッドの中にガード節として書けば良いと記載されていますが、個人的にはせっかくValueObjectを学んだので$name
自体をValueObjectにするべきだと思っています。そうすることでコンストラクタからの入力の際にも同じバリデーションを実行できますし、利用する側でも型でどういう意味の値かわかりやすくなります。
エンティティは同じ属性であっても区別される
ドメイン駆動設計のエンティティの同一性の説明でもっともありふれた例は人間です。
ここでは人間のドメインモデルとして氏名(性と名から成る名前)をもつモデルを考えてみます。
同姓同名の人間が二人いる、つまり氏名という属性が一致している場合に同じ人物を指しているといえるでしょうか。
ドメインモデルにほかのあらゆる属性を定義したとして、つまりクローンを作成したとしても人間というのは区別されます。
属性だけでは区別されないのです。
人間において何をもって区別するのか、というのは哲学の領域になってしまいますが、エンティティはこの区別に識別子を使用して区別します。
つまり、エンティティとは識別子(identifier)と属性を持つオブジェクトだといえます。
エンティティは同一性をもつ
先程、同じ属性であっても識別子によって区別されるのがエンティティと書きました。この識別子を持つことでエンティティは同一性という性質も持つことになります。
先程の人間と氏名を例にすると、氏名を変更した場合、氏名の変更前後で別人になってしまうのか?というのが同一性です。
エンティティにおいて、そのエンティティをエンティティたらしめるものは識別子です。
つまり、エンティティのもつすべての属性が変更されようとも識別子が同じであればエンティティは同一のものとなります。
この同一性の比較を行うための最も典型的な実装が、ValueObjectのときと同じく、比較用の振る舞いをもつことです。
class User { private string $identifier; private string $name; public function __construct( string $identifier, string $name) { $this->identifier = $identifier; $this->name = $name; } public function equals(self $user): bool { return (string)$this->identifier === (string)$user->identifier; } }
ValueObjectと決定的に違う点はValueObjectではすべての属性を比較の対象としていましたが、エンティティの場合は比較処理の対象が識別子のみである点です。
ドメインモデルから実装する際のValueObjectとエンティティの判断基準
ValueObjectもエンティティもドメインモデルを表現するためのドメインオブジェクトであり、非常に似通っています。
ドメインモデルの概念のうち、何をValueObjectにして何をエンティティにすればいいのでしょうか。
たしかに自分自身も普段の業務において無意識にValueObjectとエンティティを区別しているのかうまく言語化できていないなと感じました。 個人的には、概念として異なる複数の属性を持っているものがエンティティかなと思っていましたが、書籍ではより良い基準が紹介されています。
その基準はライフサイクルです。
例えば、ユーザーの場合、サービスに登録し作成された時点で生を受け、退会処理時に死を迎えます。
まさにライフサイクルを持つ概念です。こういった明確にライフサイクルを持つものはエンティティとして表現しましょう。
反対にライフサイクルを持たない、もしくは持つ意味がないオブジェクトはValueObjectとして扱いましょう。
プログラミングにおいて可変性はできる限り避け、不変な値のみを扱うほうがシンプルになります。
このことからも迷った場合もひとまずValueObjectとして表現しておくべきです。
まとめ
ドメインモデルを実装に反映したオブジェクトをドメインオブジェクトといい、ValueObjectやエンティティもドメインオブジェクトの一種です。
ValueObject
- 不変である
- 交換可能である
- 等価性によって比較される
エンティティ
- 可変である
- 同じ属性であっても区別される
- 同一性によって区別される
ドメインオブジェクトを作成し、ドメインの知識をコードにすると、たちまちコードはドキュメントとして機能し始めます。
コードとは別にドキュメントを作成して仕様を表現するというのが一般的ですが、コード上で表現できるのであればより仕様が理解しやすくなります。
また、コードからドキュメントを生成するDoxygenやphpDocumentorをはじめとしたツールを使用する場合にはより効果的に使用することができます。
また、ドメインモデルの変更が発生した場合に、コードへの実装がドメインモデルに沿ったものになっていると、その変更は容易になります。
ソフトウェアは作って終わりにはならないものです。人の営みが移ろうのにあわせてソフトウェアに求められる仕様も変わっていきます。こうした変化への対応を容易にするための一つのアプローチがドメイン駆動設計だと考えています。
所感
今回紹介した書籍はタイトルどおり、ドメイン駆動設計の入門としてかなり有効に使用できるものでした。 特に具体的にコードに落とし込む方法について見当もついていない状態では非常に頼れる道標となりそうです。
ドメイン駆動設計に限らず、プログラミングに向き合うとモデリングという壁にぶつかると思っていて、この本の内容を理解した次のステップはモデリングだと考えています。
今後の流れとしては
- Repository
- Factory
- Service
について紹介しようと思います。
今回参考にさせて頂いた書籍には他にも「集約」や「仕様」等まだまだドメイン駆動設計についての紹介がありますので一度手にとって読んで頂くことをおすすめしたいです。
今回の記事がこれからドメイン駆動設計に入門しようとする人の助けになれば幸いです。