When developing an EJB tier that implements non-trivial business logic, a good strategy for tackling complexity and improving maintainability is to design and implement a domain model, which is an object model of the application's problem domain.
On the surface, it would seem appropriate to implement the domain model classes using entity beans since they are intended to represent business objects and they also provide the necessary persistence mechanism; however, directly implementing complex business logic using entity beans can be hard to do for a number of reasons. Server-side development with EJB components is slower than client-side development because of the time it takes to package and redeploy EJB components in an application server. Also, developing entity beans requires solving several problems simultaneously: writing business logic, database schema design and persistence mapping. Furthermore, this approach reduces reusability since the implementation is coupled to the EJB framework.
This article describes a design strategy that accelerates development and improves reusability by structuring the domain model into two levels - a Plain Old Java Objects (POJO) [FOWLER] level that implements the business logic and an entity bean level that implements the persistence.
Consider the design of part of the domain model for a restaurant delivery service. In this application, the customer places an order by performing the following steps:
- Enter delivery address and time
- Select restaurant
- Select menu items and enter quantities
- Start checkout
- Enter payment information
- Place the order
The key domain model classes that implement order entry are shown in Figure 1:
Figure 1: Preliminary Domain model
The responsibilities of each class are as follows:
- Restaurant - represents a restaurant that will prepare food for delivery. It has one or more menu items available for delivery, a geographic service area and a delivery window
- MenuItem - represents an individual menu item. It has a name, a description and a price
- Address - the address to deliver the order to
- Date - the time to delivery the order
- PaymentInformation - stores the credit card information for the order
- PendingOrder - the "shopping cart" for this application. It captures the information entered by the customer: delivery address, a delivery time, selected restaurant, desired quantities of each menu item and payment information
- PendingOrderLineItem - stores the quantity of the menu item ordered by the user. It computes the extended price, which is the quantity times the unit price. There is one line item for each menu item of the selected restaurant
There are some other classes, such as Money and Customer that are not shown on this diagram.
This application has a number of business rules including:
- The delivery address and time must be serviced by at least one restaurant
- A restaurant must be compatible with the entered delivery address and time
- The customer cannot checkout unless the order meets a minimum Money quantity
The behavior of the PendingOrder can be described by the state machine shown in Figure 2:
Figure 2: PendingOrder state machine
This state machine captures the business rules as well as the flow through the application. It consists of the following states:
- New - the initial state of the pending order
- AddressAndTimeSpecified - the customer has entered a valid delivery address and time
- RestaurantSelected - the customer has selected a restaurant compatible with the delivery address and time. If the customer subsequently enters a delivery address and time that is incompatible with the selected restaurant then the new state is AddressAndTimeSpecified
- ReadyForCheckout - the order meets the minimum
- ReadyToGo - the customer has entered valid payment information
- Placed - the customer has placed the order, which results in the system creating an actual order. The PendingOrder could be deleted at this point.
One way to implement this domain model in a J2EE application is to directly implement the PendingOrder, PendingOrderLineItem, Restaurant, and MenuItem classes as entity beans using EJB 2.0 CMP. The other classes would be implemented as regular Java classes. The presentation tier would access these classes indirectly through a session facade [EJBPATTERNS].
Even though this is a common approach, there are some significant problems with doing development this way. First, developing, testing and debugging non-trivial business logic - especially for the PendingOrder entity bean - will involve many iterations through the 'edit-compile-test-debug' cycle. Creating an EJB jar and redeploying the EAR lengthens the "compilation" step of this cycle considerably. For example, on a 2.0GHz Windows 2000 workstation, compiling and executing the test cases for a class can take a second or two, yet creating an EJB jar and redeploying the EAR can take 20 seconds or more. As well as simply making things take longer, it forces the developer to sit and stare at their screen while they wait for it to deploy their changed code. This can cause the developers to lose their train of thought, and allow them to get distracted. This interrupts their flow [MARCO] and reduces their productivity. Furthermore, long deployment times will discourage developers from making small incremental changes and running the tests frequently. Instead, they will clump together their changes before testing them which will make debugging harder.
Second, the developer of an entity bean has to worry about several difficult problems simultaneously: implementing the business logic correctly, defining the database schema and the persistence mapping. It is much easier to solve one problem at a time.
Another problem with this approach is that it complicates testing by requiring a functioning database populated with correct test data in order to test the domain model and its direct and indirect clients, such as the session facade or the presentation tier.
Finally, implementing business logic directly in an entity bean couples the code directly to the bean and to the persistence mechanism. The code cannot be reused in an application that uses a different persistence mechanism. Furthermore, the classes cannot be reused to implement non-persistence components. For example, a developer might want to reduce database accesses by maintaining the session state in memory and storing the PendingOrder in the HttpSession.
Developing a Two Level Domain Model
A much better approach is to decompose the domain model into two levels - a POJO level, which consists of regular Java classes, and an entity bean level. The POJO level consists of abstract classes that implement the complex business logic. These classes, which can run outside of the EJB container, are developed and tested first. The entity bean level consists of entity beans that inherit from the POJO classes, making them persistent. The entity beans are developed once the POJO classes are working correctly.
Figure 3 shows the design of the restaurant delivery application's domain model created using this approach.
Each of the domain classes in Figure 1 - PendingOrder, PendingOrderLineItem, Restaurant, and MenuItem - is realized by the following Java types: an interface, an abstract implementation class and an entity bean. The interface specifies the business methods that can be invoked by the domain class's clients, which don't have access to the implementation class or the entity bean. The abstract implementation class implements the bulk of the business logic and the entity bean is responsible for persistence. Each entity bean's local interface extends the corresponding domain class's interface and each entity bean class extends the corresponding abstract implementation class.
The POJO level classes and the session facade need to be able to create and find domain objects. In order to decouple the POJO level and session facade from the entity bean level and to provide the ability to "plug-in" a stub implementation of the persistence layer for testing, this design uses a DomainManager class, which is essentially an Abstract Factory [GOF] with finder methods. The DomainManager defines an API consisting of abstract methods, such as makePendingOrderLineItem(), which creates a PendingOrderLineItem, and findRestaurant(), which returns a Restaurant with the specified id, and getRestaurantsThatServeZipCodeAndTime(), which returns a collection of restaurants that serve a zip code at a particular time. The signatures of these methods reference the POJO interfaces rather than the implementation classes. Subclasses of DomainManager are responsible for implementing these methods in terms of concrete domain classes.
The DomainManager is a singleton class [GOF] and its clients obtain a reference to its sole instance by calling the static method getInstance(). The default behavior of this method is to create an instance of a subclass of DomainManager specified in a property file. However, test code can override this behavior by calling setInstance() to specify the subclass instance to use. The entity bean level defines the class EntityDomainManager, which is a concrete implementation of the DomainManager. It implements the API by invoking methods on the appropriate home interfaces. For example, the method makePendingOrderLineItem() creates a PendingOrderLineItemEntity by invoking the create() method on PendingOrderLineItemEntityHome.
The domain model uses the Template Method pattern [GOF]. The abstract POJO classes define two types of methods, abstract primitive methods and template methods. The abstract primitive methods provide access to attributes, and manage relationships with other domain model objects. The business methods are template methods that call the primitive methods. The abstract methods are implemented by the entity beans. Examples of template methods defined by the PendingOrderImpl class include updateDeliveryAddressAndTime(), updateRestaurant() and updateQuantities(). Examples of primitive methods called by these template methods include setRestaurant(), getRestaurant(), getLineItems(), setLineItems(), getDeliveryAddress(),setDeliveryAddress(), getDeliveryTime(), and setDeliveryTime().
An entity bean implements the abstract primitive methods in one of two ways. If the primitive method corresponds directly to a container-managed persistent field (cmp field) or a relationship field (cmr field), the entity bean will leave the method abstract. In other cases, the entity bean will implement the primitive method in terms of accessors for container-managed fields and relationships. Also, although not done in this example, an entity bean could implement primitive methods that access the EntityContext or the entity bean's environment. A method could, for example, test whether the caller has a particular security role or obtain the value of an environment entry.
Examples of the different ways the PendingOrderEntityBean implements primitive methods are as follows. The accessors getDeliveryTime() and setDeliveryTime() correspond to the cmp-field deliveryTime and use java.util.Date, and so are left abstract.
The accessors getDeliveryAddress() and setDeliveryAddress() use the application defined Address class. Since cmp fields can only be Java types, the PendingOrderEntityBean stores the delivery address as five separate cmp-fields - street1, street2, city, state, and zip. It implements these accessors to map between an Address object and these five fields.
The accessors getRestaurant() and setRestaurant() correspond to a one-to-one container-managed relationship that the PendingOrderEntity bean has with the RestaurantEntity bean. However, since their signatures use the Restaurant interface rather than the RestaurantEntity interface, they cannot be used as accessors for a cmr-field. Consequently, the entity bean has to implement them as concrete methods that call the abstract container-managed relationship accessors, getRestaurantEntity() and setRestaurantEntity().
The accessors getLineItems() and setLineItems() correspond to a one-to-many container-managed relationship with the PendingOrderLineItemEntity bean. Because their signatures use the Collection interface they can be left abstract.
Testing the Domain Model
The test cases for this domain model consist of two sets of tests, one set that tests the POJO level independently of the entity bean level and another set of tests for the entity bean level. Figure 4 shows the design of the test classes for the PendingOrderImpl class.
Figure 4: PendingOrderImpl Test Case Design
Since the POJO level classes are abstract, the test code needs to define concrete test implementation subclasses for each of them. Examples of the test implementation classes include PendingOrderTestImpl, which extends PendingOrderImpl, and PendingOrderLineItemTestImpl, which extends PendingOrderLineItemImpl. The primitive methods implemented by these classes simply access fields in the class.
The test code defines the TestDomainManager class, which is a subclass of DomainManager, and implements factory methods that create instances of the test implementation classes, such as PendingOrderTestImpl and PendingOrderLineItemTestImpl, and finder methods that search collections of test objects.
The class PendingOrderImplTests extends JUnit TestCase and implements the test cases for PendingOrderImpl. The majority of the test cases defined by this class verify that PendingOrderImpl correctly implements the state machine shown in Figure 2. PendingOrderImplTests delegates to the PendingOrderTestHelper class, which implements the test cases that are also used to test PendingOrderEntityBean for reasons explained below.
Figure 5 shows the design of the test cases for PendingOrderEntityBean.
Figure 5: PendingOrderEntityBean Test Case Design
The class PendingOrderEntityBeanTests implements the test cases for PendingOrderEntityBean. Since the entity beans are only accessible through local interfaces this class uses Cactus [CACTUS], which makes it relatively straightforward to develop JUnit-based test cases that execute within the server. Of course, this does mean that each time a test case is changed it has to be repackaged and redeployed in the server which is time consuming.
This class implements two sets of tests. The first set of test cases verifies that the persistence mapping is implemented correctly. The second set of tests verifies that the entity bean implements the state machine correctly. Since PendingOrderEntityBeanTests needs to use Cactus it cannot straightforwardly inherit the test cases for the PendingOrderEntity state machine from PendingOrderImplTests. Instead, the test cases are shared by using delegation. The test methods of both classes that implement the state-based tests create and delegate to an instance of the class PendingOrderTestHelper, which implements the shared test cases. The object under test is passed to PendingOrderTestHelper via its constructor.
The test cases for PendingOrderEntityBean rely on the database being initialized with a set of test data consisting of restaurants with menu items.
Benefits and Drawbacks of this Approach
This approach has a number of benefits:
- Faster development - the edit-compile-test-debug cycle is considerably quicker for POJO classes than it is for EJB components. This is because it avoids the time consuming EJB deployment step, which must be done whenever a server-side class (including test cases) are changed. Avoiding this step accelerates the "edit-compile-test-debug" cycle and enables the developer to focus on the task at hand without having to deal with distracting pauses.
- Increased potential for reusability and adaptability - the code that implements the business logic is separate from the entity beans and so can be used in applications that use a different persistence mechanism. Developers can defer making the decision to use entity beans until later in the project and, if necessary, switch to an entirely different (non-EJB) persistence mechanism. Also, developers can use the abstract classes to implement non-persistent components that are, for example, stored in the HttpSession.
- Developers can focus on solving one problem at a time - since the POJOs implements business rules and the EJBs implement persistence, the developer is able to first focus on the development of the business logic without having to simultaneously worry about the complexities of persistence, database schema design, etc.
- Improved testability - this approach enables the domain model, the session facade and the presentation tier to be tested without a working database. This is accomplished by using a domain model that is configured to use a set of non-persistent test classes.
There are the following drawbacks:
- The cost of increased flexibility is generally more complexity. In this case, the complexity comes in the form of more classes - instead of a single EJB there are extra classes and interfaces.
Developers of J2EE applications are often obsessed with the designing the J2EE components for their application. They tend to overlook regular Java classes, a.k.a Plain Old Java Objects. This is unfortunate since POJOs can be quicker to develop, test and debug because they can be executed outside of the web/EJB container.
This article shows how to structure an application's domain model into two levels, the POJO level, which implements the business logic, and entity bean level, which is only responsible for persistence. Developers can first implement and test and the POJO level and then once that is working, implement the entity bean level.
This approach has a number of benefits. It enables developers to focus on one problem at a time by separating business logic from persistence. It accelerates the development of the business logic by eliminating the costly EJB redeployment step. It enables the domain model, session facade and presentation tier to be tested without a functioning database. Finally, it improves reuse and adaptability by decoupling business logic from EJB technology.
About the Author
Chris Richardson is a freelance software architect and mentor who specializes in J2EE application development. He has over 15 years of experience developing object-oriented software systems in a wide variety of domains including online trading, mobile system provisioning, telecommunications and e-commerce. He can be reached at firstname.lastname@example.org
- [CACTUS] - http://jakarta.apache.org/cactus/index.html
- [EJBPATTERNS] - EJB Design Patterns: Advanced Patterns, Processes, and Idioms, Floyd Marinescu
- [FOWLER] - http://martinfowler.com/isa/index.html
- [GOF] Design Patterns, Eric Gamma, et al
- [JUNIT] - www.junit.org
- [MARCO] - Peopleware: Product Projects and Teams, Tom Demarco and Timothy Lister