実際のDDD。 ウィッシュリストデザイン

インターネット上には、散在するDDD素材がかなりあります。 青い本は別として、これらは主に同じ本から引き出された理論を持つ短い記事であり、実践とほとんど重複していません。 もちろん、見栄えが悪いだけかもしれませんが、「from and to」と呼ばれるものの堅実な例を見つけたいとずっと思っていました。 そして、Symfony 3とVueJSでそのような例を作成することにしました。 私は最近DDDを研究しているとすぐに言いたいので、かなり単純な主題分野であるウィッシュリストを取り上げました。







ウィッシュリスト



誰もが何かを買う。 新しい電話でも、プレゼントでも、海外旅行でも、アパートでも。 「ハード」貯金箱への追加としてのウィッシュリストは、各欲求の累積資金を追跡し、これらの資金を常に増加させるように設計されています。 今日、新しいラップトップ用に貯金を開始することに決めたとします:欲望を追加し、お金を節約し始めます。 そして明日、私は毎日どれくらいのお金を節約する必要があるかを計算したいので、6か月後に妻に良い贈り物を買うことができます。







欲望



私たちが考えている欲求は、お金のために何かを買うことで満たすことができます。 各欲求には、 初期欲求(リストに欲望を追加する前に貯金を開始した場合)、および蓄積された資金(すべての預金の合計で表される基金 )があります。 預金は、特定の欲求に対する一時金です。 欲望は定期的な投資を必要とするので、それ以下では預金額ができない基本レートを決定することは良いことです。 さらに、必要に応じて預金を引き出すことができるように、どのような欲求でも預金を追跡できる必要があります。 十分な量の資金が蓄積されると、欲求が満たされます。 過剰な資金がある場合は、他の希望に再分配することができます(詳細については、次の記事のいずれかを参照してください)。







エンティティの設計



上記の要件に基づいて、 Wish



(希望)とDeposit



(寄稿)の2つのエンティティをエンコードできます。







欲望:エンティティコンストラクター



欲望から始めて、必要なフィールドとエンティティコンストラクターの設計方法について考えてみましょう。 最初に頭に浮かぶのは、次のコードのようなものです。







 <?php namespace Wishlist\Domain; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; class Wish { private $id; private $name; private $price; private $fee; private $deposits; private $initialFund; private $createdAt; private $updatedAt; public function __construct( string $name, int $price, int $fee, int $initialFund ) { $this->name = $name; $this->price = $price; $this->fee = $fee; $this->initialFund = $initialFund; $this->deposits = new ArrayCollection(); $this->createdAt = $createdAt ?? new DateTimeImmutable(); $this->updatedAt = $createdAt ?? new DateTimeImmutable(); } }
      
      





ただし、いくつかの問題があります。







  1. 使用済みの代理キー
  2. フィールド検証がありません
  3. 検証がコンストラクターにエンコードされると、さらに巨大になります
  4. 決済が行われる通貨に関する情報はありません
  5. コンストラクターは引数でオーバーロードされます


私たちは何をするつもりですか? 解決策があります-値オブジェクトを使用します。 次に、エッセンスのコンストラクターを次のように変換します。







 <?php namespace Wishlist\Domain; use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; class Wish { private $id; private $name; private $expense; private $deposits; private $published = false; private $createdAt; private $updatedAt; public function __construct( WishId $id, WishName $name, Expense $expense, DateTimeImmutable $createdAt = null ) { $this->id = $id; $this->name = $name; $this->expense = $expense; $this->deposits = new ArrayCollection(); $this->createdAt = $createdAt ?? new DateTimeImmutable(); $this->updatedAt = $createdAt ?? new DateTimeImmutable(); } }
      
      





次の3つの値オブジェクトを使用しました。







  1. WishId



    WishId



    / uuidライブラリを使用して生成されたUUID



    です
  2. WishName



    欲望の名前
  3. Expense



    、欲求に対する「支出」を表します:コスト、基本料金、初期資金(最良の名前ではないかもしれませんが、私は別のものを思いつきませんでした)


あなたは尋ねる:なぜその作成日がエンティティのコンストラクターに入ったのか? これはテストの記述を容易にするために行われ、テスト以外の場所では使用されません。 もちろん、最善の解決策ではないでしょう。







