Thursday, May 23, 2013

Enums in Java

We all know the benefits of enums from programming languages like C/C++: the most known benefit  is that you can assign values to a variable that must have a limited set of values by using an enum. Static type safety is ensured.

In most programming languages enums' types are primitives. In Java an enum is simply a class with the following characteristics:
  • it has no accessible constructors (constructors are private)
  • the enums' values is a list of final references with the same type as the enum's type
  • it can contain behavior

Design benefits

1. Avoid having a long list of constants defined in your code. In a class with lots of constants is hard to figure out what a constant may mean. Instead it's clearer to have the constants grouped together in several "types" of constants.


  • enums can't be extended
  • the only enum instances are those defined inside the enum definition
2. Enums extend java.lang.Object thus we can override any method we need (e.g. toString()). If enums were just primitive types we would have to implement this String representation outside of the enum code.

3. One can associate data with enum constants

4. Behavior can also be encapsulated inside an enum. This eliminates the need to implement logic somewhere else (worst is to have lots of constants with tons of conditional code spread inside various methods - with each conditional branch containing the logic for one or more constants)

5. Enums can't be extended, but they can implement interfaces


Small example

Suppose you'd like to implement a simple FSM.  One possible way to do it is by using enums. The enums could hold the possible states of the FSM + the transition, which is a 3-tuple of (CurrentState, Event, NextState). 

For fun, let's imagine a simple project scenario:


import static java.lang.System.out;

// this can be mapped to the project status (in our simple case)
enum CustomerSatisfaction {
    PLEASED(10), SATISFIED(1000), UNPLEASED(50), FURIOUS(-1000);
    private int money;
    private CustomerSatisfaction(int money) { this. money = money; }
    public int money() { return money; }
}

// for us, events are simply our ability to deliver
enum ProjectEvent {
    ISSUE_SOLVED,
    ELEGANT_SOLUTION,
    SOLUTION_INTRODUCED_BUGS,
    NASTY_WORKAROUND;
}

// the transition table
enum ProjectTransitions {
     T1(CustomerSatisfaction.PLEASED, ProjectEvent.ISSUE_SOLVED, 
CustomerSatisfaction.SATISFIED),
     T2(CustomerSatisfaction.SATISFIED, ProjectEvent.NASTY_WORKAROUND, 
CustomerSatisfaction.UNPLEASED),
     T3(CustomerSatisfaction.UNPLEASED, ProjectEvent.NASTY_WORKAROUND, 
CustomerSatisfaction.FURIOUS), 
     T4(CustomerSatisfaction.UNPLEASED, ProjectEvent.ELEGANT_SOLUTION, 
CustomerSatisfaction.PLEASED);

private CustomerSatisfaction current, next;
private ProjectEvent event;

private ProjectTransitions (CustomerSatisfaction current, ProjectEvent event,                                                   CustomerSatisfaction next) {
this.current= current;
this.event = event;
this.next = next;
}

// in real-world scenario, a static enum map can be used here
public static CustomerSatisfaction next(CustomerSatisfaction current, ProjectEvent event) {
for (ProjectTransitions t : ProjectTransitions.values()) {
if (t.current == current && t.event == event)
return t.next;
}
// in case there is no transition
return current;
    }
}

// dummy test client - in practice some observer + event generator would be in place
public class Project {
     private volatile CustomerSatisfaction status;
     private int earnings;

     private final Object lock = new Object();
     private final Object startLock = new Object();

     private boolean eventReceived;
     private boolean started;

     public Project(CustomerSatisfaction initialStatus) {
            status = initialStatus;
     }

     public void start() {
            synchronized(startLock) {
started = true;
startLock.notifyAll();
           }
            while (status != CustomerSatisfaction.FURIOUS) {
                   synchronized(lock) {
                         while (!eventReceived) {
                                 try {
                                       lock.wait();
                                 } catch (InterruptedException e) {
                                       out.println("What's happening?");
                                 }
                         }
                        earnings += status.money();
                    out.println("Earned " + status.money() + ". Customer is " + status.name());
                    eventReceived = false;
                         lock.notifyAll();
                   }
            }
            out.print("Project done! Earnings = " + earnings + " dollars. ");
            out.println(earnings > 1000 ? "Good job!" : "Not very good");
     }

     public void onEvent(ProjectEvent event) {
            synchronized (startLock) {
while (!started) {
try {
startLock.wait();
} catch (InterruptedException e) {
out.println("What's happening? :)");
}
}
}
             synchronized(lock) {
while (eventReceived) {
try {
lock.wait();
} catch (InterruptedException e){}
}
                  out.println("New event: " + event.name());
                  status = ProjectTransitions.next(status, event);
                  eventReceived = true;
                  lock.notify();
}
     }

     public static void main (String... args) {
          final Project productA = new Project(CustomerSatisfaction.UNPLEASED);
          Thread t = new Thread(new Runnable() {
                 @Override public void run() {
productA.start();
                  }
          }, "starter");
          t.start();
          
          // a simple event injector
          productA.onEvent(ProjectEvent.ISSUE_SOLVED);
          productA.onEvent(ProjectEvent.ELEGANT_SOLUTION);
          productA.onEvent(ProjectEvent.ISSUE_SOLVED);
          productA.onEvent(ProjectEvent.ELEGANT_SOLUTION);
          productA.onEvent(ProjectEvent.NASTY_WORKAROUND);
          productA.onEvent(ProjectEvent.NASTY_WORKAROUND);
          productA.onEvent(ProjectEvent.NASTY_WORKAROUND);
          
          try { t.join(); } catch (InterruptedException e) {}
     }
}



Notice how easy it is to modify the state machine without modifying client code (no external logic in any place outside the FSM itself): states, events and transitions can be easily added / modified without affecting the other part of the system. This also makes it harder to introduce errors in the system.

No comments: