Chapter 5. Domain Objects

Domain Objects Tutorial

This tutorial explains how to build a domain object containing application logic that operates over persistent the section called Data Objects Tutorial in Chapter 2.

Introduction

Domain objects contain logic particular to a business domain, such as managing users or credit cards. This logic operates over a persistent data object that contains the state necessary to implement the domain specific logic. Domain objects contain the application logic specific to the domain that require representing persistent state. All other application logic should be put in stateless process objects or class utilities.

This pattern is part of an overall layered architecture (Fowler, 2001). A layered architecture separates user interface logic into a presentation layer that depends on an application logic layer, which in turn depends on a persistence layer. Separating the code into these layers provides a level of encapsulation that makes the code more transparent and easier to maintain. For example, you can replace the layer that handles the application logic without touching the persistence or presentation layers.

Introducing Domain Objects

Domain objects are Java classes that perform operations on data objects and provide data for presentation. To facilitate the construction of domain objects, a number of base classes are provided. Every domain object in Red Hat Web Application Framework extends either the DomainObject or ObservableDomainObject classes. Refer to the class diagram below for the API.

The purpose of the DomainObject class is to encapsulate access to one or more types of data objects. A number of methods, such as the save and delete methods, are delegated to the data object. The delegation pattern means that a method of one object is implemented by relying completely on a method of another object. In this case, the domain object, part of the application logic layer, is acting as a facade for an operation of the data object, part of the persistence layer (Gamma, 1996). This is an interface between the persistence and application logic layers, and one point where the two layers connect. However, because the implementation of the save method is hidden from the user, it can be changed without affecting clients of the method. This demonstrates how a layered design can provide upgradability and maintainability.

The ObservableDomainObject adds additional methods to the DomainObject. The observable domain object is capable of notifying an observer when changes occur. If you want to offer this ability to clients of your object, you need to extend this class. Red Hat Web Application Framework contains another class, the ACSObject class, that extends ObservableDomainObject. The purpose of the ACSObject is to serve as a base class for use in common object-level services, such as categorization and permissioning. The ACSObject domain object provides this by relying on a special ACSObject data object. The ACSObject data object provides a unique id and an objectType attribute. The unique id attribute is used to uniquely identify an object among all other ACSObjects. The objectType attribute is used to determine what the object type is. The AuditedACSObject subclass of ACSObject provides auditing services. This class provides methods for determining when and by whom an object was created and last modified.

NoteNote
 

The ACSObject data object is not a Java class. The ACSObject domain object is a Java class that exists to provide access to ACSObject functionality, and delegates some of its calls to the ACSObject data object.

Developers who are building new domain objects need to subtype from one of the following four base classes. Refer to the summary below in choosing which to subtype from. The following section illustrates how to build a domain object with an example from the Notes domain object.

  • DomainObject: This class provides a minimal facade for a data object. Subtype from this class if you explicitly want to avoid the functionality being offered by the classes below.

  • ObservableDomainObject: This class adds the ability to notify observers of changes to a domain object's underlying data object. If you want your class to avoid being used by ACSObject services, this is the recommended class to subtype.

  • ACSObject: This class subtypes ObservableDomainObject, but adds a guarantee of a unique id and can participate in object level services, such as categorization and permissioning. This is the usual class to subtype, unless you also want auditing information. The ACSObject class will automatically set its id when it is first saved, unless an id is manually set before this.

  • AuditedACSObject: Most application objects should choose this as their parent class. It provides all of the advantages of an ACSObject, as well as tracking information of its creation and last modification.

NoteNote
 

There is not necessarily a one-to-one correspondence between a domain object class and a data object type. A domain object may wrap multiple data objects. However, each domain object has a base data object type. When a domain object is created, there is a data object whose type either matches or is a subtype of the base data object type.

Designing the Notes Domain Object

The purpose of the sample application is to allow users to write short notes and categorize those notes. This tutorial will not discuss the user interface aspect of the note application in detail, but you can try running the Notes application at /notes on an installed Red Hat Web Application Framework server.

The information stored for each note is shown in the data objects diagram for Notes below. In addition to the fields shown below, each note must be permissionable and store auditing information.

