まえおき
単純な問い合わせはレポジトリに実装すれば良いが、複雑なクエリの場合、QueryServiceを使うというのはレポジトリのアンチパターン を読んでる人には周知の事実だと思うのだけど、現在出向している会社のSさん(若いのにとても頭が良くて優秀)に話したらこんな質問が。
「それだとドメイン知識の流出にならないのですか?」
ちゃんと答えを返せなかったのでちゃんと調べてみようと思い、メモとしてまとめたものになります。
QueryServiceとは
ドメイン駆動設計におけるQueryServiceは複雑なデータの問い合わせや読み取り専用の操作を担うもの。
QueryServiceはビジネスロジックから独立しており、複雑なクエリやデータ変換、複数の集約からのデータ結合など、より高度な読み取り操作を行うために使用するもの。
レポジトリは一般的に、集約の永続化や再構築を行う役割を持ち、単純なCRUD操作に特化するもの。
レポジトリに関して補足
レポジトリでの検索は単純な検索操作も担う事は問題ない。
例としてはIDによる基本的なクエリ検索などである。例えば特定のIDを持つエンティティを取得するfindById
メソッドや特定の属性に基づく単純な検索メソッドなどがそれにあたる。
QueryServiceをサンプルコードで表すと以下のようになる
<?php
class UserQueryService {
private PDO $database;
public function __construct(PDO $database) {
$this->database = $database;
}
public function findUsersByCriteria(array $criteria): array {
// ここでSQLクエリを組み立てる
$query = "SELECT * FROM users WHERE ";
$conditions = [];
$params = [];
foreach ($criteria as $field => $value) {
$conditions[] = "$field = :$field";
$params[":$field"] = $value;
}
$query .= implode(" AND ", $conditions);
$stmt = $this->database->prepare($query);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
UserQueryService
クラスでは、コンストラクタでPDOデータベース接続を受け取り、findUsersByCriteria
メソッドでユーザーを検索するための条件を受け取っている。
このメソッドでは動的にSQLクエリを組み立て、PDOを使用してデータベースに問い合わせを行う。
QueryServiceからの戻り値をエンティティや値オブジェクトに入れるかどうかは以下の点を考慮して判断する。
複雑性とパフォーマンス
複雑なクエリからのデータをエンティティや値オブジェクトにマッピングするとオーバヘッドが増え、パフォーマンスに影響を与える事がある。特に、読み取り専用の操作やレポート生成などの場合、生のデータを直接使用する方が効率的である。
一貫性と表現力
エンティティや値オブジェクトを使用すると、ビジネスロジックがより一貫性を持ち、ドメインモデルを明確に表現できる場合もある。取得したデータをビジネスルールに適用する必要がある場合は、使用する事でコードの可読性と保守性が増すので使用する価値あり。
用途とコンテキスト
データの最終的な用途によっても異なる。
例えばユーザーインタフェースに直接表示するためのデータであれば、エンティティや値オブジェクトへの変換は必ずしも必要ではない。一方で、取得したデータに対して追加のビジネスロジックを適用する必要がある場合、エンティティや値オブジェクトを使用する方が良い。
QueryServiceはどの層に入るのか、の前に
この話をするにはまず認識を合わせる必要がある。それは
ドメイン駆動設計(DDD)とクリーンアーキテクチャは別物ですよ。
ドメイン駆動設計(DDD)とクリーンアーキテクチャには以下の違いがある。
ドメイン駆動設計(DDD)
焦点: DDDは、複雑なビジネスロジックと要件を扱うソフトウェアの設計に焦点を当てている。ビジネスドメインの専門知識をソフトウェア設計の中心に置き、そのドメイン内での言語(ユビキタス言語)やモデルを重視する。
目的: ビジネスロジックとデータの複雑性を理解し、それを反映したソフトウェアモデルを構築することに重点を置く。
クリーンアーキテクチャ
焦点: クリーンアーキテクチャは、ソフトウェアの全体構造に関する設計原則であり、フレームワークやデータベース、UIなど外部要素からの独立性を目指す。
目的: ソフトウェアシステムの柔軟性、保守性、テスト容易性を向上させることに焦点を当てている。依存関係のルール(依存関係逆転の原則など)を用いて、外部要素からの影響を受けにくい設計を目指す。
ちなみに二つを併用する事は可能ってか大体の人はそうしてるはず。
ドメイン駆動設計(DDD)がビジネスとドメインモデリングに焦点を当てるのに対し、クリーンアーキテクチャはシステムの構造と依存関係の管理に注目するので、お互いのアプローチを組み合わせる事でビジネスの複雑さを適切に扱いつつ、保守性と拡張性を高める事が可能となる。
QueryServiceをクリーンアーキテクチャの観点から見た場合に考慮すべき点
ドメイン層の独立性
QueryServiceはドメイン層の独立性を維持するように設計されるべき。
QueryServiceがドメインのビジネスルールや複雑なドメインロジックを漏らす事なく、単にデータを取得する機能に限定されるべき。
インターフェースの定義
QueryServiceのインターフェースはドメイン層で定義されることが望ましく、具体的な実装はインフラ層で行う。
こうする事で、ドメイン層はインフラ層の実装詳細から切り離され、依存関係の逆転が達成できる。
ドメイン知識の保護
QueryServiceを用いる際は、ドメインモデルの詳細やビジネスロジックが外部に漏れないよう注意する必要がある。
QueryServiceはデータの読み取りやクエリ処理に特化しているべきであり、ビジネスルールやドメインロジックの処理は行ってはいけない。
上記を踏まえた上でクリーンアーキテクチャにおけるQueryServiceはアプリケーション層、またはインフラ層に位置づけられる。
アプリケーション層での役割やユースケースに応じたデータの取得を調整する事であり、インフラ層では具体的なデータベース操作やクエリ実行の実装を行う。
ソースによる違い
ドメイン駆動設計(DDD)の場合
// ドメイン層 - エンティティの例
class Product {
private int $id;
private string $name;
private float $price;
// コンストラクタ、ゲッター、セッターなど
}
// レポジトリのインターフェース(ドメイン層)
interface ProductRepository {
public function findById(int $id): Product;
public function save(Product $product): void;
}
// インフラストラクチャ層 - レポジトリの実装
class SqlProductRepository implements ProductRepository {
// findByIdとsaveの実装
}
// ドメイン層 - インターフェース
interface ProductQueryService {
public function findProductsByCriteria(array $criteria): array;
}
// インフラストラクチャ層 - 実装
class SqlProductQueryService implements ProductQueryService {
public function findProductsByCriteria(array $criteria): array {
// SQLクエリを実行して結果を返す
}
}
ドメイン駆動設計(DDD)ではQueryServiceはドメイン層とインフラ層の間のインターフェースとして機能し、主に読み取り専用のクエリを扱う
クリーンアーキテクチャの場合
// エンティティ(ドメイン層)
class Product {
private int $id;
private string $name;
private float $price;
// コンストラクタ、ゲッター、セッターなど
}
// インターフェース(アプリケーション層)
interface ProductRepository {
public function findById(int $id): Product;
public function save(Product $product): void;
}
// インフラストラクチャ層 - リポジトリの実装
class SqlProductRepository implements ProductRepository {
// findByIdとsaveの実装
}
// アプリケーション層 - インターフェース
interface ProductQueryService {
public function findProductsByCriteria(array $criteria): array;
}
// インフラストラクチャ層 - 実装
class SqlProductQueryService implements ProductQueryService {
public function findProductsByCriteria(array $criteria): array {
// SQLクエリを実行して結果を返す
}
}
// ユースケース(アプリケーション層)
class CreateProductUseCase {
private ProductRepository $repository;
public function __construct(ProductRepository $repository) {
$this->repository = $repository;
}
public function execute(string $productName): User {
$product = new Product(0, $productName);
$this->repository->save($product);
return $product;
}
}
クリーンアーキテクチャではQueryServiceはアプリケーション層とインフラ層の間で機能し、依存関係逆転の法則に従う。
結論
ドメイン駆動設計におけるQueryServiceはインターフェースはドメイン層に属し、実装はインフラ層に属する。
クリーンアーキテクチャにおけるQueryServiceはインターフェースはアプリケーション層に属し、実装はインフラ層に属する。
なのでドメインの流出にはならないよ。って答えになってるかな?
ちなみに、エンティティがデータベースのテーブルレコードと対応する事が多いから勘違いしやすいけど、テーブル名などの情報はドメイン層に属するではなく、インフラ層に属するもの。
レポジトリがエンティティを取り扱うから勘違いしやすいけど。
エンティティに関しても記事書いてみるか。良いね次第だけど。