Skip to content
Cameron Beccario edited this page Jun 21, 2013 · 18 revisions

TL;DR?

See the README.

Otherwise, read on! Lots of documentation is better than none. The following pages will also be helpful:


Introduction

What is Grains?

Grains is a small Java framework for generating immutable, thread-safe, versionable objects.

Right. What does that mean?

Immutability is a widely accepted best practice that eliminates entire classes of software problems, especially for concurrent programs. Immutable objects are inherently thread-safe and allow the developer to focus on the separation of value, identity, and state. For a discussion of these concepts, watch this excellent presentation by Rich Hickey, starting at 7:24.

Versionable objects are capable of evolving their schema over the runtime life of a program. This is useful when scenarios require the addition of new properties at runtime, or transferring different versions of objects between two parties without losing data. Furthermore, immutability provides another facet of versioning: each time an object changes, a snapshot of its state can be retained, providing an audit trail of changes.

The Grains framework formalizes these concepts with the grain contract.

What is a grain?

A grain is an immutable map of String to Object that contains permanent keys, i.e., keys that cannot be removed from the map. This set of permanent keys is called the basis of the grain, and all instances of a particular grain implementation share the same basis. Also, each basis key restricts the types of values it can be associated to.

This pattern should appear familiar as it's roughly analogous to the more traditional OO concept of classes and fields: all instances of a class share the same set of fields, and each field stores values only of a particular type. The Grains framework takes this one step further by making objects self-descriptive, and extensible, through their map behavior.

Imagine a JavaBean that exposes its properties not only as traditional getters but also as entries in a map. Such an object would be both statically-typed and dynamically self-descriptive without having to resort to Java reflection. A grain is essentially this--a JavaBean crossed with a map.

Unlike a JavaBean, however, a grain is not limited to just basis associations. As with normal maps, grains can freely store arbitrary key-value pairs. To differentiate them from the basis, these additional entries are called "extensions".

What other benefits do grains provide?

As required by the Map contract, .equals and .hashCode have well defined behavior. And because grains are immutable, the .clone, .wait, and .notify methods serve no purpose. Even an implementation of .toString is provided. In fact, using grains means not needing to care about any method inherited from Object.

Because writing a grain implementation by hand is error-prone and time consuming, the Grains framework uses code generation to do the heavy lifting.

What does a generated grain look like?

The generator takes as input a simple "schema" interface comprised of getter methods:

@GrainSchema
public interface Order {

    String getProduct();

    int getQuantity();
}

... and outputs Java source code for a grain that implements the schema:

public interface OrderGrain extends Order, Grain {

    String getProduct();
    OrderGrain withProduct(String product);

    int getQuantity();
    OrderGrain withQuantity(int quantity);
}

There are few things to notice here:

  • The @GrainSchema annotation enables code generation by allowing the generator to identify interfaces that need processing.
  • The resulting OrderGrain is an interface, not a class. This lets the actual implementation be private and improve over time without breaking public APIs. More importantly, an interface has the ability to extend multiple interfaces, allowing for interface composition.
  • The grain's map behavior is inherited through the Grain interface (which extends Map<String, Object>).
  • "with" accessor methods have replaced the traditional set accessor methods normally seen on a JavaBean or POJO. Because grains are immutable, setters have no meaning. Instead, the "with" accessors produce a new instance containing the desired value, leaving the original instance unchanged. The popular Joda-Time library uses this pattern.

Finally, notice the grain itself represents a merging of two views of the object: a statically-typed view accessed through getters/"with" methods, and a dynamic self-describing view accessed through the map interface. Here is a simplified type hierarchy showing how the generated grain represents the merger of these two views:

Order     Map<String, Object>
  |              |
  |            Grain
  |              |
  +--OrderGrain--+

See the Schema Writing Guide for more information.

What else is generated?

The builder pattern is a common pattern for constructing instances of immutable objects, and this pattern is used to simplify grain construction. Just as with grains, the generator outputs Java source code for a grain builder that implements the schema:

public interface OrderBuilder extends Order, GrainBuilder {

    String getProduct();
    OrderBuilder setProduct(String product);

    int getQuantity();
    OrderBuilder setQuantity(int quantity);

    OrderGrain build();
}

A grain builder is the mutable analog to a grain, so rather than "with" methods, a builder has setters. It defines an additional method, build(), which constructs grain instances from the current state of the builder. Also like grains, the builder's map behavior is inherited through the GrainBuilder interface (which extends Map<String, Object>). Again, a simplified type hierarchy:

Order       Map<String, Object>
  |                |
  |           GrainBuilder
  |                |
  +--OrderBuilder--+

How are builders instantiated?

To construct instances of builders, a factory pattern is generated:

public enum OrderFactory implements GrainFactory {
    INSTANCE;

    public static OrderBuilder newBuilder() { ... }
    public OrderBuilder getNewBuilder() { return newBuilder(); }

    ...
}

Factories implement the GrainFactory interface and follow the singleton enum pattern. The combination of these two approaches allow the factory to be used either statically or polymorphically:

    // Static access:
    OrderBuilder builder = OrderFactory.newBuilder();

    // Or something a bit more dynamic:
    Class<?> clazz = Class.forName("com.acme.model.OrderFactory");
    GrainFactory factory = (GrainFactory)clazz.getEnumConstants()[0];
    GrainBuilder builder = factory.getNewBuilder();

Next Steps

Acknowledgements

Clojure's defrecord macro provided the main inspiration for grains.

Clone this wiki locally