The Note data object extends the ACSObject data object, so that ACSObject services can access the Note. ACSObjects require storing a unique id and an object type. For the Note data object, the type is com.arsdigita.notes.Note.

NoteNote
 

Although in this example, the data object type name corresponds to the domain object class name, this is not always the case. A data object does not need to be used within a domain object, and a data object may be used by multiple domain objects.

Data objects are useful for storing and retrieving data. However, in order to support other behavioral methods, the data object is used within domain objects. Domain objects are responsible for providing an interface to create, retrieve, update, and delete (the CRUD methods) the Note data object, in addition to any Note-specific methods. Because we are using a layered architecture, users of the Note domain object (the Java class) are not allowed to access the Note data object. The Note data object is a private member variable of the Note domain object. This ensures that all interaction with the Note domain object's internal state is controlled by the methods or API provided by the domain object.

Building the Domain Object

This section will take you step by step through the process of building a domain object.

Starting the Class

A domain object starts off as a standard Java class that extends one of the four domain object base classes. Because of the auditing and permissioning requirements, AuditedACSObject is the base class.

The code sample below illustrates the initial definition of the class and the import statements.

package com.arsdigita.notes;

import com.arsdigita.db.Sequences;
import com.arsdigita.domain.DataObjectNotFoundException;
import com.arsdigita.auditing.AuditedACSObject;
import com.arsdigita.kernel.Stylesheet;
import com.arsdigita.kernel.User;                          (1)
import com.arsdigita.persistence.DataAssociation;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.persistence.PersistenceException;
import com.arsdigita.persistence.OID;
import com.arsdigita.persistence.metadata.ObjectType;

import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.Date;
// Stylesheets
import java.util.Locale;
import java.util.ArrayList;

import org.apache.log4j.Category;

/**
 * Note class.  Extends AuditedACSObject to implement      (2)
 * persistent Note objects.
 *
 * <p>
 * Note: according to current DomainObject docs, APIs are not yet
 * stable. Changes may invalidate the Note class.
 *
 * @author Scott Seago
 **/
public class Note extends AuditedACSObject {               (3)


    private static Category log =                          (2)
        Category.getInstance(Note.class.getName());

    /**
     * BASE_DATA_OBJECT_TYPE represents the full objectType name for the
     * Note class
     **/
    private static final String BASE_DATA_OBJECT_TYPE =    (4)
          "com.arsdigita.notes.Note";
...

    /**
     * Returns the appropriate object type for a Note so that proper
     * type validation will occur when retrieving Notes by  OID
     *
     * @return The fully qualified name of of the base data object
     * type for the Note object type.
     */
    protected String getBaseDataObjectType() {
        return BASE_DATA_OBJECT_TYPE;                      (4)
    }

}
(1)
The AuditedACSObject is imported from the auditing service package.
(2)
Log4j is used as a uniform debugging tool across Red Hat Web Application Framework. The convention for initializing the log4j system is to initialize a category with the name of the current class, as shown here.
(3)
By extending this baseclass, this Java class becomes a domain object. It inherits a data object and methods for manipulating it. Calls to these methods are automatically observed and modifications are logged in the audit trail.
(4)
The data object type for the domain object is declared as a member variable, BASE_DATA_OBJECT_TYPE. It is final and static, so the compiler will replace it with a constant. An inherited function, getBaseDataObjectType, is overridden to return this variable. This method is used by the constructors on the superclasses to verify that the data object matches this type or is a subtype. For more information on this topic, see the constructor section.

Constructors

Before any of the methods of a class can be used, an instance must be constructed in memory. There are several different varieties of constructors for domain objects, all of which are illustrated in the Notes domain object.

There are two different types of constructors for domain objects. One is used for constructing domain objects by creating a new data object. The no-arg constructors, that is, the String typename, and the ObjectType type, are this type of constructor. These constructors use the persistence system to create a new data object that matches the type passed as an argument.

The other type of constructor relies on retrieval of an existing data object to construct the domain object. The first of these takes an Object ID or OID as input, and then retrieves a data object corresponding to that OID. An OID is comprised of two pieces: a BigDecimal numeric ID and an object type identifier. The second constructor takes a data object as input and constructs a DomainObject to wrap it. This constructor does not need to construct the underlying data object, so it is preferable to use this constructor if the data object has already been loaded into memory.