さて、値オブジェクトを使用したので、それらの実装を見てみるといいでしょう。 最初に、識別子の実装方法について考えてみましょう(先を見据えて、 WishId



に加えてWishId



WishId



することにします)。 これを行うには、そのうちの1つを例として使用して簡単なテストを作成します(本質は同じであるため、2つの異なるテストを作成しても意味がありません)。







 <?php namespace Wishlist\Tests\Domain; use Wishlist\Domain\WishId; use PHPUnit\Framework\TestCase; class IdentityTest extends TestCase { public function testFromValidString() { $string = '550e8400-e29b-41d4-a716-446655440000'; $wishId = WishId::fromString($string); static::assertInstanceOf(WishId::class, $wishId); static::assertEquals($string, $wishId->getId()); static::assertEquals($string, (string) $wishId); } public function testEquality() { $string = '550e8400-e29b-41d4-a716-446655440000'; $wishIdOne = WishId::fromString($string); $wishIdTwo = WishId::fromString($string); $wishIdThree = WishId::next(); static::assertTrue($wishIdOne->equalTo($wishIdTwo)); static::assertFalse($wishIdTwo->equalTo($wishIdThree)); } }
      
      





これらのテストに基づいて、一般的なロジックを含む識別子の基本クラスを作成できます。







 <?php namespace Wishlist\Domain; use Ramsey\Uuid\Exception\InvalidUuidStringException; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Wishlist\Domain\Exception\InvalidIdentityException; abstract class AbstractId { protected $id; private function __construct(UuidInterface $id) { $this->id = $id; } public static function fromString(string $id) { try { return new static(Uuid::fromString($id)); } catch (InvalidUuidStringException $exception) { throw new InvalidIdentityException($id); } } public static function next() { return new static(Uuid::uuid4()); } public function getId(): string { return $this->id->toString(); } public function equalTo(AbstractId $id): bool { return $this->getId() === $id->getId(); } public function __toString(): string { return $this->getId(); } }
      
      





そして、「本当の」識別子はそれを「取り除く」ためだけです。







 <?php namespace Wishlist\Domain; final class WishId extends AbstractId { // }
      
      





DepositIdでも同じです:







 <?php namespace Wishlist\Domain; final class DepositId extends AbstractId { // }
      
      





次に、 WishName



検討しWishName



。 これは最も単純な値オブジェクトであり、空でない名前のみが必要です。 テストを書くことから始めましょう:







 <?php namespace Wishlist\Tests\Domain; use Wishlist\Domain\WishName; use PHPUnit\Framework\TestCase; class WishNameTest extends TestCase { /** * @expectedException \InvalidArgumentException */ public function testShouldNotCreateWithEmptyString() { new WishName(''); } public function testGetValueShouldReturnTheName() { $expected = 'A bucket of candies'; $name = new WishName($expected); static::assertEquals($expected, $name->getValue()); static::assertEquals($expected, (string) $name); } }
      
      





それでは、実際にWishName



ましょう。 ちなみに、以降のチェックでは、非常に便利なwebmozart / assertライブラリを使用ます。







 <?php namespace Wishlist\Domain; use Webmozart\Assert\Assert; final class WishName { private $name; public function __construct(string $name) { Assert::notEmpty($name, 'Name must not be empty.'); $this->name = $name; } public function getValue(): string { return $this->name; } public function __toString(): string { return $this->getValue(); } }
      
      





次に、より興味深い値オブジェクトExpense



ましょう。 コスト、基本レート、および初期資金の正しい値を制御するように設計されています。 要件を定義することで、これを支援します。







  1. コストには正の数のみを指定できます。
  2. 同じことが基本レートにも当てはまります。
  3. 指定されている場合、開始資金を負の数にすることはできません


さらに、プロパティには次の制限が適用されます。







  1. 基本料金は費用よりも低くする必要があります
  2. 開始資金も費用よりも少なくなければなりません


通貨も必要なので、「裸の」 intを使用してお金を処理するのではなく、 moneyphp / moneyライブラリを使用します。 Expense



に関する上記のすべてを考慮して、次のテストを作成します。







 <?php namespace Wishlist\Tests\Domain; use Money\Currency; use Money\Money; use Wishlist\Domain\Expense; use PHPUnit\Framework\TestCase; class ExpenseTest extends TestCase { /** * @expectedException \InvalidArgumentException * @dataProvider nonsensePriceDataProvider */ public function testPriceAndFeeMustBePositiveNumber($price, $fee, $initialFund) { Expense::fromCurrencyAndScalars(new Currency('USD'), $price, $fee, $initialFund); } public function nonsensePriceDataProvider() { return [ 'Price must be greater than zero' => [0, 0, 0], 'Fee must be greater than zero' => [1, 0, 0], 'Price must be positive' => [-1, -1, 0], 'Fee must be positive' => [1, -1, 0], 'Initial fund must be positive' => [2, 1, -1], ]; } /** * @expectedException \InvalidArgumentException */ public function testFeeMustBeLessThanPrice() { Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 150); } /** * @expectedException \InvalidArgumentException */ public function testInitialFundMustBeLessThanPrice() { Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 50, 150); } /** * @expectedException \InvalidArgumentException */ public function testNewPriceMustBeOfTheSameCurrency() { $expense = Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 50, 25); $expense->changePrice(new Money(200, new Currency('RUB'))); } public function testChangePriceMustReturnANewInstance() { $expense = Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 50, 25); $actual = $expense->changePrice(new Money(200, new Currency('USD'))); static::assertNotSame($expense, $actual); static::assertEquals(200, $actual->getPrice()->getAmount()); } /** * @expectedException \InvalidArgumentException */ public function testNewFeeMustBeOfTheSameCurrency() { $expense = Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 50, 25); $expense->changeFee(new Money(200, new Currency('RUB'))); } public function testChangeFeeMustReturnANewInstance() { $expense = Expense::fromCurrencyAndScalars(new Currency('USD'), 100, 10, 25); $actual = $expense->changeFee(new Money(20, new Currency('USD'))); static::assertNotSame($expense, $actual); static::assertEquals(20, $actual->getFee()->getAmount()); } }
      
      





