Tuesday, November 22, 2005

The Extensible Enum Pattern in Java

There is a long history of the enum pattern in Java. What I have seen and used several times on different projects is a variant which we called the Extensible Enum Pattern. My fellow ex-ThoughtWorker, Andrew McCormick, first showed it to me.

Here is what I wrote the PCGen developer's list about this pattern when the question arose about processing program-defined and user-defined tokens together:

We have a set of conditions based on integer codes, some which we define themselves, others which are read from 3rd-party files and cannot all be known ahead of time.

The basic class for our extensible enum looks like this:

public class Status {
    private final int code;
    private final String remark;

    private Status(final int code, final String remark) {
        if (null == remark) throw new NPE;

        this.code = code;
        this.remark = remark;
    }
}

Ok, so the object is immutable: there are no setters or editable fields. That is the first step towards being an enum. And so far there are no public way to create new ones. Lets open the class and add a factory method:

    private static final Map CODES = new HashMap();

    public static Status create(final int code, final String remark) {
        final Integer key = new Integer(code);
        final Status status;

        if (!CODES.containsKey(key))
            CODES.put(key, status = new Status(code, remark));
        else
            status = (Status) CODES.get(key);

        return status;
    }

Now we have the other half of being an enum: no two instances of the same value are distinguishable. Whenever you try to create a new enum, if there is already an instance with the same code, you get back that instance rather than a new instance. (However, as the remark is just satellite data, not part of the identity of Status, you can see that you also get back the original remark.) Let's reinforce this message, and open the class again:

    public int hashCode() {
        return code;
    }

    public boolean equals(final Object o) {
        if (this == o) return true;
        if (null == o) return false;
        if (getClass() != o.getClass()) return false;

        return code == ((Status) o).code;
    }

Lastly, let's open the class one final time to prepopulate it with our defined instances (be careful to initialize CODES before creating static instances or you'll get a NPE when trying to access the cache):

    public static final Status SUCCESS = create(0, "Success");
    public static final Status BARNEY_DIED = create(-10,
            "Barney died: please reinsert purple dinosaur.");
    public static final Status OLL_KORRECT = create(10,
            "All your answers are correct!");

Now in code, I use Status like this:

    if (somethingHorribleHappened())
        return Status.BARNEY_DIED;
    else
        return Status.create(readCodeFromElsewhere(),
                "Out of body experience: external status");

So that we got well-defined static instances for internal codes, and can generate new instances as required for external codes.

At bottom, I'll paste in a JDK5 version (generics, auto-boxing, annotations) along with some business methods. Note that the new JDK enum feature is not extensible. Otherwise, it is a thing of beauty, and in fact my next revision of this class for my client will model the JDK enum class more closely but retain the extensible property.

And lastly, the same thing updated for JDK5 and with some comments:

import java.util.HashMap;
import java.util.Map;

/**
 * {@code Status} represents extensible enums for status codes.
 *
 * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a>
 */
public class Status {
    private static final Map<Integer, Status> CODES
            = new HashMap<Integer, Status>();

    /**
     * An unremarkable success.
     */
    public static final Status SUCCESS = create(0, "Success");
    /**
     * An unknown failure.
     */
    public static final Status UNKNOWN_FAILURE = create(-1, "Unknown failure");

    /**
     * Creates a new {@code Status} or reuses an existing instance for a given
     * <var>code</var>.
     *
     * @param code the status code
     * @param remark the status remark
     *
     * @throws NullPointerException if <var>remark</var> is {@code null}
     */
    public static Status create(final int code, final String remark) {
        final Status status;

        if (!CODES.containsKey(code))
            CODES.put(code, status = new Status(code, remark));
        else
            status = CODES.get(code);

        return status;
    }

    private final int code;
    private final String remark;

    private Status(final int code, final String remark) {
        if (null == remark) throw new NullPointerException();

        this.code = code;
        this.remark = remark;
    }

    public int getCode() {
        return code;
    }

    public String getRemark() {
        return remark;
    }

    /**
     * Checks if a success or failure status.  All non-negative codes
     * are successful.
     *
     * @return {@code true} if a success status
     */
    public boolean isSuccess() {
        return 0 <= code;
    }

    /**
     * Checks if {@link #getRemark()} is meaningful.  All non-zero codes
     * are remarkable.
     *
     * @return {@code true} if {@link #getRemark()} is useful
     */
    public boolean isRemarkable() {
        return 0 != code;
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Returns <em>"(code) remark"</em>.
     */
    @Override
    public String toString() {
        return "(" + code + ") " + remark;
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Considers only {@link #code} for equality.
     */
    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (null == o) return false;
        if (getClass() != o.getClass()) return false;

        return code == ((Status) o).code;
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Returns {@link #code} as the hash code.
     */
    @Override
    public int hashCode() {
        return code;
    }
}
Post a Comment