These constructors exist in DomainObject, ObservableDomainObject, ACSObject, and AuditedACSObject. Subclasses should replicate these constructors, unless there is a good reason to restrict them. The comments below explain the overall utility of each constructor, and why each subclass should use them. Additional constructors can be added, and this is encouraged if it makes the class more convenient to use.

NoteNote
 

Be aware that none of the constructors will change the persistent state of the DomainObject or its data object. To store a domain object's properties in the database, the save method must be called. To delete an DomainObject, the delete method must be called.

public class Note extends AuditedACSObject {
    /**
     * BASE_DATA_OBJECT_TYPE represents the full objectType name for the
     * Note class
     **/
    private static final String BASE_DATA_OBJECT_TYPE = 
       "com.arsdigita.notes.Note";

    /**
     * Default constructor. The contained DataObject is
     * initialized with a new DataObject with an
     * ObjectType of "Note".
     *
     * @see AuditedACSObject#AuditedACSObject(String)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.metadata.ObjectType
     **/
    public Note() {                                        (1)
        this(BASE_DATA_OBJECT_TYPE);
    }

    /**
     * Constructor. The contained DataObject is
     * initialized with a new DataObject with an
     * ObjectType specified by the string
     * typeName.
     *
     * @param typeName The name of the ObjectType of the
     * contained DataObject.
     *
     * @see AuditedACSObject#AuditedACSObject(ObjectType)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.metadata.ObjectType
     **/
    public Note(String typeName) {                         (2)
        super(typeName);
    }

    /**
     * Constructor. The contained DataObject is
     * initialized with a new DataObject with an
     * ObjectType specified by type.
     *
     * @param type The ObjectType of the contained
     * DataObject.
     *
     * @see AuditedACSObject#AuditedACSObject(ObjectType)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.metadata.ObjectType
     **/
    public Note(ObjectType type) {
        super(type);                                       (3)
    }


    /**
     * Constructor. Retrieves a Note instance, retrieving an existing
     * note from the database with OID oid. Throws an exception if an
     * object with OID oid does not exist or the object 
     * is not of type Note
     *
     * @param oid The OID for the retrieved
     * DataObject.
     *
     * @see AuditedACSObject#AuditedACSObject(OID)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.OID
     *
     * @exception DataObjectNotFoundException Thrown if we cannot
     * retrieve a data object for the specified OID
     *
     **/                                                   (4)
    public Note(OID oid) throws DataObjectNotFoundException {
        super(oid);
    }
...
}
(1)
The empty constructor is used to construct a new Note with its default data object type. This is the common case for constructing a new Note, where a specific subtype of domain object is not needed and an existing Note is not being retrieved.
(2)
The typeName specified in this constructor must be a String that names an object type. This does the same thing as the constructor that takes an object type, but is more convenient, because you only need to specify a String with the type name.
(3)
This constructor takes an ObjectType that matches the base object type, in this case, com.arsdigita.notes.Note or a subtype. The constructor requiring an ObjectType can be used to specify a specific subtype of the Notes data object. This is useful if the data object may be used with different domain objects that have different methods. In most cases, the empty constructor, which defaults to the base object type, is sufficient, and any reason to use the other constructors should be specified. Even if the Note domain object will not be used with multiple object types, it is necessary to have these constructors for a subclass to provide this functionality. Without providing the constructors on every class, a subclass cannot rely on the constructor chain to reach the constructors on ACSObject and DomainObject.
(4)
This constructor is used to construct a Notes domain object with a Notes domain object that has already been created. The OID is used to retrieve an instance of the correct domain object. The retrieved data object can then be accessed using the set/get methods and all other methods defined.

The role of each constructor can be clarified by examining how the constructors are implemented in DomainObject, the root base class for all domain objects.

public abstract class DomainObject {
    private final DataObject m_dataObject;

