5

While working on a project, I was presented with a task to design a set of classes that implement an interface defining a simple action. Usually these classes would do their job in particular sequence, all at once, but the possibility to call a method from only one of them was also a requirement.

Taking into account all the above and also considering that: - each class would have quite basic logic - extending another class was not required - it might be convenient to have all classes in a single file - editing source file when needed is not an issue

I came up with the following solution (actual class was not so contrived, but the example below is sufficient to give you some basic idea):

public enum Bestiary {
DOG(1) {
    @Override
    void makeNoise(Loudspeaker ls) {
        ls.shoutOutLoud("I am alpha dog");
    }
},

CAT(2) {
    @Override
    void makeNoise(Loudspeaker ls) {
        ls.shoutOutLoud("I am beta cat");
    }
},

RAT(3) {
    List<String> foods = new ArrayList<>();
    {
        foods.add("gods");
        foods.add("dogs");
        foods.add("cats");
        foods.add("other rats");
    }

    @Override
    void makeNoise(Loudspeaker ls) {
        StringBuilder cry = new StringBuilder("I am THE rat; usually I eat ");
        for (int i = 0; i < foods.size(); i++) {
            cry.append(foods.get(i));
            if (i != (foods.size() - 1)) {
                cry.append(", ");
            }
        }
        ls.shoutOutLoud(cry.toString());
    }
},

THE_THING(4) {
    String name = "r2d2";

    @Override
    void makeNoise(Loudspeaker ls) {
        ls.shoutOutLoud(calculateHash(name));

    }

    private String calculateHash(String smth) {
        return String.valueOf(smth.hashCode());
    }
};

private int id;

public int getId() {
    return id;
}

Bestiary(int id) {
    this.id = id;
}

abstract void makeNoise(Loudspeaker ls); // all enum elements will need to implement this - kind of like implementing an interface (which was also an option); note that we pass some arbitrary object and call methods on it
}

And code which calls this class might look like:

public final class Loudspeaker {
private static Loudspeaker loudspeaker = new Loudspeaker();

public static void shoutOutLoud(String cry) {
    System.out.println(cry);
}

static class Noizemakers {
    public static void makeSomeNoise() {
        for (Bestiary creature: Bestiary.values()) {
            System.out.println(creature + " with id " + creature.getId() +  " says: ");
            creature.makeNoise(loudspeaker);
        }
    }
}

public static void main(String[] args) {
    Noizemakers.makeSomeNoise();
    Bestiary.CAT.makeNoise(loudspeaker);
}
}

During a code review my suggestion was mocked as the one that is "too hacky, exploites the fact that enums have class body and methods, and have a bad code smell in overall". While transforming it into a separate interface, a bunch of usual Java classes etc. is a matter of few minutes, I was not really quite satisfied with this explanation. Are there any guidelines saying that you should use enums exclusively in their basic form, similarly to other languages? What real drawbacks does this approach have? How about Joshua Bloch's suggestion to write singletons as enums - in this case such an enum will have to be a full-fledged class, right?

rae1
  • 6,066
  • 4
  • 27
  • 48
Alexey Danilov
  • 301
  • 4
  • 13
  • 3
    "exploites the fact that enums have class body and methods"? That sounds like you're getting criticized for using new(ish) language features. Do they also dislike using genericized collections? – sharakan Jun 06 '13 at 19:24
  • well, they just said "hackish", and I employed the rest :) IMO there's no hack in using standard language features – Alexey Danilov Jun 06 '13 at 19:39
  • @AlexeyDanilov I'd argue that Java generics are implemented in a hackish way due to the desire for complete backwards compatibility. The usage of type erasure makes it annoying when you try to do anything significant with covariants while still desiring static type safety, such as overriding a method having a raw return type with a method having a generic one (though the opposite is valid, it seems). And there's also the issue with method calls on objects in genericized classes/methods. (Not that I don't use generics whenever possible;they're still a heck of a lot better than the alternative.) – JAB Jun 06 '13 at 19:47
  • I agree about generics. They are not as convenient to deal with as similar constructs in other languages, which were designed with type safety in the first place. But, yeah, they are still better than nothing and we use them a lot. – Alexey Danilov Jun 06 '13 at 20:07
  • 2
    I don't think there's anything 'hackish' about using enums with methods. That said, I think your enum would be clearer and cleaner if it simply kept the noise as a field in the base class, which each value passes in as a constructor argument. Right now each value has a lot of code, and that makes it look like they're all doing very different things. But it's all the same, just pass a constant value to Loudspeaker. That simplicity is getting lost in the noise, IMO. – sharakan Jun 06 '13 at 21:16
  • Sure, such optimizations would be appropriate. Then again, the code sample I provided was just to show an approach to solving a problem, the way of thinking, if I may. – Alexey Danilov Jun 07 '13 at 07:49

3 Answers3

3

You should only use an enum where there is no (or little) possibility of the addition of a new element. This is not to say that you shouldn't give an enum class-like functions. For example:

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
}

There are multiple reasons for this:

  • Semantics/intended purpose. It just doesn't make sense to have enums for non-enumerations, by the very definition of the word.
  • Compatibility. What if I want to add a bird to your bestiary? You'd have to amend the enum. Easy enough, but what if you have some users using an older version of the enum and others using a later version? This makes for lots of compatibility issues.

One (suboptimal) solution if you must use an enum would be:

interface Animal {
    void makeNoise();
}

enum Bestiary implements Animal {
    // the rest of the stuff here
}

Then, any method currently accepting a Bestiary could be easily switched to accept an Animal. However, if you do this, it's better anyway to just have:

