Model with Immutable objects


Author & Copyright 2006 David Leangen, licensed under the Apache License ver 2.0

This document describes our experiences with developing a business model under OSGi.

Notice

Qi4j renders all this not only obsolete, but total crap. Whatever you do, do not do what's described here.

For historical purposes, we should probably keep this page so we can show how much Qi4j improves the coding process. Funny that we actually used to think that this method has any merit.

The goal is to share these experiences with others in the hope of developing a "best practice". Comments are most welcome.

Overview

The goal of this theorectical discussion is to develop a "best practice" approach to developing a business model within the framework of an OSGi application.

Specifically, we want to make the model is robust and error-free by ensuring that it is:

  • immutable
  • thorougly tested
  • easily refactorable
  • quick to develop

This document explains the approach we used to attain these goals?

Note that we divide "business model" into two parts: the model that contains the system's state (most closely related to the concept of a value object) and the operations conducted on the model that change the model's state. There is a bit of an overlap between the two, so the distinction is not always clear.

In this document, we concentrate only on the model's state.

General Approach

As required by OSGi, the business model is divided into an interface and implementations. In order to maintain immutability (as described below), our interface will only provide getters to each of the fields in each domain object. Furthermore, the actual instantiated business model object is also to be immutable.

There are two means of instantiating such an immutable object: by using a builder, to be provided with the model implementation, or by using a separate bundle that interacts, for instance, with the persistence layer, and the feeds the model implementation. We explain both of these approaches in more detail below.

Model Bundle

The model bundle provides the definition for the business model, as well as the immutable implementation and a model builder.

The guidelines we provide here are intended to keep the model as flexible as possible. In an ideal world, once created, the business model interface should never be modified, as this has several side effects that require refactoring (read effort and cost).

Within our OSGi application, the model will likely be used in many places. We want to encapsulate all the state logic and validation into this model bundle so we do not have to worry about it elsewhere in the application.

Interface

The model interface must be defined once, and only once. The same interface is to be used throughout the application, wherever the model is required.

The interface must contain only getter methods. There must be exactly one getter method for every field in every model object, and each getter method must not require any parameters.

To the greatest extent possible, the interface should be self-sufficient and should not require any imports or third-party packages. Such dependencies are susceptible to change and can add an order-of-magnitude complexity to the model.

Each model object should extend a super interface, such as ModelObject.

The interface should provide a service exception (for instance, ModelException).

The model service should be the only service provided by the model (other than the builder, described below) and should be no more complex than the following:

package com.example.model;

public interface ModelService
{
    <T extends ModelObject>T getModelObject( T valueObject )
        throws ModelException;
}

We explain this service in more detail below.

No searching, finding, or processing services should be provided in this bundle. We want to strictly limit the scope of the bundle to the business model itself. Anything else is to be defined in a separate bundle, that is beyond the scope of this document.

Immutable Implementation

The implementation of the model interface must be immutable. As described in various references list here, making an object immutable drastically reduces the probability of errors elsewhere in the application. Since in our OSGi application, we do not know in advance what will be done with our object model, it is better to err on the strict side of immutability.

The implementation must not share the same namespace as the interface. We recommend keeping the same package name as the interface, and appending it with ".impl".

Each implementation class must be immutable. This means:

  1. we only provide getters, no setters
  2. the values of the object can only be set once, when the object is instantiated, which means that
  3. the values are injected into the object's constructor, which implies that
  4. the object does not have a default (no arg) constructor

Furthermore:

  1. the class is final
  2. each method is final

And:

  1. the class overrides equals() and hashCode(), and in some cases compareTo()

Note also that to keep the object immutable, a getter for an object field must itself return a new, immutable copy of the field. Note the following examples:

public final Date getDateCreated()
{
     // Defensive copy to keep object immutable
      return new Date( m_dateCreated.getTime() );
}

public final Set<String> getOwners()
{
     return Collections.unmodifiableSet( m_owners )
}

Since the object is immutable, each of the values must first be validated before populating the objects upon instantiation. We could either create some kind of validated value object, or inject a builder object. We chose to simply inject a value object and leave the responsibility of ensuring the values are valid to the developer. Although the constructor is public, it is only visible in the implementation of the bundle. So long as the developer creates the model objects responsibly, this approach will work.

Here is a complete example. First, the model interface:

package com.example.model;

public interface Vehicle
{
    String getName();
    Date getDateCreated();
    List<String> getOwners();
}

Now, the implementation:

package com.example.model.impl;

[imports]

public final class VehicleImpl
        implements Vehicle
{
    private String m_name;
    private Date m_dateCreated;
    private List<String> m_owners;

    /**
     * The constructor does not validate the values; it is intended
     * to be used only by a builder object.
     */
    public VehicleImpl( final Vehicle valueObject )
    {
        m_name = valueObject.getName();
        m_dateCreated = valueObject.getDateCreated();
        m_owners = valueObject.getOwners();
    }

    public final String getName()
    {
        return m_name;
    }

    public final Date getDateCreated()
    {
        // Defensive copy to keep object immutable
        return new Date( m_dateCreated.getTime() );
    }

    public final List<String> getOwners()
    {
        return Collections.unmodifiableList( m_owners );
    }

    public boolean equals( final Object o )
    {
        ...
    }

    public int hashCode()
    {
        ...
    }
}

Builder

The role of the builder is to create these immutable model objects. Therefore, the builder must contain enough domain logic to "know" whether or not the state of the object is valid.

