はじめに
最近、「ちょうぜつソフトウェア設計入門」を読んでいるので、本書の中で自分が疑問に感じたポイントをQ&A形式で振り返ってみたいと思います。
この記事では、各セクションの簡単な説明の後に、[Q]で疑問に感じたこと、[A]で調べてわかったことをまとめています。
なお、サンプルコードにPHPを使用していますが、言語を問わず広く有効な設計手法なので、他言語でも応用できる内容になっています。
第8章については、すでに別の記事にまとめているので、こちらもぜひご覧ください!
それでは、第9章を振り返りながら、一緒に「ソフトウェア設計」について学んでいきましょう!
Entity
CatやDog、あるいはCarなど、プログラム内の情報のうち、実世界に存在するものに対応するオブジェクトがエンティティ(Entity)です。
存在すると言っても、物質的に存在するものだけでなく、「予約」や「参加」といった、形のない実世界の情報も含みます。
エンティティを表すクラスは、アプリケーションで何を扱いたいのかを表す主要なオブジェクトとして、アーキテクチャの中心、ドメインモデルに含まれる場合が多くあります。
乱暴な言い方をすれば、アプリケーションはエンティティの属性を管理するためのプログラムにすぎないと言えます。
[NOTE]
エンティティにとって非常に重要なのが、インスタンスの一意性です。
同一人物のインスタンスが同時に2つ存在してはいけません。
データベースのレコードを読み込んだときは、プライマリキーが同じレコードのエンティティをメモリ上に複製生成しないように気をつけないといけません。
[Q]
データベースのレコードを読み込んだとき、Laravel では「プライマリキーが同じエンティティをメモリ上に複製しない」ことは保証されているの?
[A]
基本的には保証されていません。
Laravel(Eloquent)は DDD のエンティティ一意性(Identity Map)をフレームワークとしては持っていません。
[Q]
同じプライマリキーのレコードを2回取得したら、どうなるの?
[A]
別々のインスタンスが生成されます。
$user1 = User::find(1);
$user2 = User::find(1);
$user1 === $user2; // false
👉 id=1 という同じレコードでも
👉 メモリ上では別オブジェクト
[Q]
それって「同一人物のインスタンスが2つ存在する」状態じゃないの?
[A]
はい、その通りです。
ただし Laravel は DDD のエンティティモデルではなく「Active Record」モデルを採用しています。
- Eloquent = DBレコード操作が主目的
- 同一性の厳密な管理はアプリ側の責務
[Q]
じゃあ Laravel では問題にならないの?
[A]
多くの CRUD アプリでは問題になりにくいです。
理由:
- リクエスト単位でオブジェクトは破棄される
- 同一リクエスト内で複雑なドメイン操作をあまりしない
- 更新は最終的に DB に保存される
👉 短命なインスタンス前提の設計
[Q]
逆に「気をつけるべき」ケースは?
[A]
- 複雑なドメインロジックがある
- 同一エンティティを何度も操作する
- 状態の一貫性が重要
👉 DDD 的な Repository / Entity 設計を検討すべき
Service
DIコンテナ内で依存チェーンの要素になる静的モジュールは、サービス(Service)と名付けられています。
もちろん、実際にDIコンテナを使うか使わないかにかかわらず、それに相当する役目のものならすべてサービスです。
[NOTE]
サービスは疑似的なシングルトンなので、状態を持ちません。
可変部分はコンストラクタ引数だけです。
サービスの持つプロパティは、プログラムの実行中に変わることなく、コンフィギュレーションの時点で決定されています。
[Q]
「状態を持たない」とはどういうこと?
[A]
メンバ変数にビジネス上の状態を保持しない、という意味です。
// 静的なモジュール(Service)
class PriceCalculator
{
public function calculate(int $price, int $taxRate): int
{
return $price + ($price * $taxRate);
}
}
- 内部に「現在の価格」などを保持しない
- 入力 → 処理 → 出力だけ
[Q]
逆に「静的ではない」ものは?
[A]
状態を持つもの=エンティティや集約です。
class Order
{
private int $total;
public function addItem(int $price): void
{
$this->total += $price;
}
}
-
totalという状態を保持 - 時間と操作で変化する
👉 これはサービスではない
[Q]
「静的なモジュール(Service)」は 1個もプロパティを持たない ってこと?
[A]
いいえ。そうとは限りません。
ポイントは「ビジネス上の状態かどうか」です。
Factory
ファクトリ(Factory)はご存じのとおり、オブジェクトを生成するオブジェクトです。
原則に従えば、低いレイヤーのサービスを利用する上位のサービスは、先にインターフェースに依存して完結させておくのがセオリーです。
下位レイヤーは上位のインターフェースを実装する形でサービス内容を提供します。
いつの間にか、抽象と具象に分かれたAbstract Factoryパターンが発生します。
Abstract Factoryはわざわざ使おうとすると面倒なパターンですが、一度「サービスとしてのファクトリ」を認識すると、とてもカジュアルなパターンになります。
[NOTE]
原則に従えば、低いレイヤーのサービスを利用する上位のサービスは、先にインターフェースに依存して完結させておくのがセオリーです。
上位レイヤー(ユースケース・アプリケーションサービス)は
「どうやって作るか」ではなく
「何が欲しいか」だけを知る
つまり:
class OrderService
{
public function __construct(
UserFactory $userFactory // ← インターフェース
) {}
}
- 具体クラスを知らない
- 先に「契約(interface)」を決める
Repository
リポジトリ(Repository)は特殊なファクトリサービスで、エンティティの提供に特化したものです。
通常のファクトリは、使えば使うだけオブジェクトが生成されます。
同じ意味のオブジェクトでも平気で生成するので、そこから生まれてくるデータはたいてい、バリューオブジェクトになってきます。
[NOTE]
しかし、エンティティの生成では、絶対にインスタンスの一意性が担保されないといけないので、単純なファクトリは使えません。
ファクトリが行うのが「生成」なのに対して、リポジトリが行うのは「取り出し(ただし実体がまだなければ生成)」です。
ビジネスロジックにリポジトリを注入することで、ビジネスロジックは、エンティティがオンメモリなのかまだディスクにあるのか、また、一意なオブジェクトインスタンスなのか、といった煩わしさから解放されます。
[Q]
「エンティティがオンメモリなのかまだディスクにあるのか」とは、何を指しているの?
[A]
そのエンティティが、すでにメモリ上に存在しているのか、
それともまだデータベース(ディスク)にあって、これから読み込む必要があるのか、という状態の違いを指しています。
[Q]
Laravel で Repository を作る意味があるのはどんなとき?
[A]
同一エンティティを何度も扱う
- 状態遷移が重要
- ビジネスルールが複雑
- インスタンス一意性を意識したい
👉 DDD 的設計をしたいとき
[Q]
Laravel で Repository を書く、とは具体的に何をすること?
[A]
Eloquent を直接ビジネスロジックから触らせず、
「エンティティ取得専用の窓口」を作ることです。
- DB 取得
- 生成
- キャッシュ(一意性)
これらを Repository に閉じ込める、という意味です。
[Q]
Repository の interface はどうなる?
[A]
ビジネスロジックが知りたいことだけを書く。
interface UserRepository
{
public function find(UserId $id): User;
}
- Eloquent
- DB
- キャッシュ
👉 一切出てこない
[Q]
実装クラスはどうなる?
[A]
Eloquent を使って「取り出す」責務だけを持つ。
class EloquentUserRepository implements UserRepository
{
private array $identityMap = [];
public function find(UserId $id): User
{
if (isset($this->identityMap[$id->value])) {
return $this->identityMap[$id->value];
}
$model = UserModel::findOrFail($id->value);
$entity = User::fromModel($model);
return $this->identityMap[$id->value] = $entity;
}
}
ここでやっていること:
- すでにメモリにあるか?
- なければ DB から読む
- 同じ ID なら同じインスタンスを返す
👉 インスタンス一意性を担保
おわりに
今回は、「ちょうぜつソフトウェア設計入門」の第9章を題材に、自分が疑問に感じたポイントを紹介しました。
普段はあまり意識していなかった設計の役割を、みなさん自身のプロジェクトに当てはめて考えてみてもらえたら嬉しいです!
今後も読み進めながら、気づきがあればこうした形でまとめていきたいと思います!