interface Animal {
    void makeNoise();
}
public class Dog implements Animal {...}
public class Cat implements Animal {...}
public class Rat implements Animal {...}
public class Thing implements Animal {...}
wchargin
  • 15,589
  • 12
  • 71
  • 110
  • "but what if you have some users using an older version of the enum and others using a later version?" The users using the older version would get the newer one the same way they'd get any new classes. And I'm pretty sure `Bestiary` is already implementing an interface given his usage of `@Override`. – JAB Jun 06 '13 at 19:20
  • A valid point, but as I mentioned, editing source file when needed is not an issue in my case – Alexey Danilov Jun 06 '13 at 19:21
  • 1
    @JAB it's not implementing an interface; it's essentially implementing itself by overriding its own abstract method in an anonymous class. wrt the version problem, I mean what if there's a `switch` statement that can't consider all `Bestiary`s because they don't "yet" exist? – wchargin Jun 06 '13 at 19:22
  • @WChargin Oh, you're right, I missed the last two lines of the `enum`. Though how is the issue of a switch statement on an enum type any different from the issue of having to add additional checks for new `Animal`s? – JAB Jun 06 '13 at 19:25
  • @WChargin that's the nice point of enums having methods! instead of having switch statements, you refactor that in to an abstract method and have the elements provide the correct implementations. – sharakan Jun 06 '13 at 19:26
  • @sharakan that depends on how Java compiles switch statements. If it creates the branches based on the state of the enumerated type at compilation time, you would have to recompile anything referencing the type in addition to the enum, assuming that the adding things to an enum can change how earlier values of the enum are referenced. ...I think my train of thought switched tracks at some point in thinking about this, so I'm not sure if I'm making sense here. Though actually, your point about the abstract method makes it perfectly fine, and works just as well for classes.The issue is default.. – JAB Jun 06 '13 at 19:33
  • @JAB recall that `switch` statement cases must be constant value expresses. I'm pretty sure it just uses an `==` (memory address) comparison in a `goto` tree. – wchargin Jun 06 '13 at 19:34
  • @JAB Not sure that I follow your concern w.r.t compiling. But for the default case, just have a default implementation in the enum, with instances overriding as necessary. – sharakan Jun 06 '13 at 20:02
  • @sharakan ...Oh, right, the original method in the `enum` doesn't have to be abstract, does it. My mind has been more scattered than I thought this afternoon. – JAB Jun 06 '13 at 20:06
3

One could use an enum anywhere you have a shallow class hierarchy, with trade offs between extensibility (classes have more, enums have less) and conciseness (if the functionality is straightforward, enums are probably clearer). It's not the right thing to do all the time, but it's certainly ok to do some of the time, just be aware of the differences, some of which I list below.

To my mind, the situation you're dealing with seems to me to be exactly the kind of thing the language designers were working to support by allowing enums to have methods. It doesn't seem to me that you're subverting the intention of that language feature in the least.

As an example from my work, I often use enums with methods as a way of implementing a variety of stateless strategies, but have also used them for other things, including being a kind of extensible form of Class.

Answers to your specific questions:

What real drawbacks does this approach have?

Compared to the interface + concrete classes approach:

  • Methods defined in a specific enum value can't be called from outside that value. eg, if you defined a method for RAT called squeak(), no one can call it.
  • No mutable state, because each enum value is effectively a singleton.
  • Your enum class file can get excessively long if the number of types increases dramatically, or the code for each type increases.
  • Can't subclass enum values, they are effectively final
  • No doubt some others...

Are there any guidelines saying that you should use enums exclusively in their basic form, similarly to other languages?

None that I've ever seen.

How about Joshua Bloch's suggestion to write singletons as enums - in this case such an enum will have to be a full-fledged class, right?

Following the logic of your questioners, yes. So it becomes a question of whether you'd rather listen to them, or to Josh Bloch.

sharakan
  • 6,821
  • 1
  • 34
  • 61
2

My personal opinion is that enums should not contain any mutating methods, as it violates most assumptions of enumerated values having constant state. ...But looking over your work again, that does not actually appear to be the case. It certainly seems odd to do it this way, but it's more of an "unexpected usage" thing than specifically being a "wrong way to do it" thing.

Just make sure that any potentially-modifiable values within the enumerated types aren't accessible externally, such as foods. (The Strings can be made final, so that's not an issue, but making foods final wouldn't prevent people from manipulating the list itself, just assigning a new one.)

JAB
  • 20,783
  • 6
  • 71
  • 80
  • Yes, enum values are final. However, enum fields are not necessarily final. – wchargin Jun 06 '13 at 19:19
  • @AlexeyDanilov And where is the method mutating state of the enumerated type in that? – JAB Jun 06 '13 at 19:21
  • @WChargin Woops, that's what I meant. – JAB Jun 06 '13 at 19:21
  • While writing this example I was not really concerned with encapsulation, my main goal was to show design suggesion... surely, this class would need to be tweaked. But I would love to see any guidelines why doing this is considered to be a bad practice. P.S. Which method mutating state you are referring to in my example? – Alexey Danilov Jun 06 '13 at 19:26
  • @AlexeyDanilov None, technically. I edited my answer when I realized that fact. Admittedly, I'm also a little iffy on using the methods of an enumerated type to cause side effects (and `foods` could potentially be modified at a later date for the same reason `volatile` doesn't work very well for objects with mutable members) but that's more of a general design idea and applies equally well to regular class methods even though people don't usually have as much of an issue with those (although many environments do discourage them, if I'm not mistaken, which is understandable due to pot. danger). – JAB Jun 06 '13 at 20:01