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 andbookVisit
mixin action that are both contributed by thevisits
module to thePet
entity in thepets
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 Pet
s are automatically deleted when the PetOwner
is itself deleted, if there are any Visit
s 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 Pet
s from being deleted if there are Visit
s 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 correspondingVisit
s, the error we get back is a database exception -
in
PetOwner
, modify thedelete
action so that it emits a specific domain event type.PetOwner.javapublic 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 thePetOwner_delete
action if there are any visitsPetOwner_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 Visit
s 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 Visit
s, 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. |