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: mixins.

    These allow us to locate business 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 provide a way for one module to react to (or to veto) actions performed in logic in another module.

In this section we’ll look at domain events.

Ex 7.1: refactor PetOwner’s delete action

Currently the delete action for PetOwner is broken: although the owner’s Pets are automatically deleted when the PetOwner is itself deleted, if there are any Visits then the foreign key in the database will prevent deletion.

In one sense this is good: we probably 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! However, we ought to have the business logic in the domain layer rather than rely on the database’s foreign key.

In this exercise we’ll use domain events to cascade delete or to veto the action respectively if there are related objects.

Solution

git checkout tags/v3/07-01-delete-action-events
mvn clean install
mvn -pl webapp spring-boot:run -Dspring-boot.run.jvmArguments="-Dspring.profiles.active=dev"

Tasks

  • (optional) confirm that although it’s not possible to delete a PetOwner if there are corresponding Visits, the error we get back is a database exception

  • in PetOwner, modify the delete action so that it emits a specific domain event type.

    PetOwner.java
        public static class DeleteActionDomainEvent
                extends org.apache.causeway.applib.events.domain.ActionDomainEvent<PetOwner> {}     (1)
    
        @Action(
                semantics = NON_IDEMPOTENT_ARE_YOU_SURE,
                domainEvent = DeleteActionDomainEvent.class                                         (2)
        )
        @ActionLayout(
                describedAs = "Deletes this object from the persistent datastore")
        public void delete() { ... }
    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).
    in fact, domain events are always emitted; but by default a generic ActionDomainEvent is used rather than a specific subclass.
  • create a subscriber in the visit module to to veto an attempt to invoke the PetOwner_delete action if there are any visits

    PetOwner_delete_subscriber.java
    @Component
    public class PetOwner_delete_subscriber {
    
        @EventListener(PetOwner.DeleteActionDomainEvent.class)  (1)
        void on(PetOwner.DeleteActionDomainEvent event) {       (1)
            PetOwner subject = event.getSubject();              (2)
            switch (event.getEventPhase()) {                    (3)
                case HIDE:
                    break;
                case DISABLE:                                   (4)
                    List<Visit> visits = visitRepository.findByPetOwner(subject);
                    if (!visits.isEmpty()) {
                        event.veto("This owner has %d visit%s", visits.size(), (visits.size() == 1 ? "" : "s"));
                    }
                    break;
                case VALIDATE:
                    break;
                case EXECUTING:
                    break;
                case EXECUTED:
                    break;
            }
        }
    
        @Inject VisitRepository visitRepository;
    }
    1 subscribes to the event using Spring @EventListener
    2 returns the effective originator of the event. This works for both regular actions and mixin actions
    3 the subscriber is called multiple times, for the various phases of the execution lifecycle; more on this below
    4 if there are any Visits for this pet owner, then veto the interaction. In the user interface, the "delete" button will be disabled, that is greyed out. The returned string is used as the tooltip to explain why the button is disabled.

The event lifecycle allows subscribers to veto (in other words, specify preconditions) in three different ways:

  • hide - will hide the action’s button in the UI completely

  • disable - will disable (grey) out the action’s button

  • validate - will prevent the button from being pressed. this case applies when validating the action arguments.

If the action is not vetored, then the subscriber is also possible to perform additional steps:

  • executing - the action is about to execute.

  • executed - the action is just execute

For example, if the business rule had instead been to simply delete all Visits, then this could have been implemented in the "executing" phase.

it’s also worth knowing that these events are fired for properties and collections as well as actions. Therefore subscribers can substantially dictate what is accessible for any given domain object.