For the longest while, I was glad that Java didn't have support for enumerated types. I've used them in C and C++ but I often ended up in that weird place that calls for the Replace Type Code with State/Strategy refactoring: I start writing a switch statement that uses the enum and I know that the code in the cases could be in a class, if only I had one instead of the enum.
A few days ago, I was working on some Java code that was going to use in a session at the Better Software conference in San Francisco. It involved navigating a player through a game. The simplest thing was to create an enum called Direction:
public enum Direction
{
NORTH,
SOUTH,
EAST,
WEST,
}
So far, so good.
I continued working and I noticed that one of the things that I really needed to do was find the opposite of a direction. If Direction was a class it would be easy to do it in a nice OO way, I could just add a getOpposite() method.
Man! My first attempt at using enums in Java 5 and I hit a wall. "But wait!", I said to myself. I remembered that enums can have instance fields and methods in Java:
public enum Direction
{
NORTH,
SOUTH,
EAST,
WEST;
public Direction getOpposite() {
...
}
}
The trick is figuring out what code to put in getOpposite().
Ideally, it would've been great to set the opposite of each Direction on construction and return that value from getOpposite(). Initially, it looked like that was possible, too. I found that you can pass arguments to enum values on construction. The syntax is a little odd:
public enum Direction
{
NORTH(SOUTH),
SOUTH(NORTH),
EAST(WEST),
WEST(EAST);
Direction opposite;
Direction(Direction opposite) {
this.opposite = opposite;
}
public Direction getOpposite() {
return opposite;
}
}
Unfortunately, this doesn't compile. The compiler tells us that the field SOUTH can't be referenced before it is declared. It looked like the only workaround was to do the following, and you have to admit it is pretty nasty:
public Direction getOpposite() throws DirectionException {
Direction opposite;
switch(this) {
case NORTH: opposite = SOUTH; break;
case SOUTH: opposite = NORTH; break;
case EAST: opposite = WEST; break;
case WEST: opposite = EAST; break;
default:
throw new DirectionException("Direction with no opposite:" + this);
}
return opposite;
}
The exception is particularly galling. Some statically typed languages make it a compile-time error when a switch on an enumeration doesn't cover all the cases. With Java, we're left with a possible runtime error. That doesn't seem to fit the spirit of the language.
After a bit more reading, I discovered a solution. We can have instance-specific methods on enumerations like this:
public enum Direction {
NORTH { public Direction getOpposite() { return SOUTH; }},
EAST { public Direction getOpposite() { return WEST; }},
SOUTH { public Direction getOpposite() { return NORTH; }},
WEST { public Direction getOpposite() { return EAST; }};
public abstract Direction getOpposite();
}
It seems like the Java 5 designers handled the evolutionary change case well. You can start with an enum and move to classes pretty easily, but it makes me wonder whether it would've just been easier to start with enums as classes:
public abstract class Direction {}
public class North extends Direction {}
public class South extends Direction {}
public class East extends Direction {}
public class West extends Direction {}
and let them evolve into this:
public abstract class Direction {
public abstract Direction getOpposite();
}
public class North extends Direction {
public Direction getOpposite() { return new South(); }
}
...
Next time I feel the need for an enum, I might do that instead.
> Some statically typed languages make it a compile-time error when a > switch on an enumeration doesn't cover all the cases.
Indeed. In SML it is actually a warning required by the Definition. Here is how the same example would look in SML:
datatype direction = NORTH | SOUTH | WEST | EAST
val opposite = fn NORTH => SOUTH | SOUTH => NORTH | WEST => EAST | EAST => WEST
The textbook solutions to simulating variant types in OO languages can lead to extremely complex code. I recall browsing through a book on writing compilers in Java. The book used the Composite pattern to represent an AST and the Visitor pattern to manipulate ASTs. The implementation required literally about 5 to 10 times more code (one Java file [10 or more lines] + one method [3 or more lines] in a visitor for each AST constructor) than the equivalent would have required in SML using variant types and pattern matching (one variant constructor [1 line of code] + one match clause [1 or more lines] for each AST constructor).
> <pre> > public abstract class Direction {} > public class North extends Direction {} > public class South extends Direction {} > public class East extends Direction {} > public class West extends Direction {} > </pre> > > <p>and let them evolve into this:</p> > > <pre> > public abstract class Direction { > public abstract Direction getOpposite(); > } > > public class North { > public Direction getOpposite() { return new South(); > h(); } > } > ... > </pre> > > <p>Next time I feel the need for an enumeration, I might > do that instead.</p>
You can write code using classes almost identical to that of enums. Your second example is of another spirit: it uses named subclasses and provides no instances (as well as obvious singleton). Enums in Java 5 are laguage wrapping of well known enums pattern (providing some code skipping and naming):
public abstract class Direction{ private Direction(){} public abstract Direction getOpposite(); public final static Direction EAST=new Direction(){ public Direction getOpposite(){return WEST;}}; // and so on }
The key to solving this is to remember that Direction has a magnitude. That is, on a conventional x-y axis, EAST is 0 degrees (positive x-axis), North is Positive y-axis (90 degrees), etc.
Have an Enum with a Constructor that takes the orientation as a parameter. To get the opposite direction simply rotate the direction by 180 degrees, do a mopdulo 360 (in case the direction + 180 becomes greater than 360), and return the ENUM value corresponding to it. That is, to find the opposite of North, gets its direction (90 degrees), add 180 to get 270 degrees. Now you know that South has 270 degrees as its orientation. Hence, the opposite of North is South!
To find the opposite of South, we get required direction is (270+180) modulo 360 = 90 => North.
This also has the added flexibility that there is only one piece of code that is used to find the opposite. You can easily add other directions such as NE, SW, etc., without having to write code for finding the opposite.
We should start thinking in terms of how humans think. When asked to find the opposite of a direction, a human simply rotates the angle through 180 degrees, and tells the opposite. With the suggested approach, the algorithm also does the same!
You can write it as an ENUM, a class, or whatever.
If anybody is interested, I'll post the code for this later.
You can also do this directly with enums since they are classes almost identical to normal classes, there are some things that are different:
1. They are abstract with restrictions on what can call their constructors (which means you can't instanciated them directly - only via an enum declaration)
2. They extend Enum (which means they can't extend anything else)
3. The first thing in the class are the static fields that are the enum values (which means if you want other static fields first you have to put them in an interface and implement that interface)
4. The definition of the enum values is syntactic sugar (e.g. for public static final Direction NORTH = new Direction() {};)
Therefore you can write your direction example as:
public enum Direction {
NORTH() { Direction opposite() { return SOUTH; } },
SOUTH() { Direction opposite() { return NORTH; } },
EAST() { Direction opposite() { return WEST; } },
WEST() { Direction opposite() { return EAST; } };
abstract Direction opposite();
publicstaticvoid main( final String[] notUsed ) {
out.println( NORTH.opposite() );
}
}
This is actually an interesting exercise in the sense that it is pretty difficult (or even impossible) to express properly in many mainstream languages. Here's how to do it properly in SML.
Let's first specify what we actually want as a signature:
signature DIRECTION = sig eqtype direction
val north : direction val south : direction val west : direction val east : direction
val opposite : direction -> direction end
What the above signature says is that there is a type direction whose values can be tested for equality. It also says that there are four (not necessarily distinct) named values of the type: north, south, west, and east. Finally, there is a function opposite that maps a direction to a direction.
The above signature can be matched by many modules. Here is an implementation using modular arithmetic:
structure IntDirection :> DIRECTION = struct type direction = int
val north = 90 val south = 270 val west = 180 val east = 0
fun opposite d = d + 180 mod 360 end
Here is an implementation using a datatype:
structure DatatypeDirection :> DIRECTION = struct datatype direction = north | south | west | east
val opposite = fn north => south | south => north | west => east | east => west end
Here is an implementation using strings:
structure StringDirection :> DIRECTION = struct type direction = string
val north = "north" val south = "south" val west = "west" val east = "east"
fun opposite d = if d = north then south else if d = south then north else if d = west then east else west end
And so on...
All of the above structures (modules) were opaquely sealed by the same signature and they implement the same abstraction. The abstraction is enforced by the type system of SML. For instance, you can not pass an arbitrary integer to IntDirection.opposite nor can you convert an IntDirection.direction to an integer. Here is what happens (SML/NJ) if you try:
- IntDirection.opposite 50 ; stdIn:13.1-13.25 Error: operator and operand don't agree [literal] operator domain: IntDirection.direction operand: int in expression: IntDirection.opposite 50
uncaught exception Error raised at: ../compiler/TopLevel/interact/evalloop.sml:52.48-52.56 ../compiler/TopLevel/interact/evalloop.sml:35.55 - IntDirection.north + 10 ; stdIn:1.1-1.24 Error: operator and operand don't agree [literal] operator domain: IntDirection.direction * IntDirection.direction operand: IntDirection.direction * int in expression: IntDirection.north + 10 stdIn:1.20 Error: overloaded variable not defined at type symbol: + type: IntDirection.direction
> This is actually an interesting exercise in the sense that > it is pretty difficult (or even impossible) to express > properly in many mainstream languages. Here's how to do it > properly in SML. > ...
That is the thing that really throws me about languages using Hindley-Milner. You can get abstraction for something like Direction, but it is not substitutable across the parameterization. It seems easier to make building blocks, but harder to compose them. String and Int bleed through, don't they?
> You can get abstraction for something like Direction, but > it is not substitutable across the parameterization.
Sorry, I don't quite understand what you mean here. What parameterization? (There is no parameterization in the SML examples I wrote.)
> It seems easier to make building blocks, but harder to > compose them.
Again, I don't quite understand what you mean here.
Functional languages excel at composing stuff out of simple building blocks. There is a vast literature on so called combinator libraries.
> String and Int bleed through, don't they?
No, assuming that I understand your question correctly. An opaque signature ascription completely hides the representation of abstract type specifications. You can not treat a StringDirection.direction as a string nor an IntDirection.direction as an int outside the respective modules.
The idea of using enum for the Directions limits us to the specific Directions chosen at design time. If only these four directions are needed, then an enum works fine. If new Directions need to be added, we have to rewrite the enum class.
If, instead of an enum, a class is used with North, South, etc., as sub-classes, then the design becomes divorced from reality.
North, South, etc., can only be thought of as instances of specific directions, never as classes in themselves. Thinking of them as classes leads one to the strange situation where an application can have 10 "South" objects! How can there be 10 South directions in real life?
In this case, I think sub-classing Direction has its limitations. Strictly speaking, North, South, etc., are instances of Direction, not sub-classes.
If we want a more flexible architecture with the ability to define new Directions (restricting ourselves to one Direction for each physical Direction), and get their opposites; and at the same time not have to recompile the Direction class whenever a new Direction is added, then we can not use enums. We must create a Direction class without sub-classes.
If it's singletoned, there is no principal difference. Instead, if you use single class Direction with parameter, you can create 10 SOUTH directions (10 objects with the same parameter), while using enum pattern you can only get single instance.
> If it's singletoned, there is no principal difference. > Instead, if you use single class Direction with parameter, > you can create 10 SOUTH directions (10 objects with the > same parameter), while using enum pattern you can only get > single instance.
True. But that is easily handled within the class using any one of several techniques.
For example: Make the constructor private, load the various Direction instances from data source/file, then provide access to the Directions by
Direction north = Direction.get("North");
for example ensures that client code never creates the directions directly.
It is the class's responsibilty to ensure that there exists only one instance of "North" direction.
Does this look somewhat like the Flyweight pattern?
> The idea of using enum for the Directions limits us to the > specific Directions chosen at design time. If only these > four directions are needed, then an enum works fine. If > new Directions need to be added, we have to rewrite the > enum class. > > If, instead of an enum, a class is used with North, South, > etc., as sub-classes, then the design becomes divorced > from reality.
Which is okay by me. To me, our job isn't modeling reality, it's making software that it is correct and changes well. And, it helps when names map well to some real concept, but often they bend and that's fine as long as it doesn't get too confusing. A good example is a method called pay() on a class named Employee. From a reality modeling point of view, that's ridiculous, you shouldn't be able to ask any employee to pay him or her self, but from an OO point of view it may make perfect sense.
> North, South, etc., can only be thought of as instances of > specific directions, never as classes in themselves. > Thinking of them as classes leads one to the strange > situation where an application can have 10 "South" > objects! How can there be 10 South directions in real > life?
Yes, but, again, it isn't about modeling reality. In my opinion, the industry was poorly served by some early voices who touted the idea that OO involved modeling reality and that it was akin to data modeling and AI. It's taking us a long time to get over that.
> In this case, I think sub-classing Direction has its > limitations. Strictly speaking, North, South, etc., are > instances of Direction, not sub-classes.
Like everything else, it depends on the behavior. If North behaves differently than South, it can be a different class. And, with Java enums, essentially they are different classes, it's just a matter of whether we take advantage of them by creating specific methods for each enum value.
In the code examples, we had an abstract method for getOpposite in the enum which essentially makes each enum value a subclass. From what I've heard, SmallTalkers often use subclassing to do enum-ish things. One thing I know for sure is that False and True are subclasses of Boolean in many (if not all) SmallTalks.
> If we want a more flexible architecture with the ability > ty to define new Directions (restricting ourselves to one > Direction for each physical Direction), and get their > opposites; and at the same time not have to recompile the > Direction class whenever a new Direction is added, then we > can not use enums. We must create a Direction class > without sub-classes.
Agreed. The thing is, these decisions are really about what we need the behavior to be in the application, more than an a priori conception of what direction should be. If the things that Direction does deviate too far, maybe another name will be better. Or, maybe not.
I had written: If, instead of an enum, a class is used with North, South, etc., as sub-classes, then the design becomes divorced from reality.
To which Michael F. replied, Which is okay by me. To me, our job isn't modeling reality, it's making software that it is correct and changes well.
I fail to se how a class Direction with sub-classes East, West, North and South can change well when we have to add new sub-classes like North-West, South-East, etc., that are effectively identical to each other.
As mentioned earlier, if we know that the application is very, very unlikely to add other Directions, then an enum is just fine. I would still have the enum class Direction with two attributes: a name and a number representing the orientation (0 degrees, 90 degrees, etc., or alternative descriptions of the orientations.) Then, the getOpposite is a simple single method. There is no need to define the method in each of the enum sub-classes.
My main concern about this thread is that, at a superficial level, it seems to be about the limitations of enums in Java; while there are deeper problems with the solution suggested originally. Examining the problem a little more shows that the Directions have not been modelled as one thinks about them in real life. A Direction is not just a name, there is a direction (orientation) involved! The fact that a getOpposite method is required suggests that orientation is an important attribute of the problem domain.
Omitting this attribute creates problems. In particular, this leads to the necessity of over-riding the getOpposite method in each sub-class because the Direction had no concept of orientation.
With Direction modelled to have both orientation and name, it becomes obvious that there exists a simple algorithm for finding the opposite of a given Direction. Lo and behold, the original problem of having to write over-ridden methods in each of the sub-classes never arises! We do not have to access somewhat non-obvious features of enums as implemented in Java. Thus, this whole article would not have been necessary.
So, I do think that there is value in modelling things as close as possible to reality, as long as we do not go overboard. Here, it is easy to keep the model simple and close to reality at the same time. If this leads to less code and somewhat more flexible architecture, then it is even better.
Michael F. wrote: A good example is a method called pay() on a class named Employee. From a reality modeling point of view, that's ridiculous, you shouldn't be able to ask any employee to pay him or her self, but from an OO point of view it may make perfect sense.
Nitpicking here, but "pay" is usually a shorthand for "calculatePay" or "getPay". Pay is used as a noun, not as a verb. It seems to be reasonable to ask the Employee class to return a particular Employee's pay (salary).
Flat View: This topic has 25 replies
on 2 pages
[
12
|
»
]