他のすべてに加えて、彼らは欲望のコストと基本レートを変更する可能性が含まれています。 したがって、このクラスには、通貨のコンプライアンスに関する2つの追加テストが含まれています。







これでExpense



をエンコードできます:







 <?php namespace Wishlist\Domain; use Money\Currency; use Money\Money; use Webmozart\Assert\Assert; final class Expense { private $price; private $fee; private $initialFund; private function __construct(Money $price, Money $fee, Money $initialFund) { $this->price = $price; $this->fee = $fee; $this->initialFund = $initialFund; } public static function fromCurrencyAndScalars( Currency $currency, int $price, int $fee, int $initialFund = null ) { foreach ([$price, $fee] as $argument) { Assert::notEmpty($argument); Assert::greaterThan($argument, 0); } Assert::lessThan($fee, $price, 'Fee must be less than price.'); if (null !== $initialFund) { Assert::greaterThanEq($initialFund, 0); Assert::lessThan($initialFund, $price, 'Initial fund must be less than price.'); } return new static( new Money($price, $currency), new Money($fee, $currency), new Money($initialFund ?? 0, $currency) ); } public function getCurrency(): Currency { return $this->price->getCurrency(); } public function getPrice(): Money { return $this->price; } public function changePrice(Money $amount): Expense { Assert::true($amount->getCurrency()->equals($this->getCurrency())); return new static($amount, $this->fee, $this->initialFund); } public function getFee(): Money { return $this->fee; } public function changeFee(Money $amount): Expense { Assert::true($amount->getCurrency()->equals($this->getCurrency())); return new static($this->price, $amount, $this->initialFund); } public function getInitialFund(): Money { return $this->initialFund; } }
      
      





そこで、 Wish



エンティティで使用されるすべての値オブジェクトを調べ、コンストラクターを決定したので、今度はビジネスロジックに直接移動します。







欲望:お金を節約する



普通の貯金箱を想像してください。 そこに、特定の額面と通貨の硬貨または紙片を入れます。 すなわち 貯金箱への寄付が行われています。 貯金箱がいっぱいになると壊れます。 だから、それは私たちの欲求にあります:私たちは彼らに一定量のお金を投資し、十分な量が蓄積されると、私たちは欲求が満たされたと信じています(あなたは店に行くことができます:)、したがって、それにさらに貢献することは意味がありません。 もう1つの小さな制限があります。寄付は、希望が公開された場合にのみ行うことができます(たとえば、より良い時期まで延期できます)。







