Slide 1

Slide 1 text

@pelshoff

Slide 2

Slide 2 text

@pelshoff

Slide 3

Slide 3 text

@pelshoff aggregate

Slide 4

Slide 4 text

@pelshoff

Slide 5

Slide 5 text

@pelshoff Rules & Behavior at the Information

Slide 6

Slide 6 text

@pelshoff 1/3 Objects & Messages

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

@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!

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

@pelshoff Input Local Global Information Aggregate Value object Entity Service

Slide 17

Slide 17 text

@pelshoff Aggregate Root entity Entities

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

@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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

@pelshoff The need for information drives design

Slide 26

Slide 26 text

@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

Slide 27

Slide 27 text

@pelshoff 2/3 Context

Slide 28

Slide 28 text

@pelshoff

Slide 29

Slide 29 text

@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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

@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

Slide 32

Slide 32 text

@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

Slide 33

Slide 33 text

@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!

Slide 34

Slide 34 text

@pelshoff Mock and spy make me cry :’(

Slide 35

Slide 35 text

@pelshoff

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

@pelshoff

Slide 38

Slide 38 text

@pelshoff MeetingService::registerAttendee() R R M

Slide 39

Slide 39 text

@pelshoff

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

@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

Slide 45

Slide 45 text

@pelshoff 3/3 Heuristics

Slide 46

Slide 46 text

@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/

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

@pelshoff Aggregates guard transactional consistency, services guard eventual consistency

Slide 49

Slide 49 text

@pelshoff Programmers make theoretical rules

Slide 50

Slide 50 text

@pelshoff Keep aggregates small

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

@pelshoff Rules and Behavior at the Information

Slide 53

Slide 53 text

@pelshoff Why is there so much confusion about aggregates?

Slide 54

Slide 54 text

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