We will create a new package for our builder:

  • interface: com.example.model.builder
  • implementation: com.example.model.builder.impl

Again, as is standard in OSGi, we divide the builder service into interface and implementation. We place the interface into the package com.example.model.builder. For each object in the domain model, we provide a builder. This is a bit tedious, but since each object has its own logic and structure, we see no other way to proceed.

Each field would get its own builder method, making our Vehicle object look like the following:

package com.example.model.builder;

public interface VehicleBuilder
    extends ModelObjectBuilder
{
    VehicleBuilder setName( String name )
        thows IllegalArgumentException;


    VehicleBuilder setDateCreated( Date dateCreated )
        thows IllegalArgumentException;
    VehicleBuilder addOwner( String owner )
        thows IllegalArgumentException;

    Vehicle createObject()
        thows IllegalStateException;
}

And, of course, our service definition is:

public interface BuilderService
{
    <T extends ModelObjectBuilder>ModelObjectBuilder getBuilder( Class<T> builderType )
            throws BuilderException;
}

Notes:

  • we explicitly throw IllegalArgumentException
  • each method returns "this". Not essential, but allows us to do the following:
    builder.addName( "name" ).addOwner( "owner1" ).addOwner( "owner2 ");
    
  • as with our model objects, there is one super interface ModelObjectBuilder
  • if a field is itself a model object, the parameter is a builder object. For example:
    public interface GarageBuilder
        extends ModelObjectBuilder
    {
        VehicleBuilder setName( String name )
            thows IllegalArgumentException;
        VehicleBuilder addVehicle( VehicleBuilder vehicleBuilder )
            thows IllegalArgumentException;
    
        Garage createObject()
            thows IllegalStateException;
    }
    
  • each builder must have a createObject method (as defined in the super interface)

Making the builder implementation in an efficient, reusable way is the most useful part of the exercise. We are still experimenting with this.

We are trying to chain reusable validation objects. The validation objects define, for instance, if the field may be null, what the range of the value must be, and so forth.

In the builder implementation, each field may only be set once, or it will throw an IllegalArgumentException. Some values MUST be set, while others may remain unset. The builder "decides" what the default value is and injects the value into the model obejct above.

If any of the objects that must be set are not yet set when createObject() is called, an IllegalArgumentException is thrown.

Also, createObject() may be called once and only once. Otherwise, an IllegalStateException is thrown.

From the outside, other bundles would create a validated, immutable model object from the model object service like this:

final BuilderService builderService = serviceManager.getService( BuilderService.class );
final VehicleBuilder vehicleBuilder = builderService.getBuilder( VehicleBuilder.class );
vehicleBuilder.setName( "car" ).addOwner( "me" ).setDate( ... );
final Vehicle vehicle = vehicleBuilder.createObject();

And voila! We have a validated, safe, immutable object that we can throw around in other bundles without worry.

We will post more on the builder implementation and validation filters when we are happy with the results.

Other Creation Methods

Of course, model objects can be created in other ways. For instance, users can enter values into a GUI, which would create or modify the state of the model. That could later be persisted into a database.

Here, we explain very briefly how this approach relates to the persistence layer. The principle is the same in any other circumstance as well.

In our case, an object's state is persisted in a database. We create a separate bundle whose sole role is to persist the model and unmarshal persisted objects into a validated, immutable state. We create a separate bundle for several reasons:

  • we want to keep our model bundle independent
  • we may want to have several implementations (for different persistence mechanisms, or whatever)
  • we are already constrained enough by the persistence layer implementation, so we don't want to create more "rules" or unnecessary complexity by mixing things with our model implementation
  • we want to hide as much as possible the persistence details from higher levels of the application

Remeber that one of the main goals of the model bundle is to encapsulate object validation. Spreading out validation into different bundles is highly prone to error, especially as the system grows. So, we don't care about object validation in our persistence bundle. This means that other than the rules defined by the persistence tool or method we use, we are free to implement the model interface however we want. It could be as simple as:

package com.example.model.persistence;

public class VehicleImpl
    implements Vehicle
{
    public String name;
    public Date dateCreated;
    public List<String> owners;

    public String getName()
    {
        return name;
    }

    Date getDateCreated()
    {
        return dateCreated;
    }

    List<String> getOwners()
    {
        return owners;
    }
}

Here, we have no validation and the object is unsafe. But, we don't care.

Under the hood, when we retrieve an object from the DB, recalling our model service from above, we would do something like:

final Vehicle persistedVehicle = dbService.findVehicle( "car" );
final Vehicle validatedVehicle = modelService.getModelObject( persistedVehicle );

Our ModelService, using a combination of a factory and our builder service, returns a validated, immutable model object with the values retrieved from the DB.

Testability

Some basic tests that can applied generally. By "standardizing" our approach to model building, in many cases the test patterns can be automated, reducing the time required to build the model object.

To be continued...

Refactorability

The automated tests mentioned above are a major factor that allow us to refactor our model without any problems.

To be continued...

Quick to Develop

Doing all the above by hand is a bit tedious Therefore, need to somehow be automated.

To be continued...

Conclusion

The domain model we used while developing the above approach was relatively complex. Though complex, it seemed to give relatively good results. The implementation bundles are reliable, which we believe should be a major design goal of any OSGi bundle.

The only complaint we have with this approach is that it can be a bit time-consuming. By automating some or even most of the above, the time factor could be greatly reduced, while simultaneously reducing the number of bugs inserted into the model implementation.