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 Apache Isis framework provides some powerful tools.
Introducing Packages
At the moment all of the domain services and entities are in a single package.
Referring back to our design we see though that there are meant to be two packages, pets
and visits
:
Also, the fixture scripts should probably be kept separate from the production code.
Exercise
Rename the following:
Class | From | To |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Also move the supporting .layout.xml
, .png
files.
Inverting responsibilities (Refactoring the Pet
's visits)
For long-term maintainability it’s important to keep the application modular. In particular, that means avoiding cyclic dependencies.
If we look at our original design, we see that the original idea was for visits
package depends upon the pets
package, but not the other way around:
However, as things stand this dependency is bidirectional: Pet
acts as the factory of Visit
, and yet Visit
references back to that same Pet
.
We fix the issue by moving the behaviour out of Pet
, and into a "mixin".
This mixin then resides in the visits
package.
Exercise
-
create the following "mixin" class (most of the code can be copied-n-pasted out of
Pet
):@Mixin(method = "act") (1) public class Pet_bookVisit { private final Pet pet; public Pet_bookVisit(Pet pet) { (2) this.pet = pet; } @Action(semantics = SemanticsOf.NON_IDEMPOTENT) public Visit act( (1) final LocalDateTime at, @Parameter(maxLength = 4000) @ParameterLayout(multiLine = 5) final String reason) { return repositoryService.persist(new Visit(this.pet, at, reason)); } public LocalDateTime default0Act() { (1) return clockService.now() .plusDays(1) .toDateTimeAtStartOfDay() .toLocalDateTime() .plusHours(9); } public String validate0Act(final LocalDateTime proposed) { (1) return proposed.isBefore(clockService.nowAsLocalDateTime()) ? "Cannot enter date in the past" : null; } @javax.jdo.annotations.NotPersistent @javax.inject.Inject RepositoryService repositoryService; @javax.jdo.annotations.NotPersistent @javax.inject.Inject ClockService clockService; }
1 the name of the action is derived from the class rather than the method name (by convention, called simply "act"). 2 constructor determines the type that the mixin contributes to. This can be a class or an interface. -
remove the corresponding code from
Pet
-
refactor the
Pet_bookVisit_Test
unit test to exercise the mixin rather than thePet
.
Pet’s visits (a contributed collection)
We also have the issue that we can’t actually access the Visit
s once they have been created.
An obvious place to see them would probably be from the Pet
.
Similar to the "bookVisit" contributed action, we can also contribute a "visits" collection:
Exercise
-
we’ll start by creating a
Visits
domain service repository:@DomainService(nature = NatureOfService.DOMAIN) (1) public class Visits { @Programmatic (2) public java.util.Collection<Visit> findByPet(Pet pet) { TypesafeQuery<Visit> q = isisJdoSupport.newTypesafeQuery(Visit.class); final QVisit cand = QVisit.candidate(); q = q.filter( cand.pet.eq((q.parameter("pet", Pet.class)) ) ); return q.setParameter("pet", pet) .executeList(); } @javax.inject.Inject IsisJdoSupport isisJdoSupport; }
1 don’t show in the menu 2 and in any case, exclude this method from the metamodel. -
create the
Pet_visits
mixin and have it delegate to theVisits
service:@Mixin(method = "coll") (1) public class Pet_visits { private final Pet pet; public Pet_visits(Pet pet) { this.pet = pet; } @Action(semantics = SemanticsOf.SAFE) (2) @ActionLayout(contributed = Contributed.AS_ASSOCIATION) (3) @CollectionLayout(defaultView = "table") public java.util.Collection<Visit> coll() { (1) return visits.findByPet(pet); } @javax.inject.Inject Visits visits; }
1 the collection name is derived from the class name, not the method name 2 behind the scenes contributed collections are just a type of action. They must take no arguments, and have no side-effects. 3 this is what makes the contributed action instead be rendered as a collection -
associate the
Pet_bookVisit
action with this collection (so is rendered as part of the "visits" collection):@Action(semantics = SemanticsOf.NON_IDEMPOTENT, associateWith = "visits") @ActionLayout(named = "Book") public Visit act(...) { ... }
Events
Mixins are a powerful technique to decouple the application, but they are only half the story.
What happens if we attempt to delete an Owner
that has associated Pet
s which in turn have associated Visit
s?
Well, the Pet
s will be cascade-deleted, but the Visit
s are not.
This prevents the delete from occurring.
What we want to happen is for the Visit
s also to be deleted.
However, this can’t be a responsibility of Owner
or Pet
, because they are not meant to "know" about the associated visits.
What we can do instead is to use domain events, and set up a subscriber domain service to do the delete of associated Visit
s when a Pet
is deleted.
Solution
git checkout tags/250-events
mvn clean package jetty:run
To try this out, book a Visit
for a Pet
, then navigate back to the parent Owner
and delete it.
All associated Pet
s and Visit
s should be deleted: the Pet
s because the Owner ←→ Pet association is declared for cascade-delete , the Visit
s because of the subscriber.
Exercise
-
update
Pet
so that events will be emitted when it is deleted:@DomainObject( auditing = Auditing.ENABLED, removingLifecycleEvent = Pet.RemovingEvent.class (1) ) ... public class Pet implements Comparable<Pet> { public static class RemovingEvent extends ObjectRemovingEvent<Pet> {} ... }
1 an instance of this class will be emitted when the Pet
instance is about to be deleted -
add a new
PetVisitCascadeDelete
subscriber.@DomainService(nature = NatureOfService.DOMAIN) public class PetVisitCascadeDelete extends org.apache.isis.applib.AbstractSubscriber { (1) @org.axonframework.eventhandling.annotation.EventHandler (2) public void on(Pet.RemovingEvent ev) { (3) Collection<Visit> visitsForPet = visits.findByPet(ev.getSource()); for (Visit visit : visitsForPet) { repositoryService.removeAndFlush(visit); } } @javax.inject.Inject Visits visits; @javax.inject.Inject RepositoryService repositoryService; }
1 convenience superclass that hooks up the subscriber with the internal event bus 2 the event bus is implemented using the Axon Framework so the callback method must be annotated with the appropriate annotation. 3 called only when a Pet
is about to be deleted.