Enum
From APIDesign
(→Infinite Set) |
(→Rewrite Cookbook) |
||
(9 intermediate revisions not shown.) | |||
Line 5: | Line 5: | ||
[[Good]] [[API]] design is about having [[evolution]] plan prepared in advance. One way of evolving an [[Enum]] is to enlarge the set of its constants. As [[Blogs:AndreiBadea:EnumsInAPIs|Andrei points out]] adding new [[enum]] constant is not strictly semantically [[BackwardCompatible]], but this [[evolution]] path has been well thought in advance when [[Enum]]s were designed. | [[Good]] [[API]] design is about having [[evolution]] plan prepared in advance. One way of evolving an [[Enum]] is to enlarge the set of its constants. As [[Blogs:AndreiBadea:EnumsInAPIs|Andrei points out]] adding new [[enum]] constant is not strictly semantically [[BackwardCompatible]], but this [[evolution]] path has been well thought in advance when [[Enum]]s were designed. | ||
- | Each switch statement (over an [[Enum]]) needs to have ''default'' branch. In case new constants are added, the code ends up in there. There are going to be some changes in newer versions - the ''default'' branch may be used more and more often, but overall, there are no surprises. Both sides of the [[API]] contract - the publisher as well as consumer - are ready for this kind of [[evolution]]. | + | Each switch statement (over an [[Enum]]) needs to have ''default'' branch. In case new constants are added, the code ends up in there. There are going to be some changes in newer versions - the ''default'' branch may be used more and more often, but overall, there are no surprises. Both sides of the [[API]] contract - the publisher as well as consumer - are (sort of) ready for this kind of [[evolution]]. |
== Infinite Set == | == Infinite Set == | ||
Line 25: | Line 25: | ||
==== Rewrite Cookbook ==== | ==== Rewrite Cookbook ==== | ||
- | Options? | + | Options? Not many. Most promissing is to provide [[AlternativeBehavior|alternatives]] by creating new type to represent the same functionality: |
- | + | <source lang="java"> | |
- | + | public enum OldCategory { | |
- | + | public String getDisplayName(); | |
+ | } | ||
- | In my case, this was exactly what I had to do. Only with one problem. The [[enum]] was in signature of an interface, and I could not change that compatibly | + | public class NewCategory { |
+ | private NewCategory() {} | ||
+ | |||
+ | public String getDisplayName(); | ||
+ | |||
+ | public static NewCategory fromOld(OldCategory from); | ||
+ | public OldCategory toOld(); | ||
+ | |||
+ | public static NewCategory create(String name); | ||
+ | } | ||
+ | </source> | ||
+ | Some people may prefer to define ''NewCategory'' as [[Java]] interface. I don't like usage of [[Java]] interfaces for [[ClientAPI]] as there are problems associated with [[ExtendingInterfaces|extending such interfaces]], but in this case pure [[Java]] interface is the only option if one wants to retrofit the ''OldCategory'' to implement the new interface as well (and get rid of ''fromOld'' and ''toOld'' conversion methods). | ||
+ | |||
+ | <source lang="java"> | ||
+ | public enum OldCategory implements NewCategory { | ||
+ | public String getDisplayName(); | ||
+ | public static NewCategory create(String name); | ||
+ | } | ||
+ | |||
+ | public interface NewCategory { | ||
+ | public String getDisplayName(); | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | Too bad this cannot be done with classes! [[Java]] classes can have just one superclass. In the case of [[enum]]s the superclass is restricted to [http://download.oracle.com/javase/6/docs/api/java/lang/Enum.html Enum]. | ||
+ | |||
+ | In any case, the problem does not stop with the definition of ''NewCategory''. One also needs to duplicate all the methods that accept or return the ''OldCategory'' with their new versions that deals with ''NewCategory'': | ||
+ | <source lang="java"> | ||
+ | // old method | ||
+ | public OldCategory findMoreStable(OldCategory...); | ||
+ | // new method | ||
+ | public NewCategory findMoreStable(NewCategory...); | ||
+ | </source> | ||
+ | |||
+ | In my case, this was exactly what I had to do. Only with one problem. The [[enum]] was in signature of an interface, and I could not change that compatibly. I had to design new interface. Introducing the [[AlternativeBehavior|alternatives]] is transitive change - just to point out the complexity. | ||
== The [[Enum]] Problem == | == The [[Enum]] Problem == | ||
Line 36: | Line 71: | ||
Shall [[Enum]] be used when designing [[API]]s? Probably yes, but only with care. One has to be aware that it is almost impossible to switch from its finite set of values to infinite number of instances in future. | Shall [[Enum]] be used when designing [[API]]s? Probably yes, but only with care. One has to be aware that it is almost impossible to switch from its finite set of values to infinite number of instances in future. | ||
- | The root cause of the problems seem to be the existence of a marker [http://download.oracle.com/javase/6/docs/api/java/lang/Enum.html super class | + | The root cause of the problems seem to be the existence of a marker [http://download.oracle.com/javase/6/docs/api/java/lang/Enum.html Enum] super class that every [[enum]] automatically inherits from. Knowing all I know now, it would be better to use [[DelegationAndComposition]] rather than inheritance. If the compiler emitted code which inherits directly from '''Object''', it would be much easier to replace the keyword '''enum''' in class definition by keyword '''class''', while remaining compatible (one would need to implement few methods manually and make sure serialization works). |
+ | |||
+ | I know only one reason why the [http://download.oracle.com/javase/6/docs/api/java/lang/Enum.html Enum] superclass is handy. It is possible to properly type [http://download.oracle.com/javase/6/docs/api/java/util/EnumMap.html EnumMap]. But who needs proper types in this case anyway? It would be enough to have [[factory]] method to support the same use case: | ||
+ | |||
+ | <source lang="java"> | ||
+ | /** If the K class is enum, the map is effective, otherwise it is just regular HashMap | ||
+ | */ | ||
+ | public static <K,V> Map<K,V> Collections.efficientMap(Class<K> keys, Class<V> values) | ||
+ | </source> | ||
- | The [[Enum]] design as of [[Java]] 5 is associated with significant [[evolution]] flaws. | + | In summary: The [[Enum]] design as of [[Java]] 5 is associated with significant [[evolution]] flaws caused by artificial requirement to extend a marker [http://download.oracle.com/javase/6/docs/api/java/lang/Enum.html super class]. Thus, when designing [[API]]s, use [[Enum]]s with care! |
<comments/> | <comments/> |
Current revision
Java has been enhanced with enums in version 1.5. The Enums greatly simplify and standardize the way one can express the enumerated types in Java. However since their introduction I was not sure how suitable they are for API Design (I was not the only one, see few year's old Andrei's post). Today I finished one Enum related API change for NetBeans, and I felt all the pain. I guess I understand now what is wrong with Enums in APIs.
Contents |
Growing Set
Good API design is about having evolution plan prepared in advance. One way of evolving an Enum is to enlarge the set of its constants. As Andrei points out adding new enum constant is not strictly semantically BackwardCompatible, but this evolution path has been well thought in advance when Enums were designed.
Each switch statement (over an Enum) needs to have default branch. In case new constants are added, the code ends up in there. There are going to be some changes in newer versions - the default branch may be used more and more often, but overall, there are no surprises. Both sides of the API contract - the publisher as well as consumer - are (sort of) ready for this kind of evolution.
Infinite Set
The real problem starts when you find out that finite amount is not enough. When you need an unlimited number of instances, then the Java Enum design starts to stay in the way.
This is not as rare situation as you may think. Often one starts with Singleton instance and evolves it to multiple instances in subsequent releases of the API. Why not start with few Enum instances at first and change them to regular classes in future? Well, because this is (almost) impossible with Enums!
Standard, Beta, Community & co.
There are online repositories of additional modules in any NetBeans based application. As they are of various quality, they can be classified according to their stability as Standard, Beta and Community. At least this was the initial design. Over the time it turned out that such classification is too restrictive. I got a request to allow users of that API to define their own new stability categories associated icons. Easy task, one may think. Basically just add new Factory method, right?
Well, it would be an easy task. If the stability category class was not originally declared as enum!
But nobody is allowed to dynamically create instances of Enums. It is forbidden by Java compiler. Even resolving to reflection does not help, as the Constructor.newInstance has a special check for Enum subclasses and throws an exception. No luck.
What can one do? Stop extending the Enum class? But that is horribly backward incompatible! Before that people could assign instances of the class to variables of Enum type. Or use EnumSet with instances of that class. After stopping to extend the Enum marker class, things of this kind are no longer possible. Code that used to compile, can be no longer compilable.
Rewrite Cookbook
Options? Not many. Most promissing is to provide alternatives by creating new type to represent the same functionality:
public enum OldCategory { public String getDisplayName(); } public class NewCategory { private NewCategory() {} public String getDisplayName(); public static NewCategory fromOld(OldCategory from); public OldCategory toOld(); public static NewCategory create(String name); }
Some people may prefer to define NewCategory as Java interface. I don't like usage of Java interfaces for ClientAPI as there are problems associated with extending such interfaces, but in this case pure Java interface is the only option if one wants to retrofit the OldCategory to implement the new interface as well (and get rid of fromOld and toOld conversion methods).
public enum OldCategory implements NewCategory { public String getDisplayName(); public static NewCategory create(String name); } public interface NewCategory { public String getDisplayName(); }
Too bad this cannot be done with classes! Java classes can have just one superclass. In the case of enums the superclass is restricted to Enum.
In any case, the problem does not stop with the definition of NewCategory. One also needs to duplicate all the methods that accept or return the OldCategory with their new versions that deals with NewCategory:
// old method public OldCategory findMoreStable(OldCategory...); // new method public NewCategory findMoreStable(NewCategory...);
In my case, this was exactly what I had to do. Only with one problem. The enum was in signature of an interface, and I could not change that compatibly. I had to design new interface. Introducing the alternatives is transitive change - just to point out the complexity.
The Enum Problem
Shall Enum be used when designing APIs? Probably yes, but only with care. One has to be aware that it is almost impossible to switch from its finite set of values to infinite number of instances in future.
The root cause of the problems seem to be the existence of a marker Enum super class that every enum automatically inherits from. Knowing all I know now, it would be better to use DelegationAndComposition rather than inheritance. If the compiler emitted code which inherits directly from Object, it would be much easier to replace the keyword enum in class definition by keyword class, while remaining compatible (one would need to implement few methods manually and make sure serialization works).
I know only one reason why the Enum superclass is handy. It is possible to properly type EnumMap. But who needs proper types in this case anyway? It would be enough to have factory method to support the same use case:
/** If the K class is enum, the map is effective, otherwise it is just regular HashMap */ public static <K,V> Map<K,V> Collections.efficientMap(Class<K> keys, Class<V> values)
In summary: The Enum design as of Java 5 is associated with significant evolution flaws caused by artificial requirement to extend a marker super class. Thus, when designing APIs, use Enums with care!
<comments/>