もう一度テストを書きます。







 <?php namespace Wishlist\Tests\Domain; use DateInterval; use DateTimeImmutable; use Money\Currency; use Money\Money; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Wishlist\Domain\DepositId; use Wishlist\Domain\Expense; use Wishlist\Domain\Wish; use Wishlist\Domain\WishId; use Wishlist\Domain\WishName; class WishTest extends TestCase { /** * @expectedException \Wishlist\Domain\Exception\DepositIsTooSmallException */ public function testMustDeclineDepositIfItIsLessThanFee() { $wish = $this->createWishWithPriceAndFee(1000, 100); $wish->publish(); $wish->deposit(new Money(50, new Currency('USD'))); } public function testExtraDepositMustFulfillTheWish() { $wish = $this->createWishWithPriceAndFund(1000, 900); $wish->publish(); $wish->deposit(new Money(150, new Currency('USD'))); static::assertTrue($wish->isFulfilled()); } /** * @expectedException \Wishlist\Domain\Exception\WishIsUnpublishedException */ public function testMustNotDepositWhenUnpublished() { $wish = $this->createWishWithEmptyFund(); $wish->deposit(new Money(100, new Currency('USD'))); } /** * @expectedException \Wishlist\Domain\Exception\WishIsFulfilledException */ public function testMustNotDepositWhenFulfilled() { $fulfilled = $this->createWishWithPriceAndFund(500, 450); $fulfilled->publish(); $fulfilled->deposit(new Money(100, new Currency('USD'))); $fulfilled->deposit(new Money(100, new Currency('USD'))); } public function testDepositShouldAddDepositToInternalCollection() { $wish = $this->createWishWithEmptyFund(); $wish->publish(); $depositMoney = new Money(150, new Currency('USD')); $wish->deposit($depositMoney); $deposits = $wish->getDeposits(); static::assertCount(1, $deposits); static::assertArrayHasKey(0, $deposits); $deposit = $deposits[0]; static::assertTrue($deposit->getMoney()->equals($depositMoney)); static::assertSame($wish, $deposit->getWish()); } /** * @expectedException \InvalidArgumentException */ public function testDepositAndPriceCurrenciesMustMatch() { $wish = $this->createWishWithEmptyFund(); $wish->publish(); $wish->deposit(new Money(125, new Currency('RUB'))); } private function createWishWithEmptyFund(): Wish { return new Wish( WishId::next(), new WishName('Bicycle'), Expense::fromCurrencyAndScalars( new Currency('USD'), 1000, 100 ) ); } private function createWishWithPriceAndFund(int $price, int $fund): Wish { return new Wish( WishId::next(), new WishName('Bicycle'), Expense::fromCurrencyAndScalars( new Currency('USD'), $price, 10, $fund ) ); } }
      
      





テストを機能させるために、 Wish



エンティティにいくつかのメソッドを追加します。







 <?php namespace Wishlist\Domain; // ... //    use   use Wishlist\Domain\Exception\DepositIsTooSmallException; use Wishlist\Domain\Exception\WishIsFulfilledException; use Wishlist\Domain\Exception\WishIsUnpublishedException; // ... public function deposit(Money $amount): Deposit { $this->assertCanDeposit($amount); $deposit = new Deposit(DepositId::next(), $this, $amount); $this->deposits->add($deposit); return $deposit; } private function assertCanDeposit(Money $amount) { if (!$this->published) { throw new WishIsUnpublishedException($this->getId()); } if ($this->isFulfilled()) { throw new WishIsFulfilledException($this->getId()); } if ($amount->lessThan($this->getFee())) { throw new DepositIsTooSmallException($amount, $this->getFee()); } Assert::true( $amount->isSameCurrency($this->expense->getPrice()), 'Deposit currency must match the price\'s one.' ); } public function isFulfilled(): bool { return $this->getFund()->greaterThanOrEqual($this->expense->getPrice()); } public function publish() { $this->published = true; $this->updatedAt = new DateTimeImmutable(); } public function unpublish() { $this->published = false; $this->updatedAt = new DateTimeImmutable(); } public function getFund(): Money { return array_reduce($this->deposits->toArray(), function (Money $fund, Deposit $deposit) { return $fund->add($deposit->getMoney()); }, $this->expense->getInitialFund()); }
      
      





