Abstract Factory in Domain Modelling

2012-09-05
6 min read

The Abstract Factory pattern is an important building block for Domain Modelling. It hides the complexity of creating a domain object from the caller of the factory. It also enables us to create domain objects those have complex dependencies without worrying about when and how to inject its dependencies.

It is easier to explain the idea with a concrete example. I used to work on a project to build a simple online booking system for a heath club. The heath club provides one-to-many training sessions. The club administrators schedule training sessions in advance and publish them on a web page. Each training session has limited space. Members can reserve space for one or many people against a training session as long as there are enough slots available. Members may cancel their reservations any time.

I am going to ignore the process of creating and displaying training sessions, but concentrate on the reservation process here.

I started by creating the domain objects first:

package com.thinkinginobjects.domain;

public class TrainingSession {

     private int totalCapacity;
     private List<Reservation> reserved;

     public TrainingSession(int totalSeats) {
          this.totalCapacity = totalSeats;
          this.reserved = new ArrayList<Reservation>();
     }

     public Reservation reserveSpace(long userId, int numberOfPeople) throws ReservationException {
          boolean available = seatsAvailable(numberOfPeople);
          if (available) {
               Reservation newReservation = new Reservation(userId, numberOfPeople);
               reserved.add(newReservation);
               newReservation.confirmed();
               return newReservation;
          } else {
               throw new ReservationException("No available space.");
          }
     }

     private boolean seatsAvailable(int requested) {
          int reservedCount = 0;
          for (Reservation each : reserved) {
               reservedCount += each.getOccupiedSeats();
          }
          return reservedCount + requested <= totalCapacity;
     }
}

package com.thinkinginobjects.domain;

public class Reservation {

     private long userId;
     private int numberOfPeople;
     private boolean cancelled;

     public Reservation(long userId, int numberOfPeople) {
          this.userId = userId;
          this.numberOfPeople = numberOfPeople;
          this.cancelled = false;
     }

     public void confirmed() {
     }

     public boolean bookedBy(long userId) {
          return this.userId == userId;
     }

     public int getOccupiedSeats() {
          if (cancelled) {
               return 0;
          } else {
               return numberOfPeople;
          }
     }

     public void cancel() {
          this.cancelled = true;
     }
}

package com.thinkinginobjects.service;

public class ReservationService {

     private TrainingSessionRepository trainingSessionRepository;
     private ReservationRepository reservationRepository;

     public void reserve(long userId, long sessionId, int numberOfPeople) throws ReservationException {
          TrainingSessionById query = new TrainingSessionById(sessionId);
          TrainingSession session = trainingSessionRepository.querySingle(query);
          session.reserveSpace(userId, numberOfPeople);
     }

     public void cancel(long userId, long sessionId) {
          Reservation reservation = reservationRepository.querySingle(new ReservationsByUserAndSession(userId, sessionId));
          reservation.cancel();
     }
}

When a member fills in the number of people and click “Reserve” button on a web page, the web servlet invokes ReservationService.reserve(), which simply delegate the request to TrainingSession. The TrainingSession creates a Reservation instance and remembers it for availability checking purpose.

If a member want to cancel a particular reservation, the system calls ReservationService.cancel(). Then the ReservationService finds the right reservation instance and delegate the cancellation to it.

Nice and simple. We are going to add more challenge by asking the system to send an email to a member when he make a reservation or cancel one.

A naive solution is to add the email logic to the ReservationService:

package com.thinkinginobjects.service;

public class BadReservationService {

     private TrainingSessionRepository trainingSessionRepository;
     private ReservationRepository reservationRepository;
     private EmailSender emailSender;

     public void reserve(long userId, long sessionId, int numberOfPeople) throws ReservationException {
          TrainingSessionById query = new TrainingSessionById(sessionId);
          TrainingSession session = trainingSessionRepository.querySingle(query);
          session.reserveSpace(userId, numberOfPeople);
          emailSender.send(userId, "Booking confirmed", String.format("Your booking has been confirmed."));

     }

     public void cancel(long userId, long sessionId) {
          Reservation reservation = reservationRepository.querySingle(new ReservationsByUserAndSession(userId, sessionId));
          reservation.cancel();
          emailSender.send(userId, "Booking cancelled", String.format("Your booking has been cancelled."));
     }
}

