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

Final Class Aggregate

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

Final Class Aggregate

Presented at PHP fwdays'20 online conference

Avatar for pelshoff

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/