これらすべての方法を1つずつ考えてみましょう。







  1. deposit



    が可能かどうかをチェックし、可能であれば、指定された金額の欲求に貢献します。 これを行うには、Depositエンティティオブジェクトを作成し、内部の預金のコレクションに保存します。
  2. isFulfilled



    ウィッシュが満たされているかどうかを示します。 まあ、私たちは以前、その蓄積が価値以上である場合、欲求は満たされていると判断することを決定しました。
  3. publish/unpublish



    -それに応じて公開または下書きします。
  4. getFund



    ファンド、つまり 蓄積された資金。


Wish::deposit



メソッドは同じ名前のエンティティを使用することに注意してください。 ここで、欲求のビジネスロジックをさらに発展させ続けるために、 Deposit



エンティティをプログラムする必要があります。 幸いなことに、これははるかに簡単で時間もかからないようにしましょう。







デポジット:コンストラクター



コントリビューションには4つのプロパティのみがあります。







  1. これはエンティティであり、預金を管理できる必要があるため、識別子
  2. この貢献が関係する欲求
  3. 入金額
  4. 預金作成日


想像上のお金を貯金箱に入れたように無意味であり、手にお金に相当するおもちゃさえないので、貢献がゼロになることはないことも考慮する必要があります:)。







いつものように、テストから始めましょう:







 <?php namespace Wishlist\Tests\Domain; use Mockery; use Money\Currency; use Money\Money; use PHPUnit\Framework\TestCase; use Wishlist\Domain\Deposit; use Wishlist\Domain\DepositId; use Wishlist\Domain\Wish; class DepositTest extends TestCase { /** * @expectedException \InvalidArgumentException */ public function testDepositAmountMustNotBeZero() { $wish = Mockery::mock(Wish::class); $amount = new Money(0, new Currency('USD')); new Deposit(DepositId::next(), $wish, $amount); } }
      
      





このテストでは、欲望を完全に説明しないように、 mockery / mockery libraryを使用しました。 貢献自体のロジックに興味があります。 Wish::deposit



メソッドで行われるのと同様に、 Deposit



コンストラクターで要求チェックを行う必要があるかどうかを議論する理由があります。 Deposit



エンティティはどこでも直接使用されないため、記事で検討されるすべての預金操作はWish



エンティティでのみ実行されます。







結果は次のような単純なエンティティです。







 <?php namespace Wishlist\Domain; use DateTimeImmutable; use DateTimeInterface; use Money\Money; use Webmozart\Assert\Assert; class Deposit { private $id; private $wish; private $amount; private $createdAt; public function __construct(DepositId $id, Wish $wish, Money $amount) { Assert::false($amount->isZero(), 'Deposit must not be empty.'); $this->id = $id; $this->wish = $wish; $this->amount = $amount; $this->createdAt = new DateTimeImmutable(); } public function getId(): DepositId { return $this->id; } public function getWish(): Wish { return $this->wish; } public function getMoney(): Money { return $this->amount; } public function getDate(): DateTimeInterface { return $this->createdAt; } }
      
      





欲望:寄付を撤回する



Deposit



の本質をDeposit



、プログラミングの欲求に戻ることができます。 タスクの条件に応じて、私たちは欲望のためにお金を蓄積するだけでなく、すでに行われた寄付を撤回することもできます。 たとえば、そのうちの1つが誤って作成された場合。







当然、最初にいくつかのテストをWishTest



クラスに追加します。







 /** * @expectedException \Wishlist\Domain\Exception\WishIsUnpublishedException */ public function testMustNotWithdrawIfUnpublished() { $wish = $this->createWishWithPriceAndFund(500, 0); $wish->publish(); $deposit = $wish->deposit(new Money(100, new Currency('USD'))); $wish->unpublish(); $wish->withdraw($deposit->getId()); } /** * @expectedException \Wishlist\Domain\Exception\WishIsFulfilledException */ public function testMustNotWithdrawIfFulfilled() { $wish = $this->createWishWithPriceAndFund(500, 450); $wish->publish(); $deposit = $wish->deposit(new Money(100, new Currency('USD'))); $wish->withdraw($deposit->getId()); } /** * @expectedException \Wishlist\Domain\Exception\DepositDoesNotExistException */ public function testWithdrawMustThrowOnNonExistentId() { $wish = $this->createWishWithEmptyFund(); $wish->publish(); $wish->withdraw(DepositId::next()); } public function testWithdrawShouldRemoveDepositFromInternalCollection() { $wish = $this->createWishWithEmptyFund(); $wish->publish(); $wish->deposit(new Money(150, new Currency('USD'))); $wish->withdraw($wish->getDeposits()[0]->getId()); static::assertCount(0, $wish->getDeposits()); }
      
      





