読書メモ。
- オブジェクト設計スタイルガイド
- https://www.oreilly.co.jp/books/9784814400331/
オブジェクト設計において、コードの読みやすさ、書きやすさ、メンテナンス性を向上させるにはどうすればよいでしょうか? 本書は、より良いオブジェクト指向のコードを書くためのルールを紹介します。オブジェクトの種類に応じたオブジェクトの構築、メソッドの定義、状態の変更や公開など、設計ルールを説明します。Java、Python、C#など、あらゆるオブジェクト指向言語に適用できるテクニックを、擬似コードを使ってわかりやすく解説します。コードの品質を上げるためのルールを紹介する本書は、プログラマ必携の一冊です。
本書について #
コードについて
コードサンプルは、多くのオブジェクト指向プログラマーが読めるように最適化された、架空のオブジェクト指向言語で書かれています。この言語は実際には存在しないので、本書のコードはどのような実行環境でも実行できません。PHP, Java, C# などの言語を使った経験があれば、コードサンプルは簡単に理解できると確信しています。
1 章:オブジェクトを使ったプログラミング入門 #
(省略)
2 章:サービスの作成 #
本章の内容
- サービスオブジェクトのインスタンス化
- 依存関係や設定値の注入と検証
- 省略可能なコンストラクタ引数の必須化
- 暗黙の依存関係の明示化
- サービスをイミュータブルにするための設計
まとめ
- サービスは1回で作成し、依存関係や設定値をすべてコンストラクタ引数として与えるようにします。サービスの依存関係はすべて明示的に、オブジェクトとして注入されるべきです。すべての設定値は検証されるべきです。コンストラクタに何らかの形で無効な引数が渡された場合は、例外を投げるべきです。
- 作成後のサービスはイミュータブルであるべきで、どのメソッドを呼び出しても振る舞いが変化してはいけません。
- アプリケーションの全サービスを組み合わせると、大きなイミュータブルオブジェクトグラフが形成されます。このオブジェクトグラフは多くの場合サービスコンテナによって管理されます。コントローラは、このグラフのエントリポイントになります。サービスは一度インスタンス化すれば、何度でも再利用できます。
3 章:ほかのオブジェクトの作成 #
本章の内容
- ほかの種類のオブジェクトのインスタンス化
- オブジェクトが不完全になることの防止
- ドメイン不変条件の保護
- 名前付きコンストラクタの使用
- アサーションの使用
まとめ
- サービスオブジェクトでないオブジェクトは、依存関係ではなく、値またはバリューオブジェクトを受け取ります。オブジェクトの構築時には、一貫した振る舞いをするために最低限必要なデータを提供してもらう必要があります。提供されたコンストラクタ引数のいずれかが何らかの形で無効な場合、コンストラクタはそれに関する例外を投げる必要があります。
- プリミティブ型の引数を(バリュー)オブジェクトでラップすると便利です。そうすることで、これらの値に対する検証ルールを簡単に再利用できます。また、値の型(クラス)にドメイン固有の名前をつけることで、コードに意味を持たせることができます。
- サービス以外のオブジェクトの場合は、コンストラクタはスタティックメソッドにする必要があります。これは名前付きコンストラクタとしても知られ、これを使うことでコード中にドメイン固有の名前を導入する機会が得られます。
- そのオブジェクトがユニットテストで指定された通りに振る舞うために必要なもの以上のデータをコンストラクタに渡さないようにしましょう。
- これらのルールの殆どが適用されないオブジェクトのひとつに、データ転送オブジェクト(DTO)があります。DTO は外の世界から提供されたデータを運ぶために使われ、その内部のすべてを公開します。
バリューオブジェクト #
ドメイン不変条件が複数の場所で検証されるのを防ぐために新しいオブジェクトを抽出する
同じ検証ロジックが同じクラス、あるいは異なるクラスで繰り返されているのをよく見かけるかもしれません。メールアドレス検証のロジックを別のメソッドに抽出することも簡単にできますが、よりよい解決策は、有効なメールアドレスを表す新しいタイプのオブジェクトを導入することです。すべてのオブジェクトは作成された時点で有効であると期待されます。
final class EmailAddress
{
private string emailAddress;
public function __construct(string emailAddress)
{
if (!is_valid_email_address(emailAddress)) {
throw new InvalidArgumentException(
'Invalid email address'
);
}
this.emailAddress = emailAddress;
}
}
EmailAddress オブジェクトに出会うたびにそれはすでに検証済みの値であることがわかっています。
バリューオブジェクトと呼ばれる新しいオブジェクトの中に値をラップすることは、検証ロジックをさまざまな場所で繰り返すことを回避するのに役に立つだけではありません。あるメソッドがプリミティブ型の値(string, int など)を受け入れていることに気づいたら、すぐにその値のためのクラスを導入することを検討すべきです。string や int などが型であるのと同様に、バリューオブジェクトクラス自体も型であると考えるべきです。型システムを効果的に拡張できます。言語のコンパイラやランタイムが型チェックを行い、メソッド引数を渡したり値を返したりする際に、正しい型だけが使われるようにできるからです。
複合地を表現する新しいオブジェクトを抽出する
たとえば、金額には常にその金額の通貨が付随します。もしメソッドが金額だけを受け取ったとしてもそれをどう扱えばよいのかわからないでしょう。
final class Amount
{
// ...
}
final class Currency
{
// ...
}
final class Product
{
public function setPrice(
Amount amount,
Currency currency
): void {
// ...
}
}
final class Converter
{
public function convert(
Amount localAmount,
Currency localCurrency,
Currency targetCurrency
): Amount {
// ...
}
}
この例の最後のメソッドでは、戻り値の型は非常に混乱を招くものです。Amount が返され、この金額の通貨は引数で渡された targetCurrency と一致することが期待されます。しかし、このメソッドの型を見ても、それは明らかではありません。
複数の値が一緒になっている(あるいは常に一緒に見つかる)ことに気づいたら、それらの値を新しい型にラップしてください。Amount と Currency の場合、この2つの組み合わせの名前は「money」が適切で、結果として Money クラスが生まれます。
final class Money
{
public function __construct(Amount amount, Currency currency)
{
// ...
}
}
この型を使用することで、これらの値をまとめて使用したいことを示しますが、別々に使用したい場合は、依然としてそれも可能です。
データ転送オブジェクト(DTO) #
アプリケーションの境界の近くにあるオブジェクトで、外界から送られてきたデータを、アプリケーションが扱えるような構造に変換するものです。この種類のオブジェクトは、データ転送オブジェクト(DTO)として知られています。
DTO はその状態を保護せず、すべてのプロパティを公開するため、ゲッタとセッタは必要ありません。つまり、public プロパティを使用すれば十分です。
アサーションを使ってコンストラクタ引数を検証する #
何か問題がある時に例外を投げるコンストラクタのような冒頭でのチェックは「アサーション」と呼ばれ。基本的には安全性のチェックです。アサーションは状況を把握し、材料を吟味し、異常があればシグナルを送ることができます。このため、アサーションは「前提条件チェック」とも呼ばれます。
例外を投げるのではなくバリデーションエラーを収集する #
ユーザーがフォームを再送信する前にすべての間違いを一度に修正できるようにするには、そのオブジェクトを処理するサービスにわたす前にコマンドのデータを検証する必要があります。これを行うひとつの方法として、コマンドに validate()
メソッドを追加し、バリデーションエラーの一覧を返すというものがあります。リストが空の場合は、送信されたデータが有効であることを意味します。
final class ScheduleMeetup
{
public string title;
public string date;
public function validate(): array
{
errors = [];
if (this.title == '') {
errors['title'][] = 'validation.empty_title';
}
if (this.date == '') {
errors['date'][] = 'validation.empty_date';
}
DateTime.createFromFormat('d/m/y', this.date);
errors = DateTime.getLastErrors();
if (errors['error_count'] > 0) {
errors['date'][] = 'validation.invalid_date_format';
}
return errors;
}
}
4 章:オブジェクトの操作 #
本章の内容
- ミュータブルオブジェクトとイミュータブルオブジェクトの区別
- モディファイアメソッドによる状態の変更や変更されたコピーの作成
- オブジェクトの比較
- 無効な状態への変更に対する保護
- イベントを使ったミュータブルオブジェクトへの変更の追随
まとめ
- 常に、作成後に変更できないイミュータブルオブジェクトを優先しましょう。もしイミュータブルオブジェクトに変更を加えたいのであれば、まずコピーを作成し、それから変更を加えてください。これを行うメソッドには宣言的な名前をつけましょう。モディファイアメソッドが呼ばれたあとも、オブジェクトは有効な状態であるようにしましょう。そのためには、正しいデータのみを受け取り、オブジェクトが無効な状態遷移をしないようにしましょう。
- エンティティのようなミュータブルオブジェクトでは、モディファイアメソッドの戻り値型は
void
となるべきです。このようなオブジェクトで発生する変更は、内部に記載されたイベントを解析することで明らかにできます。イミュータブルオブジェクトとは対象的に、ミュータブルオブジェクトは流れるようなインターフェイスを持つべきではありません。(this を返すことでメソッドチェーンできるような fluent interface のこと)
イミュータブルオブジェクトのモディファイアメソッドでは変更されたコピーを返す #
不変性についての知見に基づくと、イミュータブルオブジェクトは変更をするとみなされるメソッド(モディファイアメソッド)を持っても良いですが、それらはメソッドを呼び出したオブジェクトの状態を変更しないようにしましょう。その代わり、そのようなメソッドでは意図にあった状態のオブジェクトのコピーを返しましょう。メソッドの戻り値の型は、そのオブジェクトのクラスそのものであるべきです。
final class Integer
{
private int integer;
public function __constructor(int integer)
{
this.integer = integer;
}
public function plus(Integer other): Integer
{
return new Integer(this.integer + other.integer);
}
}
Integer には int 値を受け取るコンストラクタがすでにあるので、既存の整数を加算してできた int 値を Integer のコンストラクタに渡せばよいのです。
複数のプロパティを持つイミュータブルオブジェクトに対して有効な別の方法もあります。それは、clone
演算子を使用してオブジェクトのコピーを作成し、それに必要な変更を加えるというものです。次の withX()
メソッドがこれを行います。
final class Position
{
private int x;
private int y;
public function __construct(int x, int y)
{
this.x = x;
this.y = y;
}
public function withX(int x): Position
{
copy = clone this;
copy.x = x;
return copy;
}
}
withX()
はセッタメソッドに似ていて、クライアントが1つのプロパティの値を書き換えることができるようになっています。この方法では、新しく設定する値の計算をクライアントに強いています。これよりも良い選択肢がある場合があります。クライアントがどのようにこれらのメソッドを使うかを確かめることで、より良い選択肢への有効な手がかりが見つかるかもしれません。
たとえば、以下は withX()
メソッドのクライアントです。
nextPosition = position.withX(position.x() - 4); // 4歩左に移動する
Position は x に新しい値を設定するモディファイアメソッドしか持っていないので、このクライアントはどういった値を渡すか、自分自身で計算しなければなりません。しかし、このクライアントは x を変更する方法を探しているのではなく、4歩左に移動したら次の位置がどうなるかを知る方法を必要としているのです。
クライアントに計算を指せる代わりに、Position オブジェクトに計算をさせればよいのです。次の toTheLeft()
のような、より便利なモディファイアメソッドを提供すればよいのです。
final class Position
{
// ...
public function toTheLeft(int steps): Position
{
copy = clone this;
copy.x = copy.x - steps;
return copy;
}
}
position = new Position(10, 20);
nextPosition = position.toTheLeft(4);
ミュータブルオブジェクトではモディファイアメソッドはコマンドメソッドとする #
ほとんどすべてのオブジェクトがイミュータブルであるべきとはいえ、そうではないオブジェクト、すなわちエンティティが通常は存在します。
例として、Player クラスを見てみましょう。このクラスは X と Y の値としてエンコードされた現在位置を持ちます。これはミュータブルオブジェクトです。moveLeft()
メソッドを持っており、プレイヤーの位置を更新し(実際には置き換え)ます。Position オブジェクトはイミュータブルですが、Player オブジェクト自体はミュータブルです。
final class Player
{
private Position position;
public function __construct(Position initialPosition)
{
this.position = initialPosition;
}
public function moveLeft(int steps): void
{
this.position = this.position.toTheLeft(steps);
}
public function currentPosition(): Position
{
return this.position;
}
}
moveLeft()
の中で代入しているので、このクラスはミュータブルであるとわかります。このメソッドを呼び出すと、position プロパティは新しい値に更新されます。もうひとつの特徴は、戻り値が void であることです。この2つの特徴は、いわゆるコマンドメソッドの証しです。
オブジェクトの状態を変更するメソッドは、常にこのようなコマンドメソッドであるべきです。コマンドメソッドは命令形の名前を持ち、オブジェクトの内部データ構造に変更を加えることが許され、そして何も返しません。
イミュータブルオブジェクトではモディファイアメソッドは宣言的な名前 #
ミュータブルオブジェクトのモディファイアメソッドは、オブジェクトの状態を変更することが期待されます。これはコマンドメソッドの特徴とよく一致します。イミュータブルオブジェクトのモディファイアメソッドについては、別の取り決めが必要です。
先ほど見たのと同じ Position の実装で、今度は toTheLeft()
が moveLeft()
という名前だと想像してください。
final class Position
{
// ...
public function moveLeft(int steps): Position
{
// ...
}
}
ミュータブルオブジェクトのモディファイアメソッドはコマンドメソッドであるべきというルールを考えると、この moveLeft()
は紛らわしいです。命令形の名前(moveLeft()
)を持っていますが、戻り値型 void ではありません。実装を見ない限り、このコードの読み手はこのメソッドを呼ぶことでオブジェクトの状態が変化するのかどうかがわかりません。
イミュータブルオブジェクトのモディファイアメソッドに適した名前を作るには、次のようなテンプレートを使うと良いでしょう。「私は … がほしいが、… となっていてほしい」。Position の場合にこれを使うと「私はこの位置がほしいが、n 歩左に移動(n steps to the left)していてほしい」となるので、toTheLeft()
が適切なメソッドと言えそうです。
final class Position
{
// ...
public function toTheLeft(int steps): Position
{
// ...
}
}
このテンプレートに従うと、「with」という単語を使ったり、いわゆる過去形の分詞形容詞(たとえば “multiplied” のように動詞の過去形をした形容詞のこと)を使うことが多くなるでしょう。たとえば「私はこの量が欲しいが、n 倍にしてほしい(multiplied n times)」です。これらは宣言的な名前です。なにをしてほしいか指示するものではなく、捜査の結果どうなっていてほしいかを「宣言」しているからです。
また良い名前を探すときは、技術的な名前ではなく、ドメインに特化した抽象度の高い名前を選ぶようにしましょう。たとえば、withDecreasedBy()
の代わりに toTheLeft()
という名前を選択しましたが、抽象度が異なります。
5 章:オブジェクトの使用 #
本章の内容
- テンプレートを使ったメソッドの記述
- メソッド引数と戻り値の検証
- メソッド内部での失敗への対応
まとめ
- メソッドを実装するためのテンプレートは、作業を開始する前に作業場をきれいにすることを目的としています。まず、渡された引数を分析し、おかしいと思われるものは例外を投げて拒否します。そして、実際の作業を行い、失敗があればそれに対処します。そして最後に、クライアントに値を返します。
- InvalidArgumentException は、クライアントが渡した引数に問題があることを知らせるために使用されるべきです。RuntimeException は、論理的な間違いではない問題が発生したことをクライアントに知らせるために使用されるべきです。
- カスタム例外クラスと名前月コンストラクタを定義して、例外メッセージの質を向上させ、例外を作成し投げることを容易にしましょう。
6 章:情報の取得 #
本章の内容
- 情報を取得するためのクエリメソッドの使用
- 単一で特定の戻り値型の使用
- 内部データをオブジェクト内にとどめるための設計
- クエリメソッド呼び出しに対する抽象の導入
- クエリメソッド呼び出しに対するテストダブルの使用
まとめ
- クエリメソッドは、情報の断片を取得するために使用できるメソッドです。クエリメソッドは単一の型の戻り値を持つべきです。null を返してもよいですが、null オブジェクトや空リストを返すなど、代替となる手段を探すようにしましょう。代わりに例外を投げることもできます。クエリメソッドは、オブジェクトの内部をできるだけ露出させないようにしましょう。
- 質問したいこと、得たい答えのそれぞれについて、特化したメソッドと戻り値を定義しましょう。質問に対する答えがシステムの境界を超えることでしか得られない場合は、これらのメソッドに対する抽象(実装の詳細を含まないインターフェイス)を定義しましょう。
- 情報を取得するクエリを使用するサービスをテストする場合、それらを自分で書いたフェイクやスタブに置き換えましょう。ただし、クエリメソッドが実際に呼び出されていることをテストしないように注意しましょう。
コマンドクエリ分離原則 #
メソッドは常にコマンドメソッドかクエリメソッドのどちらかであるべきというルールに従いましょう。これはコマンドクエリ分離原則(command/query separation principle, CQS)と呼ばれます。
- Command Query Separation | Martin Fowler
- https://www.martinfowler.com/bliki/CommandQuerySeparation.html
Counter の実装はこの原則に従っています。
final class Counter
{
private int count = 0;
public function increment(): void
{
this.count++;
}
public function currentCount(): int
{
return this.count;
}
}
increment()
はコマンドメソッド、currentCount()
はクエリメソッドで、Counter のどのメソッドもコマンドメソッドとクエリメソッドの両方であることはありません。
オブジェクトをイミュータブルにしましょう。アプリケーションのほぼすべてのオブジェクトは、エンティティを除いて可能な限りイミュータブルにすべきです。
Counter がイミュータブルオブジェクトとして実装された場合、increment()
はモディファイアメソッドになります。その場合、メソッド名は incremented()
とした方が、より宣言的でよいでしょう。
final class Counter
{
private int count = 0;
public function incremented(): Counter
{
copy = clone this;
copy.count++;
return copy;
}
public function currentCount(): int
{
return this.count;
}
}
モディファイアメソッドは、コマンドメソッドなのかクエリメソッドなのか
モディファイアメソッドは、あなたが求めている情報を返す訳ではありません。実際にはオブジェクト全体のコピーを返します。そのコピーを手に入れたら、ようやくオブジェクトに情報を問い合わせることができます。つまりモディファイアメソッドはクエリメソッドではありません。しかし伝統的なコマンドメソッドでもないのです。イミュータブルオブジェクトに対するコマンドメソッドは、オブジェクトの状態を変更することを示唆しますが、実際に変更はしません。これは新しいオブジェクトを生成します。その点で、単にクエリに答えているのと大きな違いはありません。
7 章:タスクの実行 #
本章の内容
- コマンドメソッドによるタスクの実行
- イベントとイベントリスナによる大きなタスクの分割
- コマンドメソッドでの失敗の処理
- コマンドメソッド呼び出しに対する抽象の導入
- コマンドメソッド呼び出しに対するテストダブルの作成
まとめ
- コマンドメソッドはタスクを実行するために使用されます。コマンドメソッドの名前は命令形(「Do this」「Do that」)にし、多くのことをやりすぎないようにしましょう。メインの仕事とそうではない仕事を区別しましょう。イベントを発行して、他のサービスに追加のタスクを実行してもらいましょう。タスクを実行する際、コマンドメソッドは必要な情報を収集するためにクエリメソッドを呼び出す事もできます。
- サービスは、外部からだけでなく内部からもイミュータブルであるべきです。データを取得するサービスと同様に、タスクを実行するサービスも何度も再利用可能であるべきです。タスクの実行中に何か問題が発生した場合は、(それが分かり次第すぐに)例外を投げましょう。
- システム境界を超えるコマンド(リモートサービスやデータベースなどにアクセスするコマンド)の抽象を定義しましょう。コマンドメソッド自身がコマンドメソッドを呼び出すテストでは、モックやスパイを使用してこれらのメソッドの呼び出しをテストできます。モッキングツールを使用することもできますし、自分でスパイを作成することもできます。
8 章:責務の分離 #
本章の内容
- リードモデルとライトモデルの区別
- リードモデルとライトモデルそれぞれのリポジトリの定義
- ユースケースに特化したリードモデルの設計
- イベントや共有データソースからのリードモデルの構築
まとめ
- ドメインオブジェクトでは、ライトモデルとリードモデルを分離しましょう。エンティティからデータを取得することだけに関心のあるクライアントは、状態を変更できるメソッドを公開するエンティティではなく、専用のオブジェクトを使用する必要があります。
- リードモデルはライトモデルから直接作成することもできますが、より効率的な方法は、ライトモデルで使用されているデータソースから作成することでしょう。それが不可能な場合、または効率的な方法でリードモデルを作成できない場合は、ドメインイベントを使用してリードモデルを構築することを検討しましょう。
ライトモデルとリードモデルを分離する #
オブジェクトの種類にはサービスとそのほかのオブジェクトがあります。これらのほかのオブジェクトの中には、エンティティとして特徴付けられるものがあります。エンティティは特定のドメイン概念をモデル化します。その際、エンティティは関連するデータを含み、そのデータを有効かつ意味のある方法で操作する方法を提供します。エンティティはデータを公開することもでき、公開された内部データ(注文日など)や計算データ(注文の合計金額など)から、クライアントは情報を取得できます。
実際には、クライアントによってエンティティの使い方はさまざまです。あるクライアントはコマンドメソッドでエンティティのデータを操作したいかもしれませんし、あるクライアントはクエリメソッドで情報の一部を取得したいだけかもしれません。とはいえ、これらのクライアントはすべて同じオブジェクトを共有しており、たとえそのクライアントにとっては不要なメソッドやアクセスすべきでないメソッドであっても、潜在的にはすべてのメソッドにアクセスできます。
変更可能なエンティティを、変更することが許されていないクライアントに渡してはいけません。たとえクライアントが今のところはそれを変更していなかったとしても、ある日突然変更するようになるかもしれませんし、そうなったら何が起こったのかを見つけるのは難しいでしょう。そのため、エンティティの設計を改善するために最初にすべきことは、ライトモデルとリードモデル(コマンドモデルとクエリモデルと呼ばれることもある)を分離することです。
例として PurchaseOrder エンティティを使って、どのように分離できるかを見ていきましょう。発注書(purchase order)とは、ある企業があるサプライヤーから製品を購入することを表すものです。購入した製品を受け取ると、その製品はその企業の倉庫に保管されます。この時点から、その企業はその製品の在庫を持つことになります。
// なお、簡略化のためプリミティブ型の値を使用しているが、実際にはバリューオブジェクトの利用が望ましい。
final class PurchaseOrder
{
private int purchaseOrderId;
private int productId;
private int orderedQuantity;
private bool was Received;
private function __construct()
{
}
public static function place(
int purchaseOrderId,
int productId,
int orderedQuantity
): PurchaseOrder {
purchase Order = new PurchaseOrder();
purchaseOrder.productId = productId;
purchaseOrder.orderedQuantity = orderedQuantity;
purchaseOrder.wasReceived = false;
return purchaseOrder;
}
public function markAsReceived(): void
{
this.wasReceived = true;
}
public function purchaseOrderID(): int
{
return this.purchaseOrderId;
}
public function productId(): int
{
return this.productId;
}
public function orderedQuantity(): int
{
return this.orderedQuantity:
}
public function wasReceived(): bool
{
return this.wasReceived;
}
}
現在の実装では、PurchaseOrder エンティティは、エンティティを作成・操作するメソッド(place()
および markAsRedeived()
)と、エンティティから情報を取得するメソッド(productId()
、orderedQuantity()
および wasReceived()
)を公開しています。
次に、さまざまなクライアントがこのエンティティをどのように使用するかを見てみましょう。まず、コントローラから ReceiveItems サービスが呼び出され、生の注文書 ID が渡されます。
final class ReceiveItems
{
private PurchaseOrderRepository repository;
public function __construct(PurchaseOrderRepository repository)
{
this.repository = repository;
}
public function receiveItems(int purchaseOrderId): void
{
purchaseOrder = this.repository.getById(purchaseOrderId);
purchaseOrder.markAsReceived();
this.repository.save(purchaseOrder);
}
}
このサービスは、PurchaseOrder のゲッタを一切使用していないことに注目してください。このサービスは、エンティティの状態を変更することのみに関心があります。
次に、この企業が持っている製品の在庫数量の詳細を示す JSON エンコードされたデータ構造をレンダリングするコントローラを見てみましょう。
final class StockReportController
{
private PurchaseOrderRepository repository;
public function __construct(PurchaseOrderRepository repository)
{
this.repository = repository;
}
public function execute(Request request): Response
{
allPurchaseOrders = this.repository.findAll();
stockReport = [];
foreach(allPurchaseOrders as purchaseOrder) {
if (!purchaseOrder.wasReceived()) {
continue;
}
if (!isset(stockReport[purchaseOrder.productId()])) {
stockReport[purchaseOrder.productId()] = 0;
}
stockReport[purchaseOrder.productId()] += purchaseOrder.orderedQuantity;
}
return newJsonResponse(stockReport);
}
}
このコントローラは PurchaseOrder に何の変更も加えません。ただ、すべての発注書に関する情報の一部を必要としているだけです。言い換えれば、エンティティの書き込み部分には興味がなく、読み取り部分だけに興味があるのです。クライアントが必要とする以上の振る舞いを公開することは望ましくないという事実に加え、ある製品の在庫量を調べるために、常にすべての発注書をループすることはあまり効率的ではありません。
これに対する解決策は、エンティティの責務を分割することです。まず、発注書に関する情報を取得するために使用できる新しいオブジェクトを作成することにします。これを PurchaseOrderFOrStockReport と呼ぶことにしましょう。
final class PurchaseOrderForStockReport
{
private int productId;
private int orderedQuantity;
private bool wasReceived;
public function __construct(
int productId,
int orderedQuantity,
bool wasReceived
) {
this.productId = productId;
this.orderedQuantity = orderedQuantity;
this.wasReceived = wasReceived;
}
public function productId(): ProductId
{
return this.productId;
}
public function orderedQuantity(): int
{
return this.orderedQuantity;
}
public function wasReceived(): bool
{
return this.wasReceived;
}
}
この新しい PurchaseOrderForStockReport オブジェクトは、それを提供できるリポジトリがあればすぐにコントローラ内で使用できます。手っ取り早い解決策は、PurchaseOrder がその内部データに基づいて PurchaseOrderForStockReport のインスタンスを返すようにすることです。
final class PurchaseOrder
{
private int purchaseOrderId;
private int productId;
private int orderedQuantity;
private bool was Received;
// ...
public function forStockReport(): PurchaseOrderForStockReport
{
return new PurchaseOrderForStockReport(
this.productId,
this.orderedQuantity,
this.wasReceived
)
}
}
final class StockReportController
{
private PurchaseOrderRepository repository;
public function __construct(PurchaseOrderRepository repository)
{
this.repository = repository;
}
public function execute(Request request): Response
{
allPurchaseOrders = this.repository.findAll();
forStockReport = array_map(
function (PurchaseOrder purchaseOrder) {
return purchaseOrder.forStockReport();
},
allPurchaseOrders
)
// ...
}
}
これで、元の PurchaseOrder エンティティから、ほとんどすべてのクエリメソッド(productId()
、orderedQuantity()
、wasReceived()
)を削除することが可能になりました。これにより、PurchaseOrder エンティティは適切なライトモデルになります。つまり PurchaseOrder エンティティは、情報を得たいだけのクライアントからは使われなくなりました。
これらのクエリメソッドを削除しても、先に見た ReceiveItems サービスのように、このオブジェクトをライトモデルとして使用する PurchaseOrder の既存のクライアントには何の害もありません。
クエリメソッドが禁止なわけではない
クライアントの中には、エンティティをライトモデルとして使用しても、そこから何らかの情報も取得する必要があるものがあります。こういったクライアントは意思決定をしたり、特別な検証のためにこの情報を必要とします。このような場合にクエリメソッドを追加してはいけないと思う必要はありません。クエリメソッドは決して禁止されているわけではありません。ポイントは、情報を取得するためだけにエンティティを使用するクライアントは、ライトモデルではなく、専用のリードモデルを使用すべきだということです。
ユースケースに特化したリードモデルを作成する #
ここまでで PurchaseOrder エンティティをライトモデルとリードモデルに分割しました。ライトモデルは古い名前のままですが、リードモデルは PurchaseOrderForStockReport と名付けました。ForStockReport という修飾語は、このオブジェクトが特定の目的のためにあることを示しています。このオブジェクトは、ユーザーにとって有用な在庫レポートを作成するためにデータを整えるという、非常に特化した文脈での使用に適しています。先に示した解決策はまだ最適とは言えません。なぜなら、コントローラはまだすべての PurchaseOrder エンティティをロードし、forStockReport()
を呼び出して PurchaesOrderForStockRport インスタンスに変換しなければならないからです。つまり、クライアントはライトモデルにまだアクセスしていることを意味します。これでは、ライトモデルにアクセスしないようにするという当初の目標を達成できていません。
この設計にはほかにも適切でない面があります。PurchaseOrderForStockReport オブジェクトがあるにもかかわらず、ユーザーにデータを表示する前に、それらをループして別のデータ構造を構築する必要があるのです。もし、私たちが意図する使い方と一致する構造を持つオブジェクトがあったらどうでしょうか?このオブジェクトの名前については、リードモデルの名前(ForStockReport)にすでにヒントがあります。そこで、この新しいオブジェクトを StockReport と呼び、すでに存在すると仮定しましょう。そうすると、コントローラはよりシンプルになります。
final class StockReportController
{
private StockReportRepository repository;
public function __construct(StockReportRepository repository)
{
this.repository = repository;
}
public function execute(Request request): Response
{
stockReport = this.repository.getStockReport();
return new JsonResponse(stockReport.asArray()); // asArray() は、先ほど手動で作成したものと同じような配列を返すことが期待される。
}
}
StockReport のほかにも、アプリケーションの特定のユースケースに対応するリードモデルをいくつでも作成できます。たとえば、発注書のリストを作るためだけに使用するリードモデルを作成できます。このモデルでは、ID と作成日だけを公開するでしょう。そのうえで、ユーザーが情報の一部を更新できるようなフォームをレンダリングするために必要なすべての詳細を提供する別のリードモデルを用意する、といった具合です。
9 章:サービスの振る舞いの変更 #
本章の内容
- コードを変更せずに振る舞いを変更すること
- 振る舞いを設定および交換可能にすること
- コンポジションとデコレーションを可能にする抽象の導入
- オブジェクトの振る舞いをオーバーライドする継承の回避
- オブジェクトの乱用を防ぐためにクラスを final に、メソッドを private にすること
まとめ
- サービスの振る舞いを変更する必要がある場合は、コンストラクタ引数で振る舞いを設定できる方法を探しましょう。より大きなロジックを変える必要があり、この方法が使えない場合は、コンストラクタ引数として渡される依存関係を置き換える方法を探しましょう。
- 変更したい振る舞いがまだ依存関係として表現されていない場合は、より高レベルの概念とインタフェースによる抽象を導入することで依存関係を抽出しましょう。そうすることで、変更するのではなく、置き換えることができる部分を手に入れることができます。抽象によって、振る舞いを合成したり、装飾したりできるようになり、元のサービスがそれを知らなくても(あるいはそのために変更されなくても)、より複雑なことができるようになります。
- 継承を使って、サービスのメソッドをオーバーライドすることで振る舞いを変更するのはやめましょう。常にオブジェクトコンポジションを使用した解決策を探しましょう。実際には、すべてのクラスを完全に継承できないようにしましょう。クラスは final とし、クラスのパブリックインタフェースの一部でない限り、すべてのプロパティとメソッドを private にしましょう。
10 章:オブジェクトフィールドガイド #
本章の内容
- 典型的な Web アプリケーションで見かけるさまざまな種類のオブジェクト
- 異なる種類のオブジェクトがどのように連携するか
- これらのオブジェクトがどのアプリケーションレイヤに存在するか
- これらのレイヤはどのように関連しているか
まとめ
- アプリケーションのフロントコントローラは、受け取ったリクエストをコントローラのいずれかに転送します。これらのコントローラは、アプリケーションのインフラストラクチャレイヤの一部であり、受け取ったデータをどのようにアプリケーションサービスやリードモデルリポジトリの呼び出しに変換するかを知っています。アプリケーションサービスやリードモデルリポジトリはどちらもアプリケーションレイヤの一部です。
- アプリケーションサービスは、ユーザーからのリクエストがどのように来たかには関係なく、Web アプリケーションでもコンソールアプリケーションでも同じように簡単に使用できます。アプリケーションサービスは、アプリケーションのユースケースのひとつと考えられているタスクを実行します。その過程で、ライトモデルリポジトリからエンティティを取り出し、そのメソッドを呼び出し、変更された状態を保存することがあります。エンティティ自体は、そのバリューオブジェクトを含めて、ドメインレイヤの一部です。
- リードモデルリポジトリは、情報を取得するために使用できるサービスです。リードモデルリポジトリは、あるユースケースに特化し、必要十分な情報を提供するリードモデルを返します。
- さまざまな種類からなるオブジェクト群は、自然とレイヤを形成します。コードが下位のレイヤのコードのみに依存するレイヤシステムは、ドメインとアプリケーションのコードをアプリケーションのインフラストラクチャ面から分離する方法を提供します。