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:

diagram

Also, the fixture scripts should probably be kept separate from the production code.

Solution

git checkout tags/220-introducing-packages
mvn clean package jetty:run

Exercise

Rename the following:

Class From To

PetClinicFixtureScript-Provider

domainapp.dom.impl

domainapp.modules.impl

Owner

domainapp.dom.impl

domainapp.modules.impl.pets.dom

OwnerTest_updateName

domainapp.dom.impl

domainapp.modules.impl.pets.dom

OwnerTest_delete

domainapp.dom.impl

domainapp.modules.impl.pets.dom

Owners

domainapp.dom.impl

domainapp.modules.impl.pets.dom

Pet

domainapp.dom.impl

domainapp.modules.impl.pets.dom

PetSpecies

domainapp.dom.impl

domainapp.modules.impl.pets.dom

Pet_bookVisit_Test

domainapp.dom.impl

domainapp.modules.impl.pets.dom

RecreatePetsAndOwners

domainapp.dom.impl

domainapp.modules.impl.pets.fixtures

Visit

domainapp.dom.impl

domainapp.modules.impl.visits.dom

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:

diagram

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.

Solution

git checkout tags/230-inverting-responsibilities
mvn clean package jetty:run

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 the Pet.

Pet’s visits (a contributed collection)

We also have the issue that we can’t actually access the Visits 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:

Pet visits collection

Solution

git checkout tags/240-pets-visits-a-contributed-collection
mvn clean package jetty:run

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 the Visits 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 Pets which in turn have associated Visits? Well, the Pets will be cascade-deleted, but the Visits are not. This prevents the delete from occurring.

What we want to happen is for the Visits 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 Visits 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 Pets and Visits should be deleted: the Pets because the Owner ←→ Pet association is declared for cascade-delete , the Visits 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.