ドメイン駆動設計(Domain Driven Design)
ドメインとは
- 商品を検索する
- 商品をカートに入れる
- 商品を購入する
など、アプリケーションを通じてユーザーが実際に取る行動、もしくは行動したいという要求(関心事)
ドメインモデル
- エンティティ
- 値オブジェクト
- サービス
ドメインモデルのライフサイクル管理
- アグリゲート
- ファクトリー
- リポジトリー
エンティティ
一意となる要素を保持するオブジェクト
- ユーザー
- カート
- 商品
など
値オブジェクト
保持した値が決して変更されない不変のオブジェクト
- 都道府県情報
- 郵便番号情報
- 決済代行サービス
など
サービス
エンティティでも値オブジェクトでもない処理を持つオブジェクト
- 検索処理
など
ファクトリー
- オブジェクトの生成をカプセル化
リポジトリー
- 永続化されたオブジェクトへのアクセス手段を提供
アグリゲート
- エンティティ間の依存関係を保持
- 関連する複数のエンティティに対して処理が必要な場合に命令を受け取る
ドメインモデルをCakePHP3で表現
CakePHP3のModel
CakePHPはバージョン3になって2種類のモデルに分割
- Tablesクラス
- Entityクラス
While Table Objects represent and provide access to a collection of objects, entities represent individual rows or domain objects in your application.
テーブルオブジェクトはオブジェクトの集合へのアクセスを可能としており、エンティティは個々の行やドメインオブジェクトを表している。
ドキュメントにもあるようにドメインオブジェクトはEntityクラスを継承する。
ポイント
- 各ドメインモデルはEntityクラスを継承
- リポジトリーはTableクラスを継承
- リポジトリーはドメインモデルではないため
- CakePHP3のQuery BuilderはTableクラスが持つため
商品を検索する処理
UserEntity
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\ORM\Entity\ProductSearchService;
class UserEntity extends Entity
{
public function productSearch($keyword)
{
$productSearchService = new ProductSearchService();
$productSearchService->keyword = $keyword;
return $productSearchService->searchByUser();
}
}
ProductSearchService
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\ORM\TableRegistry;
class ProductSearchService extends Entity
{
private $keyword;
public function setKeyword($keyword)
{
$this->keyword = $keyword;
}
public function getKeyword()
{
return $this->keyword;
}
public function searchByUser()
{
$productRepository = TableRegister::get('ProductRepository');
return $productRepository->searchByUser($this);
}
}
ProductRepository
namespace App\Model\Table;
use Cake\ORM\Table;
class ProductRepository extends Table
{
public function searchByUser($productSearchService)
{
$keyword = $productSearchService->keyword;
return $this->find()
->where(['id' => $keyword])
->orWhere(['product_name' => $keyword])
->all();
}
}
判断に悩んだ点
-
ProductSearchService
は必要か-
ProductRepository
の生成やsearchByUser
の呼び出しはUserEntity
から行えばよいという考えもあったが、検索項目を保持する何らかのサービスドメインが必要であると判断した。厳密には、キーワードは対象となる全てのProduct
プロパティにセットしたり、価格帯は相当する値オブジェクトを作成したりするなど、各検索項目に最適なドメインクラスを作成すればProductSearchService
は必要ないと思われる。仕様が複雑さを増すと共に前記の変更は必要であり、このような状況に応じた設計の判断を求められることが、ドメイン駆動は変化し続けることが重要と言われる所以のひとつであろう。
-
-
ProductSearchService
やProductRepository
を生成するファクトリーは必要か- ファクトリーの役割はドメインを生成することよりも、生成する際の前処理(複数のドメインの生成や初期値の設定など)が複雑な場合、それらの処理をまとめておく(カプセル化)のに利用するものであるため、今回の簡単な例題では必要ないと判断した。
商品をカートに入れる
UserEntity
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\ORM\TableRegistry;
class UserEntity extends Entity
{
public function putProductInCart($selectedProductId)
{
$product = new ProductEntity();
$product->id = $selectedProductId;
$productRepository = TableRegister::get('ProductRepository');
$selectedProduct = $productRepository->getSelectedProductByUser($product);
$cart = new Cart();
$cart-> setProduct($selectedProduct);
}
}
CartEntity
namespace App\Model\Entity;
use Cake\ORM\Entity;
class CartEntity extends Entity
{
private $products;
public function setProduct($product)
{
$this->products[] = $product;
}
}
ProductEntity
namespace App\Model\Table;
use Cake\ORM\Table;
class ProductEntity extends Entity
{
private $id;
public function setId($id)
{
$this->id = $id;
}
}
ProductRepository
namespace App\Model\Table;
use Cake\ORM\Table;
class ProductRepository extends Table
{
public function getSelectedProductByUser($product)
{
return $this->find()
->where{['id' => $product->id])
->first();
}
}
判断に悩んだ点
-
PutProductInCartService
は必要か- 検索処理とは異なり、カートに入れる商品情報(ここでは商品ID)は
Product
プロパティとしてセットでき、ProductRepository
から対象の商品情報を取得できるため、必要ないと判断した。
- 検索処理とは異なり、カートに入れる商品情報(ここでは商品ID)は
まとめ
- 設計に非常に時間がかかる
- 判断に基準が必要
- クラス図必須
- コードレビュー必須
- テストクラス必須