Effective Java discusses two variants of the type-safe enum pattern, one that allows subclassing and one that does not. The enum language construct added in JDK 5 only provides the non-subclassing variant (because supporting subclassing would have confusing interactions with switch statements and other enum features). However, having the enum class implement a mixin interface can restore some of the third-party extensibility of the subclassing variant. (In Java, a mixin interface is used to capture some secondary behavior a class can provide; in other languages, mixins can carry implementations too.)
During JSR 199, Peter, Jon, and I were having a meeting discussing a few API design issues. The JavaFileMangager
has a notion of locations of where to look for files. While there are a number of standard locations that needed to be supported for a Java compiler, such as classpath and sourcepath, other kinds of tools need to be able to define their own locations too. So using an enum for the known compiler locations would be structured, but not support the needed extensibility while using a list of known strings to lookup locations would allow extensions, but be very unstructured. The solution we came up with was to use an enum and a name-based lookup.
The solution has a few parts:
Define a simple interface with the needed functionality.
In this case, the JavaFileManager.Location
interface just has two methods, getName
and isOutputLocation
. The first is used to retrieve the key of the location and the second is used by the file manager.
Declare an enum implementing the interface where the enum constants represent the known values. For example, StandardLocation
implements JavaFileManager.Location
and has constants for the classpath, sourcepath, output directory, and other locations already in use by javac
.
Since enum types are classes, they can implement interfaces.
Include a factory method mapping from names to objects implementing the interface. For JSR 199, The locationFor
method maps from strings to locations. A factory method for the interface, unlike the valueOf
method on the enum, is able to support aliasing of names and return objects from types that implement the interface other than the enum. That is, it would be possible for the factory method to map multiple names to the same object rather than just one canonical name. Including a factory isn't strictly necessary, but is would be a bit onerous to force clients to write their own factory or use anonymous classes, etc. to create non-standard values.
This combination of a enum for known values and an interface for extensibility provides a good alternative to string-based provider lookups.
I.E.:
If you have one factory that is fine. You check the super factory, the built in one, and then check for other locations locally. The problem arises when you have two factors that are unrelated - perhaps from two separate third parties.
Now you have no one place to look for locations, you have to check both factories.
Let's see,
As currently written and specified, the implicit map backing locationFor will retain objects. Enums are defined to use identity for equality; that is, .equals is ==. So for other implementations of the interface to interoperate with the enum constants in collections, the other implementations will have to use identity for equality too. A variant of the pattern could require consumers of the objects to rely more on aspects of the objects other than their identity and thereby not necessarily have a persistent map backing the factory.
Some parts of JSR 199 (and 269) API are named at other service providers other than users. So, if I were to write JoesOwnJavaFileManager I could also provide an enum of locations my file manager recognized in addition to the standard ones.