Modularity
Keeping applications modular is key to their long-term maintainability. If every class potentially can depend on any other class, we’ll end up with a "big ball of mud" that becomes almost impossible to change.
Instead, we need to ensure that the dependency graph between packages remains acyclic. The framework provides two main tools:
-
the first we’ve already seen is mixins.
These allow us to locate busines logic in one module that "appears" to reside in another module. Examples are the
visits
mixin collection andbookVisit
mixin action that are both contributed by thevisits
module to thePet
entity in thepets
module. -
the second is domain events.
These we haven’t yet seen, but provide a way for one module to react to (or to veto) actions performed in logic in another module.
In this part of the tutorial we’ll look at domain events.
Ex 7.1: refactor PetOwner’s delete action
Currently the delete
action for PetOwner
is implemented as a mixin within the Pet
package.
That’s a nice place for that functionality, because it can delete any Pet`s for the `PetOwner
if any exist.
However, we also have added Visit
, which has the same issue: we cannot delete a Pet
if there are associated Visit
s.
And, in fact, we don’t want to allow a PetOwner
and their Pet
s from being deleted if there are Visit
s in the database; they might not have paid!
In this exercise we will move the responsibility to delete an action back to PetOwner
, and then use subscribers for both Pet
and Visit
to cascade delete or to veto the action respectively if there are related objects.
Solution
git checkout tags/07-01-delete-action-events
mvn clean install
mvn -pl spring-boot:run
To test this out:
-
try deleting a
PetOwner
where none of theirPet
s have anyVisit
s; the action should succeed, and thePetOwner
and thePet
s should all be deleted. -
now book a
Visit
for aPet
, then navigate back to the parentPetOwner
and attempt to delete it. This time the action should be vetoed, because of thatVisit
.
Tasks
-
in
PetOwner_delete
remove the code that deletes thePet
s. In its place, define a subclass of ActionDomainEvent as a nested class of the mixin, and reference in the @Action#domainEvent attribute.PetOwner_delete.java@Action( domainEvent = PetOwner_delete.ActionEvent.class, (1) semantics = SemanticsOf.NON_IDEMPOTENT_ARE_YOU_SURE, commandPublishing = Publishing.ENABLED, executionPublishing = Publishing.ENABLED ) @ActionLayout( associateWith = "name", position = ActionLayout.Position.PANEL, describedAs = "Deletes this object from the persistent datastore") @RequiredArgsConstructor public class PetOwner_delete { public static class ActionEvent (2) extends ActionDomainEvent<PetOwner_delete>{} private final PetOwner petOwner; public void act() { repositoryService.remove(petOwner); return; } @Inject RepositoryService repositoryService; }
1 specifies the domain event to emit when the action is called 2 declares the action event (as a subclass of the framework’s ActionDomainEvent). -
create a subscriber in the
pets
package to delete allPet
s when thePetOwner_delete
action is invoked:PetOwnerForPetsSubscriber.java@Service public class PetOwnerForPetsSubscriber { @EventListener(PetOwner_delete.ActionEvent.class) public void on(PetOwner_delete.ActionEvent ev) { switch(ev.getEventPhase()) { case EXECUTING: (1) PetOwner petOwner = ev.getSubject(); (2) List<Pet> pets = petRepository.findByPetOwner(petOwner); pets.forEach(repositoryService::remove); break; } } @Inject PetRepository petRepository; @Inject RepositoryService repositoryService; }
1 events are emitted at different phases. The EXECUTING
phase is fired before the delete action itself is fired, so is the ideal place for us to perform the cascade delete.2 is the mixee of the mixin that is emitting the event. -
create a subscriber in the
visits
module to veto thePetOwner_delete
if there are anyPet`s of the `PetOwner
with at least oneVisit
:PetOwnerForVisitsSubscriber.java@Service public class PetOwnerForVisitsSubscriber { @EventListener(PetOwner_delete.ActionEvent.class) public void on(PetOwner_delete.ActionEvent ev) { switch(ev.getEventPhase()) { case DISABLE: PetOwner petOwner = ev.getSubject(); List<Pet> pets = petRepository.findByPetOwner(petOwner); for (Pet pet : pets) { List<Visit> visits = visitRepository.findByPetOrderByVisitAtDesc(pet); int numVisits = visits.size(); if(numVisits > 0) { ev.disable(String.format("%s has %d visit%s", titleService.titleOf(pet), numVisits, numVisits != 1 ? "s" : "")); } } break; } } @Inject TitleService titleService; @Inject VisitRepository visitRepository; @Inject PetRepository petRepository; }