It is easy to add a reference of MailSender in ReservationService because the ReservationService is a singleton effectively. If I use spring to wire up my services, this may be as simple as adding a line of xml to my spring config file.

However this approach moves a part of the business logic into ReservationService. The domain logic is fragmented across the domain layer and the service layer. I much prefer to keep all business logic together in the domain model.

A better solution:

The Reservation class knows about the completion of reservation and cancellation. It is a good candidate to host the email logic.

package com.thinkinginobjects.domainalternative;

public class Reservation {

     private EmailSender mailSender;

     private long id;
     private long userId;
     private int numberOfPeople;
     private boolean cancelled;

     public Reservation(long reservationId, long userId, int numberOfPeople, EmailSender mailSender) {
          this.userId = userId;
          this.numberOfPeople = numberOfPeople;
          this.cancelled = false;
          this.mailSender = mailSender;
     }

     public void confirmed() {
          mailSender.send(userId, "Booking confirmed", String.format("Your booking has been confirmed, your booking id is %s", id));
     }

     public boolean bookedBy(long userId) {
          return this.userId == userId;
     }

     public int getOccupiedSeats() {
          if (cancelled) {
               return 0;
          } else {
               return numberOfPeople;
          }
     }

     public void cancel() {
          this.cancelled = true;
          mailSender.send(userId, "Booking cancelled", String.format("Your booking %s has been cancelled.", id));
     }
}

However the TrainingSession is no longer able to create a Reservation instance because it cannot provide MailSender’s reference. The TraininSession does not use MailSender directly. I don’t want the TraininSession to carry a reference to MailSender around for the sole purpose of passing it to Reservation’s constructor.

The Abstract Factory pattern comes to solve my problem. Instead of instantiating a Reservation directly, the TrainingSession can use a ReservationFactory to create an instance of Reservation, passing in only the relevant business information. The actual implementation of ReservationFactory has a reference to MailSender, which the factory use to construct Reservation instances.

package com.thinkinginobjects.domainalternative;

public interface ReservationFactory {

     Reservation create(long userId, int numberOfPeople);

}

package com.thinkinginobjects.domainalternative;

import com.thinkinginobjects.domain.ReservationException;

public class TrainingSession {

     private int totalCapacity;
     private List<Reservation> reserved;
     private ReservationFactory reservationFactory;

     public TrainingSession(int totalSeats) {
          this.reserved = new ArrayList<Reservation>();
     }

     public Reservation reserveSpace(long userId, int numberOfPeople) throws ReservationException {
          boolean available = seatsAvailable(numberOfPeople);
          if (available) {
               Reservation newReservation = reservationFactory.create(userId, numberOfPeople); // Use factory instead of "new"
               reserved.add(newReservation);
               newReservation.confirmed();
               return newReservation;
          } else {
               throw new ReservationException("No available space.");
          }
     }

     private boolean seatsAvailable(int requested) {
          int reservedCount = 0;
          for (Reservation each : reserved) {
               reservedCount += each.getOccupiedSeats();
          }
          return reservedCount + requested <= totalCapacity;
     }
}

package com.thinkinginobjects.servicealternative;

public class ReservationFactoryImpl implements ReservationFactory{

     private IdAllocator idAllocator;
     private EmailSender mailSender;

     @Override
     public Reservation create(long userId, int numberOfPeople) {
          long newId = idAllocator.allocate();
          return new Reservation(newId, userId, numberOfPeople, mailSender);
     }
}

The factory is also a good place to generate a unique id for a new Reservation. In the example, the factory implementation use an IdAllocator to create new ids based on a sequence table in the database.

The factory is an interface, which makes easier to mock it up when unit testing domain objects. The factory should be treat as a part of the Domain model and we are safe to let other domain objects to depend on it.

The factory also decouples the caller from the actual type of the factory product. If we expand the use case further to distinguish cancellable and non-cancellable reservations, the abstract factory can instantiate different subclasses of the Reservation for different scenarios, and hide all the details from the caller at the same time.

Conclusion:

The Abstract Factory plays an important role in Domain modelling. The key benefits are:

  • Hide the details of creating a complex domain object.
  • Enables one domain object to create another object without worrying about its dependencies.
  • Factory can produce instances of different classes for different use cases.

The factory interface belongs to the domain model. It is used by other domain objects. It worth to consider it even just for dependency injection and id generation purpose.