Final Class Aggregate

Final Class Aggregate

Presented at PHP fwdays'20 online conference

B84af63b07f297643ab1fd943c9ac59c?s=128

pelshoff

May 30, 2020
Tweet

Transcript

  1. @pelshoff

  2. @pelshoff

  3. @pelshoff aggregate

  4. @pelshoff

  5. @pelshoff Rules & Behavior at the Information

  6. @pelshoff 1/3 Objects & Messages

  7. @pelshoff $response = $object->message($input);

  8. @pelshoff Value object Entity Service Identity X V X State

    V V X Rules V V V Behavior ? V V
  9. @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!
  10. @pelshoff https://i.imgflip.com/43d4ha.jpg

  11. @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));
  12. @pelshoff $yesterday = $today->previousDay(); $otherDay = $today->withDay(32);

  13. @pelshoff $yesterday = $today->previousDay(); $otherDay = $today->withDay(32); $bluesDay = Date::fromString('My

    favorite color is blue');
  14. @pelshoff $yesterday = $today->previousDay(); $otherDay = $today->withDay(32); $bluesDay = Date::fromString('My

    favorite color is blue'); $newBalance = $balance->subtract(Money::EUR(999));
  15. @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();
  16. @pelshoff Input Local Global Information Aggregate Value object Entity Service

  17. @pelshoff Aggregate Root entity Entities

  18. @pelshoff $meeting = $meetingRepository->getMeeting($meetingId); try { $meeting->register($attendeeId); } catch (CouldNotRegisterAttendee

    $e) { /**/ } $meetingRepository->save($meeting); Single transaction
  19. @pelshoff $myAggregate->addAnEntity(new AnEntity(/**/)); $myAggregate->addAnEntity($anEntityId, /**/);

  20. @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
  21. @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, /**/);
  22. @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, /**/);
  23. @pelshoff query failed: [1213] Deadlock: wsrep aborted transaction

  24. @pelshoff Service - Eventually consistent Aggregate - Transactionally consistent Entity

    Value Object
  25. @pelshoff The need for information drives design

  26. @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
  27. @pelshoff 2/3 Context

  28. @pelshoff

  29. @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
  30. @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); } }
  31. @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
  32. @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
  33. @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!
  34. @pelshoff Mock and spy make me cry :’(

  35. @pelshoff

  36. @pelshoff MeetingService::registerAttendee() R R R M

  37. @pelshoff

  38. @pelshoff MeetingService::registerAttendee() R R M

  39. @pelshoff

  40. @pelshoff MeetingService::registerAttendee() R R R RegisterNewAttendee::execute

  41. @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); } }
  42. @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() ); } } }
  43. @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()
  44. @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
  45. @pelshoff 3/3 Heuristics

  46. @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/
  47. @pelshoff Everything is a value (object) until it's not

  48. @pelshoff Aggregates guard transactional consistency, services guard eventual consistency

  49. @pelshoff Programmers make theoretical rules

  50. @pelshoff Keep aggregates small

  51. @pelshoff Push context and side-effects to the edge of the

    domain
  52. @pelshoff Rules and Behavior at the Information

  53. @pelshoff Why is there so much confusion about aggregates?

  54. Pim Elshoff Work: developer.procurios.com Follow: @pelshoff Coaching: pelshoff.com Slides: speakerdeck.com/pelshoff/final-class-aggregate

    Diagrams: diagram.codes