    /**
     * Constructor. The contained DataObject is
     * initialized with a new DataObject with an
     * ObjectType specified by the string
     * typeName.
     *
     * @param typeName The name of the ObjectType of the
     * new instance.
     *
     * @see com.arsdigita.persistence.Session#create(String)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.metadata.ObjectType
     **/
    public DomainObject(String typeName) {
        Session s = SessionManager.getSession();           (1)
        if (s == null) {
            throw new RuntimeException("Could not retrieve a session 
                from " + "the session manager while instantiating " +
                "a class with ObjectType = " + typeName);
        }

        m_dataObject = s.create(typeName);
        initialize();
    }

    /**
     * Constructor. The contained DataObject is
     * initialized with a new DataObject with an
     * ObjectType specified by type.
     *
     * @param type The ObjectType of the new instance.
     *
     * @see com.arsdigita.persistence.Session#create(ObjectType)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.metadata.ObjectType
     **/
    public DomainObject(ObjectType type) {
        Session s = SessionManager.getSession();           (1)
        if (s == null) {
            throw new RuntimeException("Could not retrieve a session 
                from " + "the session manager while instantiating " +
                "a class with ObjectType = " + type.getName());
        }

        m_dataObject = s.create(type);
        initialize();
    }

    /**
     * Constructor. The contained DataObject is retrieved
     * from the persistent storage mechanism with an OID specified by
     * oid.
     *
     * @param oid The OID for the retrieved
     * DataObject.
     *
     * @exception DataObjectNotFoundException Thrown if we cannot
     * retrieve a data object for the specified OID
     *
     * @see com.arsdigita.persistence.Session#retrieve(OID)
     * @see com.arsdigita.persistence.DataObject
     * @see com.arsdigita.persistence.OID
     **/
    public DomainObject(OID oid) throws DataObjectNotFoundException {
        Session s = SessionManager.getSession();
        if (s == null) {
            throw new RuntimeException("Could not retrieve a session (2)
                from " + "the session manager while instantiating " +
                "a class with OID = " + oid.toString());
        }

        m_dataObject = s.retrieve(oid);
        if (m_dataObject == null) {
            throw new DataObjectNotFoundException
                ("Could not retrieve a DataObject with " +
                 "OID = " + oid.toString());
        }
        initialize();
    }

    /**
     * Constructor. Creates a new DomainObject instance to encapsulate 
     * a given data object.
     *
     * @param dataObject The data object to encapsulate in the new domain
     * object.
     * @see com.arsdigita.persistence.Session#retrieve(String)
     **/
    public DomainObject(DataObject dataObject) {
        m_dataObject = dataObject;
        initialize();
    }                                                      (2)

    /**
     * Returns the base data object type for this domain object class.
     * Intended to be overridden by subclasses whenever the subclass will
     * only work if their primary data object is of a certain base type.
     *
     * @return The fully qualified name ("modelName.typeName") of the base
     * data object type for this domain object class,
     * or null if there is no restriction on the data object type for
     * the primary data object encapsulated by this class.
     **/
    protected String getBaseDataObjectType() {
        return null;
    }