ご覧のとおり、預金の引き出しの制限は、預金を作成するために私たちが書いたものと似ています。 必要なロジックをウィッシュクラスに追加します。







 <?php namespace Wishlish\Domain; // <...> public function withdraw(DepositId $depositId) { $this->assertCanWithdraw(); $deposit = $this->getDepositById($depositId); $this->deposits->removeElement($deposit); } private function assertCanWithdraw() { if (!$this->published) { throw new WishIsUnpublishedException($this->getId()); } if ($this->isFulfilled()) { throw new WishIsFulfilledException($this->getId()); } } private function getDepositById(DepositId $depositId): Deposit { $deposit = $this->deposits->filter( function (Deposit $deposit) use ($depositId) { return $deposit->getId()->equalTo($depositId); } )->first(); if (!$deposit) { throw new DepositDoesNotExistException($depositId); } return $deposit; }
      
      





彼らが言うように、理解できない状況では、処刑を投げます! withdraw



メソッドは非常に単純であることが判明しましたが、問題のすべての条件を考慮しました。







  1. そうでない寄付を撤回することはできません
  2. 要望がドラフトであるか、すでに満たされている場合、これを行うことはできません


欲求:余剰貯蓄を計算する



この機能は実際には最も重要ではありませんが、ある時点で在庫を補充するのに適切な量がないことが判明した場合に作成されましたが、大きなものがあります。 まあ、または、たとえば、かなりの量の欲求を脇に置いて、利用可能な資金の量を追わずに単に「逃した」場合。 実際、余剰の計算は簡単です。欲求の値から資金を引き、絶対値を取ります。 差が正の場合、余剰はゼロに等しいと見なすことができます。







WishTest



クラスWishTest



新しいテストWishTest



追加します。







 public function testSurplusFundsMustBe100() { $wish = $this->createWishWithPriceAndFund(500, 300); $wish->publish(); $wish->deposit(new Money(100, new Currency('USD'))); $wish->deposit(new Money(200, new Currency('USD'))); $expected = new Money(100, new Currency('USD')); static::assertTrue($wish->calculateSurplusFunds()->equals($expected)); } public function testSurplusFundsMustBeZero() { $wish = $this->createWishWithPriceAndFund(500, 250); $wish->publish(); $wish->deposit(new Money(100, new Currency('USD'))); $expected = new Money(0, new Currency('USD')); static::assertTrue($wish->calculateSurplusFunds()->equals($expected)); }
      
      





前述のテストおよび書面によるテストに基づいて、このメソッドをWish



の本質で記述できます。







 <?php namespace Wishlist\Domain; // <...> public function calculateSurplusFunds(): Money { $difference = $this->getPrice()->subtract($this->getFund()); return $difference->isNegative() ? $difference->absolute() : new Money(0, $this->getCurrency()); }
      
      





欲望:実行日を打ち消す



願いが叶う日を予測する方法は2つあります。







  1. コストと基本料金に基づく
  2. 価値と蓄積された資金に基づく


最初の方法は簡単です。コストを基本レートで除算し、その結果、資金の蓄積に費やされる日数を取得します。 受け取った日を現在の日付に追加します。ここに、欲求が満たされた日付があります。







2番目の方法はもう少し複雑です。 蓄積された資金をコストから減算し、結果の差を基本レートで除算します。これにより、希望が満たされるまでの日数が得られ、現在の日付に加算されて希望の日付が取得されます。







アルゴリズムがあり、テストを作成します。







 public function testFulfillmentDatePredictionBasedOnFee() { $price = 1500; $fee = 20; $wish = $this->createWishWithPriceAndFee($price, $fee); $daysToGo = ceil($price / $fee); $expected = (new DateTimeImmutable())->add(new DateInterval("P{$daysToGo}D")); static::assertEquals( $expected->getTimestamp(), $wish->predictFulfillmentDateBasedOnFee()->getTimestamp() ); } public function testFulfillmentDatePredictionBasedOnFund() { $price = 1500; $fund = 250; $fee = 25; $wish = $this->createWish($price, $fee, $fund); $daysToGo = ceil(($price - $fund) / $fee); $expected = (new DateTimeImmutable())->add(new DateInterval("P{$daysToGo}D")); static::assertEquals( $expected->getTimestamp(), $wish->predictFulfillmentDateBasedOnFund()->getTimestamp() ); }
      
      





