社内でドメイン駆動設計入門の読書会 #2
こんにちは。Nonです。
今回も会社で読書会をしている話をしようと思います。
内容は控えめに、ディスカッションの内容重視で書いていきたいと思います。内容が気になる方は購入しましょう!
読んでいる本
読んでいる本はこちらのドメイン駆動設計です。
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本
以前の記事でも言っていたように、業務でDDDを利用して開発することが多くなったのですが、DDDに精通している人が少ないという問題がありました。
そこで、その精通している人が読書会をしようかと誘ってくださいまして、是非にと参加させていただきました。
進行方法
読書会の進行方法は
- 今回読書の対象にする章を決める。
- 20分間その章を読む
- 読み終わってしまった人は、もう一周読み直すか、次の章に進んでもらう
- その後40分間で、その章に対する疑問や考え方をディスカッションする
- 1〜3を毎週定期的に行う
という進行方向となっています。
社内で読書会をするのはこれが初めてなので、進行方法はもっといいのあったら教えて下さい。
今回読んだ内容
第2章の値オブジェクトです。
- 値オブジェクトとは
- 値の性質と値オブジェクトの実装
- 不変である
- 不変のメリット
- 交換が可能である
- 等価性によって比較される
- 値オブジェクトにする基準
- 振る舞いをもった値オブジェクト
- 定義できないからこそわかること
- 不変である
- 値オブジェクトを採用するモチベーション
- 表現力を増す
- 不正な値を存在させない
- 誤った代入を防ぐ
- ロジックの散在を防ぐ
- まとめ
この本で一番ページのある章だったはず、、、
それくらい大事な内容ということですね。
ディスカッション
値オブジェクト書いたことある?
僕はありませんでした。
しかし、リファクタリング入門
でクラス型についてすでに知っており、ValueObject(以下VO)ではなく、所謂クラス型として採用してきました。
例えば、
class ProductId
{
private $id;
public function setId(int $productId): void
{
$this->id = $productId;
}
public function getId(): int
{
return (int)$this->id;
}
}
こんな感じで変数に型を付けるために採用したことはありました。
しかしこの本ではsetter
が無く、値を再代入することができないように作ることで、不変性をもたせるということでした。
class ProductId
{
private $id;
public function __construct(int $productId)
{
$this->id = $productId;
}
public function toInt(): int
{
return (int)$this->id;
}
}
このように書くことで、ProductId
のインスタンスを生成するときにしか、値をセットすることができないので、不変となります。一度作ったProductId
はこれ以降信頼できる変数として使用することができますね。
振る舞いをもつってどういうこと?
VOなので、振る舞いをもつ値、変数ということに疑問を持った方の質問でした。
上で書いた
class ProductId
{
private $id;
public function __construct(int $productId)
{
$this->id = $productId;
}
public function toInt(): int
{
return (int)$this->productId;
}
}
にも値を返すtoInt
しか存在しませんね。
回答ととして出た例として、弊社スマレジではPOSを開発しております。
POSレジなので、税抜、税込のデータは必須となっています。このようなとき、VOを使えば簡単だよねという話でした。
class ProductPrice
{
/** @var int $price */
private $price;
/** @var Tax $tax */
private $tax;
/** @var TaxDivision $taxDivision */
private $taxDivision;
public function __construct(
int $productPrice,
Tax $tax,
TaxDivision $taxDivision
) {
$this->price = $productPrice;
$this->tax = $tax;
$this->taxDivision = $taxDivision;
}
// ~~省略
public function toIncludedTax(): int
{
$price = $this->price;
if (!$this->taxDivision->isIncludeTax()) {
$price = $price * $this->tax->toDecimal();
}
return floor($price);
}
}
上記のコードはかなり省いていますが、商品の各項目のVOを用意して商品型VO
に対して、注入を行います。
こうすればProduct
は不変で税込、税抜を考慮した値段を取得できるふるまいを持つクラスとなります。
更に、
class ProductPrice
{
/** @var int $price */
private $price;
/** @var Tax $tax */
private $tax;
/** @var TaxDivision $taxDivision */
private $taxDivision;
public function __construct(
int $productPrice,
Tax $tax,
TaxDivision $taxDivision
) {
$this->price = $productPrice;
$this->tax = $tax;
$this->taxDivision = $taxDivision;
}
// ~~省略
public function toIncludedTax(): int
{
$price = $this->price;
if (!$this->taxDivision->isIncludeTax()) {
$price = $price * $this->tax->toDecimal();
}
return floor($price);
}
public function addPriceIncludedTax(ProductPrice $price): int
{
return $this->toIncludedTax() + $price->toIncludedTax();
}
}
とすれば、他の商品単価を税込か税抜を意識せずに価格計算をすることができます。
// 軽減税率で税抜価格
$lunchPrice = new ProductPrice(750, new Tax(8), new TaxDivision(0));
// 標準税率で税込価格
$teaPrice = new ProductPrice(150, new Tax(10), new TaxDivision(1));
// 税込価格の合計金額を取得
$sum = $lunchPrice->addPriceIncludedTax($teaPrice);
税計算まわりはバグを生みやすい箇所なので、VOの採用でそれが軽減できるならかなりいいものですね。
バリデーションについて
あなたは「何回」入力チェックをしているだろうか?で記事を書きましたが、バリデーションは多階層に渡って実装すべき処理です。
VOに採用することで、更にセキュアな処理が実行できそうですね。
<?php
class ProductPrice
{
/** @var int $price */
private $price;
/** @var Tax $tax */
private $tax;
/** @var TaxDivision $taxDivision */
private $taxDivision;
public function __construct(
int $productPrice,
Tax $tax,
TaxDivision $taxDivision
) {
$this->validate($productPrice);
$this->price = $productPrice;
$this->tax = $tax;
$this->taxDivision = $taxDivision;
}
// ~~省略
public function validate(int $price): void
{
if (is_null($price)) {
throw new InvalidArgumentException('Price is required.');
}
if (!is_int($price)) {
throw new InvalidArgumentException('Price must be integer.');
}
}
}
Request
でバリデーションValueObject
でバリデーション- ビジネスロジックでバリデーション
この3つを必ず行うことで、リリース前にかなりのバグを滅ぼすことができそうです。
一度実装したらVOを使い回すことになるので忘れなどの防止になりそうですね。
class CreateProductAction extends Controller
{
private $productRepository;
public function __construct(ProductRepository $productRepository)
{
$this->productRepository = $productRepository;
}
// CreateProductRequestでバリデーション
public function __invoke(CreateProductRequest $request)
{
// ProductPriceでバリデーション
$price = new ProductPrice((int)$this->request->getPrice());
// 登録時にバリデーション
$this->productRepository->registerPrice();
// ~~省略
}
}
いつVOにするか?その粒度は?
もう1文字ずつVOしちゃえば良くない???
いや、よくない。
ドメインに必要な分だけをVOにすればいいのではとディスカッションで結論づきました。本でも出てきていましたが、例えば、
class FirstName
{
private $firstName;
public function __construct(string $firstName)
{
$this->firstName = $firstName;
}
}
class LastName
{
private $lastName;
public function __construct(string $lastName)
{
$this->lastName = $lastName;
}
}
class FullName
{
/** @var FirstName $firstName */
private $firstName;
/** @var LastName $lastName */
private $lastName;
public function __construct(FirstName $firstName, LastName $lastName)
{
$this->firstName = $firstName;
$this->lastName = $lastName;
}
}
例えばこのコードを使用するアプリが、姓と名を別々に考えるアプリで、さらにフルネームとしても使う場合は、上記3つのクラスは必要かと思います。
しかし、フルネームをユーザー名として、姓名関係無く使用する場合は、FirstName
/ LastName
クラスは必要無いでしょう。
最後に
ディスカッションで出たのはこんなものでしょうか?
全体的にやはりVOについてはポジティブにとらえていて、同実装すべきか、どこまでVOにすべきかと言った議論となっていました。
僕の個人開発プロジェクトでも、勉強がてら導入していますので、今後も試行錯誤していきたいと思います。
次回の読書回の内容も書く予定です。
その時はよしなに。
.