    /**
     * Called from all of the DomainObject constructors
     * to initialize or validate the new domain object or its
     * encapsulated data object.  This was introduced in order to
     * support efficient validation of the encapsulated data object's
     * type.  If the validation is typically performed in class
     * constructors, then redundant validation is performed in
     * superclass constructors.  This validation now occurs here.
     **/
    protected void initialize() {
        if (m_dataObject == null) {
            throw new RuntimeException
                ("Cannot create a DomainObject with        (3)
                   a null data object");
        }

        String baseTypeName = getBaseDataObjectType();
        if (baseTypeName == null) {
            return;
        }
        // ensure data object is instance of baseTypeName 
        // or a subtype thereof.
        ObjectType.verifySubtype(baseTypeName, 
                                 m_dataObject.getObjectType());
    }
...
}
(1)
These constructors are used to create an empty data object. The Session class is used look up the metadata that defines the object type and returns the data object. Because the data object is a private member variable, it is denoted m_dataObject.
(2)
These two constructors are used to create a DomainObject when a data object already exists. The OID-based constructor searches for a data object with the given OID. If the object cannot be found, a DataObjectNotFound exception is thrown. Otherwise, the DomainObject is initialized with the given data object. The data-object-based constructor is used if the data object is available, so that an additional one does not need to be retrieved from the database.
(3)
The initialize() is called after the domain object instance is created. One use of this function is to validate the data object type to see if it is the same type or a subtype of the base data object type. If not, an exception is thrown. In order for this to work, your subclass must implement getBaseDataObjectType(), as in the Notes example. Subclasses of DomainObject can add their own initialization logic, but should always call super.initialize() in the first line of the function so that initializations from the superclasses are processed.

Adding Create Methods

The constructors described in the preceding section are intended to be used for creating empty data objects or retrieving existing data objects. However, in some situations, it is useful to have a method that takes in a set of application-specific parameters and creates a domain object for you. See the following example:

/**
 * Creates a new note and sets the title, body, and theme.
 *
 * @param title The title describing the note.
 * @param body  The body of the note.
 * @param theme The theme of the note.
 */
public static Note create(String title, String body, NoteTheme theme) {
    Note note = new Note();
    note.setTitle(title);
    note.setBody(body);
    note.setTheme(theme);
    return note;
}

This method is static, so it can be called without an instance of a Note. It constructs a new note and then sets the title, body, and theme properties to the values passed into the method. It then returns the constructed Note object. The Note's data is not saved in the database by the constructor. Such data is not persisted until the save method is executed.

Domain Object Methods

Several methods are inherited from DomainObject that are useful for building other methods for subclasses. In general, methods are added to subclasses to provide application logic specific to a business domain. A common case of this are accessors and mutators for data object properties. Such methods are usually called by process objects or UI components.

Accessors and Mutators

Because DomainObjects wrap data objects, accessors and mutators for the domain object properties are common methods to provide on a domain object. The DomainObject class provides a set and get that are automatically delegated to the data object member of DomainObject. The set method is used to set properties of the domain object to a value. The get method is used to retrieve the values of those properties.

Using the Note domain object, the following example demonstrates how to implement an accessor and mutator for the contained data object's property.

/**
 * Retrieve the title of the note.
 *
 * @return The title.
 */
 public String getTitle() {
     return (String) get("title");
 }

/**
 * Set the title of the note.
 *
 * @param title A title for the note.
 */
 public void setTitle(String title) {
     set("title", title);
 }

The accessor, getTitle works by calling get with the name of the property to retrieve. The getmethod returns a java.lang.Object, so the return type must be casted to a String. The setTitle method works by calling the set with the name of the property to be set and the value.

The member data object in DomainObject is private to prevent access to the data object outside of the methods, ensuring an interface/implementation barrier. Subclasses of DomainObject cannot access the data object directly but must use methods on DomainObject instead. Because this barrier guarantees that all access to the properties of the data object are through set and get methods, subclasses can add behavior to these methods. For example, the ObservableDomainObject adds behavior to the set method that enables observers of the DomainObject to be notified when a property is changed.

Initialize

The DomainObject class provides an initialize method that is executed after the constructor is run. The Domain Subclasses can override this method and add further initialization logic. However, the initialize method on the superclass should be run to ensure that all inherited member variables are initialized and so that the data object type checking is executed.

A good use of the initialize method is to set initial values for data object properties. The ACSObject class uses the initialize method to determine the value for the ObjectType property.

/**
 * Called from base class constructors (DomainObject constructors).
 */
protected void initialize() {
    super.initialize();
    if (isNew()) {
        String typeName =
            getObjectType().getModel().getName() +
            "." + getObjectType().getName();

        set("objectType", typeName);
    }
}

Conventions

  1. Every domain object with a public constructor should have a public final static String BASE_DATA_OBJECT_TYPE variable with the value equal to the primary data object type used by the domain object. The common use for the BASE_DATA_OBJECT_TYPE variable is to construct an OID for the data object given its numeric id. For convenience, you can provide a no-arg constructor that will dispatch to the object type constructor with the BASE_DATA_OBJECT_TYPE variable.

  2. The protected method getBaseDataObjectType is required to provide proper data object type checking. In the common case, the method returns the BASE_DATA_OBJECT_TYPE variable. The DomainObject initialize method ensures that the data object used to create a DomainObject is equal to that type, or a subtype of the object type returned by this method. For example, if you try to instantiate an ACSObject with a data object that is not a subtype of the ACSObject data object type, you will get an exception. By default, this method returns null, which disables the type checking

Bibliography