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

    View Slide

  2. @pelshoff

    View Slide

  3. @pelshoff
    aggregate

    View Slide

  4. @pelshoff

    View Slide

  5. @pelshoff
    Rules & Behavior
    at the Information

    View Slide

  6. @pelshoff
    1/3 Objects & Messages

    View Slide

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

    View Slide

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

    View Slide

  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!

    View Slide

  10. @pelshoff
    https://i.imgflip.com/43d4ha.jpg

    View Slide

  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));

    View Slide

  12. @pelshoff
    $yesterday = $today->previousDay();
    $otherDay = $today->withDay(32);

    View Slide

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

    View Slide

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

    View Slide

  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();

    View Slide

  16. @pelshoff
    Input Local Global
    Information Aggregate
    Value object Entity Service

    View Slide

  17. @pelshoff
    Aggregate
    Root entity
    Entities

    View Slide

  18. @pelshoff
    $meeting = $meetingRepository->getMeeting($meetingId);
    try {
    $meeting->register($attendeeId);
    } catch (CouldNotRegisterAttendee $e) {
    /**/
    }
    $meetingRepository->save($meeting);
    Single
    transaction

    View Slide

  19. @pelshoff
    $myAggregate->addAnEntity(new AnEntity(/**/));
    $myAggregate->addAnEntity($anEntityId, /**/);

    View Slide

  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

    View Slide

  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, /**/);

    View Slide

  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, /**/);

    View Slide

  23. @pelshoff
    query failed: [1213] Deadlock: wsrep aborted transaction

    View Slide

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

    View Slide

  25. @pelshoff
    The need for information drives design

    View Slide

  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

    View Slide

  27. @pelshoff
    2/3 Context

    View Slide

  28. @pelshoff

    View Slide

  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

    View Slide

  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);
    }
    }

    View Slide

  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

    View Slide

  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

    View Slide

  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!

    View Slide

  34. @pelshoff
    Mock and spy
    make me cry :’(

    View Slide

  35. @pelshoff

    View Slide

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

    View Slide

  37. @pelshoff

    View Slide

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

    View Slide

  39. @pelshoff

    View Slide

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

    View Slide

  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);
    }
    }

    View Slide

  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()
    );
    }
    }
    }

    View Slide

  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()

    View Slide

  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

    View Slide

  45. @pelshoff
    3/3 Heuristics

    View Slide

  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/

    View Slide

  47. @pelshoff
    Everything is a value (object) until it's not

    View Slide

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

    View Slide

  49. @pelshoff
    Programmers make theoretical rules

    View Slide

  50. @pelshoff
    Keep aggregates small

    View Slide

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

    View Slide

  52. @pelshoff
    Rules and Behavior at the Information

    View Slide

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

    View Slide

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

    View Slide