Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Final Class Aggregate

Final Class Aggregate

Presented at PHP fwdays'20 online conference

pelshoff

May 30, 2020
Tweet

More Decks by pelshoff

Other Decks in Programming

Transcript

  1. @pelshoff $today = Date::today(); $yesterday = $today->previousDay(); $lastYear = $today->previousYear();

    $request = $request->withHeader('x-pim-is', 'awesome'); $fiftyCents = Money::EUR(50); $oneEuro = $fiftyCents->multiply(Amount::fromFloat(2.0)); https://twitter.com/Pelshoff/status/1266012117230071808 for many more examples!
  2. @pelshoff $today = Date::today(); $yesterday = $today->previousDay(); $lastYear = $today->previousYear();

    $request = $request->withHeader('x-pim-is', 'awesome'); $fiftyCents = Money::EUR(50); $oneEuro = $fiftyCents->multiply(Amount::fromFloat(2.0));
  3. @pelshoff $yesterday = $today->previousDay(); $otherDay = $today->withDay(32); $bluesDay = Date::fromString('My

    favorite color is blue'); $newBalance = $balance->subtract(Money::EUR(999));
  4. @pelshoff $yesterday = $today->previousDay(); $otherDay = $today->withDay(32); $bluesDay = Date::fromString('My

    favorite color is blue'); $newBalance = $balance->subtract(Money::EUR(999)); $currentMeetings = $meetingService->getCurrentMeetings();
  5. @pelshoff $myAggregate->addAnEntity(new AnEntity(/**/)); $myAggregate->addAnEntity($anEntityId, /**/); $anEntity = $myAggregate->getAnEntity($anEntityId); $aViewModel =

    $myAggregate->getAnEntity($anEntityId); // best not? $aViewModel = $myAggregate->view()->getAnEntity($anEntityId); // my preference $myAggregate = $anEntityViewModel->getAnEntity($anEntityId); // CQRS
  6. @pelshoff $myAggregate->addAnEntity(new AnEntity(/**/)); $myAggregate->addAnEntity($anEntityId, /**/); $anEntity = $myAggregate->getAnEntity($anEntityId); $aViewModel =

    $myAggregate->getAnEntity($anEntityId); // best not? $aViewModel = $myAggregate->view()->getAnEntity($anEntityId); // my preference $myAggregate = $anEntityViewModel->getAnEntity($anEntityId); // CQRS $myAggregate->getAnEntity($anEntityId)->update(/**/); $myAggregate->updateAnEntity($anEntityId, /**/);
  7. @pelshoff $myAggregate->addAnEntity(new AnEntity(/**/)); $myAggregate->addAnEntity($anEntityId, /**/); $anEntity = $myAggregate->getAnEntity($anEntityId); $aViewModel =

    $myAggregate->getAnEntity($anEntityId); // best not? $aViewModel = $myAggregate->view()->getAnEntity($anEntityId); // my preference $myAggregate = $anEntityViewModel->getAnEntity($anEntityId); // CQRS $myAggregate->getAnEntity($anEntityId)->update(/**/); $myAggregate->updateAnEntity($anEntityId, /**/);
  8. @pelshoff Value object Entity Aggregate Service Identity X V V

    X State V V V X Rules V V V V Behavior V V V V Consistency Transactional Eventual
  9. @pelshoff final class MeetingService { private MeetingRepository $repository; private Clock

    $clock; public function __construct(/**/) {/**/} public function getCurrentMeetings(): array { $range = new ClosedDateTimeRange($this->clock->now(), $this->clock->now('+1 month')); return array_map( fn (Meeting $meeting) => $meeting->view(), $this->repository->findMeetingsBySpecification( new IsPublishedDuring($range) ) ); } } No input! Context Context Context
  10. @pelshoff final class MeetingServiceTest extends TestCase { public function testThatItOnlyFindsCurrentMeetings():

    void { $meetingService = new MeetingService( new InMemoryMeetingRepository(), new Clock('1997-01-01') ); $meetingService->planNewMeetingAt(new DateTimeImmutable('1996-12-01')); $meetingService->planNewMeetingAt(new DateTimeImmutable('1997-01-11')); $meetingService->planNewMeetingAt(new DateTimeImmutable('1997-05-11')); $actual = $meetingService->getCurrentMeetings(); $expected = [new MeetingView(new DateTimeImmutable('1997-01-11'))]; $this->assertEquals($expected, $actual); } }
  11. @pelshoff final class RegistrationService { private MeetingRepository $meetingRepository; private AttendeeRepository

    $attendeeRepository; private RegistrationRepository $registrationRepository; public function __construct(/**/) {/**/} public function registerAttendee(Uuid $meetingId, Uuid $attendeeId): void { $meeting = $this->assertMeertingExists($meetingId); $this->assertAttendeeIsNotImaginary($attendeeId); $this->assertAttendeeIsNotRegistered($meetingId, $attendeeId); $registration = $meeting->register($attendeeId); $this->registrationRepository->save($registration); } } Context Context Context Side-effect Input
  12. @pelshoff final class RegistrationServiceTest extends TestCase { public function testThatItSavesRegistrations():

    void { /**/ $this->getMockBuilder(RegistrationRepository::class) ->getMock() ->expects($this->once()) ->method('sav') ->with(new Registration($meetingId, $attendeeId)); } } Oops
  13. @pelshoff $client->expects($this->any()) ->method('request') ->willReturnOnConsecutiveCalls( new JsonResponse(['access_token' => 'testAccessToken']), // authenticateClient

    new JsonResponse(['refresh_token' => 'testToken']), new JsonResponse(['data' => [ [ 'standingInstructionType' => StandingInstruction::STANDING_INSTRUCTION_PURCHASE, 'fundCode' => 'testFundCode', 'endDate' => date('Y-m-d', strtotime('+99 year')), 'standingInstructionNumber' => 3, ], ]]), // get new JsonResponse([]), // le remove new JsonResponse([]), // put ); 100% converage! :D Real code!
  14. @pelshoff final class RegistrationService { private MeetingRepository $meetingRepository; private AttendeeRepository

    $attendeeRepository; private RegistrationRepository $registrationRepository; public function __construct(/**/) {/**/} public function registerAttendee(Uuid $meetingId, Uuid $attendeeId): void { $meeting = $this->assertMeertingExists($meetingId); $attendee = $this->assertAttendeeIsNotImaginary($attendeeId); $listOfAttendees = $this->attendeeRepository->listAttendeesFor($meetingId); $context = new RegisterNewAttendee($meeting, $listOfAttendees); $registration = $context->register($attendee); $this->registrationRepository->save($registration); } }
  15. @pelshoff final class RegisterNewAttendee { private Meeting $meeting; private ListOfRegistrations

    $registrations; /**/ public function register(Attendee $attendee): Registration { $this->assertAttendeeIsNotRegistered($attendee); return $this->meeting->register($attendee->getId()); } private function assertAttendeeIsNotRegistered(Attendee $attendee) { if ($this->registrations->isAttendeeRegistered($attendee->getId())) { throw CouldNotRegisterAttendee::becauseAttendeeWasPreviouslyRegistered( $this->meeting->getId(), $attendee->getId() ); } } }
  16. @pelshoff 1, 2... // Integration/DB or Unit+Mock function testThatNewRegistrationsAreSaved() function

    testThatRegistrationsAreUpdatedAndSaved() function testThatXyAndZAndSaved() many // Integration/DB function testThatTheAggregateCanBeSaved() function testThatSpecialCircumstancesAlsoIntegrateWell() // Unit function testThatBusinessLogicWorksAsExpected() function testThatTheyDontRequireManyMocks() function testThatIfEvenAnyAtAll() function testThatItMakesYouHappierAndMoreProductive()
  17. @pelshoff Service Aggregate Service+Context Performance + - +/- Code overhead

    +/- + - Infra complexity + +/- + Unit testing - + + Consistency Eventual Transactional It depends Conclusion For simple cases (most) For transactional boundaries For complex cases
  18. @pelshoff Heuristics anything that provides a plausible aid or direction

    in the solution of a problem but is in the final analysis unjustified, incapable of justification, and potentially fallible. -Billy Vaughn Koen https://www.dddheuristics.com/