テストが緑色に変わるために、私たちは与えられたアルゴリズムに従って欲求の充足の日付の計算をプログラムします:







 public function predictFulfillmentDateBasedOnFee(): DateTimeInterface { $daysToGo = ceil( $this->getPrice() ->divide($this->getFee()->getAmount()) ->getAmount() ); return $this->createFutureDate($daysToGo); } public function predictFulfillmentDateBasedOnFund(): DateTimeInterface { $daysToGo = ceil( $this->getPrice() ->subtract($this->getFund()) ->divide($this->getFee()->getAmount()) ->getAmount() ); return $this->createFutureDate($daysToGo); } private function createFutureDate($daysToGo): DateTimeInterface { return (new DateTimeImmutable())->add(new DateInterval("P{$daysToGo}D")); }
      
      





欲望:状態を変える



この段階でエンコードする必要があるのは、欲望の状態を変えるいくつかの簡単な方法だけです。







  1. 下書きへの公開と投稿
  2. 価値の変化
  3. 基本預金レートの変更


私なしでは、最初にこの全体の下でWishTest



クラスのテストを書くと推測したと思います。 最初の公開:







 public function testPublishShouldPublishTheWish() { $wish = $this->createWishWithEmptyFund(); $updatedAt = $wish->getUpdatedAt(); $wish->publish(); static::assertTrue($wish->isPublished()); static::assertNotSame($updatedAt, $wish->getUpdatedAt()); } public function testUnpublishShouldUnpublishTheWish() { $wish = $this->createWishWithEmptyFund(); $updatedAt = $wish->getUpdatedAt(); $wish->unpublish(); static::assertFalse($wish->isPublished()); static::assertNotSame($updatedAt, $wish->getUpdatedAt()); }
      
      





エンティティメソッドはテストと同じくらい簡単です:







 <?php namespace Wishlist\Domain; // <...> class Wish { // <...> public function publish() { $this->published = true; $this->updatedAt = new DateTimeImmutable(); } public function unpublish() { $this->published = false; $this->updatedAt = new DateTimeImmutable(); } public function isPublished(): bool { return $this->published; } // <...> }
      
      





, . :







 public function testChangePrice() { $wish = $this->createWishWithPriceAndFee(1000, 10); $expected = new Money(1500, new Currency('USD')); $updatedAt = $wish->getUpdatedAt(); static::assertSame($updatedAt, $wish->getUpdatedAt()); $wish->changePrice($expected); static::assertTrue($wish->getPrice()->equals($expected)); static::assertNotSame($updatedAt, $wish->getUpdatedAt()); } public function testChangeFee() { $wish = $this->createWishWithPriceAndFee(1000, 10); $expected = new Money(50, new Currency('USD')); $updatedAt = $wish->getUpdatedAt(); static::assertSame($updatedAt, $wish->getUpdatedAt()); $wish->changeFee($expected); static::assertTrue($wish->getFee()->equals($expected)); static::assertNotSame($updatedAt, $wish->getUpdatedAt()); }
      
      





:







 <?php namespace Wishlist\Domain; // <...> class Wish { // <...> public function changePrice(Money $amount) { $this->expense = $this->expense->changePrice($amount); $this->updatedAt = new DateTimeImmutable(); } public function changeFee(Money $amount) { $this->expense = $this->expense->changeFee($amount); $this->updatedAt = new DateTimeImmutable(); } // <...> }
      
      





, , . :







  1. Wish::deposit(Money $amount)



  2. Wish::withdraw(DepositId $depositId)



  3. Wish::predictFulfillmentDateBasedOnFee()



    Wish::predictFulfillmentDateBasedOnFund()



  4. Wish::publish()



    Wish::unpublish()



  5. Wish::changePrice(Money $amount)



    Wish::changeFee(Money $amount)





- , , Wish:







 <?php namespace Wishlist\Domain; interface WishRepositoryInterface { public function get(WishId $wishId): Wish; public function put(Wish $wish); public function slice(int $offset, int $limit): array; public function contains(Wish $wish): bool; public function containsId(WishId $wishId): bool; public function count(): int; public function getNextWishId(): WishId; }
      
      





, , . 頑張って







PS: . , . , « » :)








All Articles