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 and bookVisit mixin action that are both contributed by the visits module to the Pet entity in the pets 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 Visits. And, in fact, we don’t want to allow a PetOwner and their Pets from being deleted if there are Visits 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 their Pets have any Visits; the action should succeed, and the PetOwner and the Pets should all be deleted.

  • now book a Visit for a Pet, then navigate back to the parent PetOwner and attempt to delete it. This time the action should be vetoed, because of that Visit.

Tasks

  • in PetOwner_delete remove the code that deletes the Pets. 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 all Pets when the PetOwner_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 the PetOwner_delete if there are any Pet`s of the `PetOwner with at least one Visit:

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

Optional Exercise

Improve the implementation of PetOwnerForVisitsSubscriber so that it performs only a single database query to find if there are any Visit for the PetOwner.