17

Consider the example below. Any change to the ColorChoice enum affects all IWindowColor subclasses.

Do enums tend to cause brittle interfaces? Is there something better than an enum to allow for more polymorphic flexibility?

enum class ColorChoice
{
    Blue = 0,
    Red = 1
};

class IWindowColor
{
public:
    ColorChoice getColor() const=0;
    void setColor( const ColorChoice value )=0;
};

Edit: sorry for using color as my example, that's not what the question is about. Here is a different example that avoids the red herring and provides more info about what I mean by flexibility.

enum class CharacterType
{
    orc = 0,
    elf = 1
};

class ISomethingThatNeedsToKnowWhatTypeOfCharacter
{
public:
    CharacterType getCharacterType() const;
    void setCharacterType( const CharacterType value );
};

Further, imagine that handles to the appropriate ISomethingThatNeedsToKnowWhatTypeOfCharacter subclass are handed out by a factory design pattern. Now I have an API that cannot be extended in the future for a different application where the allowable character types are { human, dwarf }.

Edit: Just to be more concrete about what I'm working on. I am designing a strong binding of this (MusicXML) specification and I am using enum classes to represent those types in the specification which are declared with xs:enumeration. I am trying to think about what happens when the next version (4.0) comes out. Could my class library work in a 3.0 mode and in a 4.0 mode? If the next version is 100% backward compatible, then maybe. But if enumeration values were removed from the specification then I'm dead in the water.

  • 2
    When you say "polymorphic flexibility", what capabilities do you have in mind exactly? – Ixrec Mar 16 '15 at 21:05
  • 2
  • 3
    Using an enum for colors creates brittle interfaces, not just "using an enum". – Doc Brown Mar 16 '15 at 21:23
  • 3
    Adding a new enum variant breaks code using that enum. Adding a new operation to an enum is quite self-contained on the other hand, as all cases that need to be handled are right there (contrast this with interfaces and superclasses, where adding a non-default method is a serious breaking change). It depends on the kind of changes that are really necessary. – user7043 Mar 16 '15 at 22:25
  • 1
    Re MusicXML: If there's no easy way to tell from the XML files which version of the schema each one is using, that strikes me as a critical design flaw in the specification. If you have to workaround it somehow, there's probably no way for us to help until we know exactly what they'll choose to break in 4.0 and you can ask about a specific problem it causes. – Ixrec Mar 16 '15 at 22:55
25

When used properly, enums are far more readable and robust than the "magic numbers" they replace. I don't normally see them making code more brittle. For instance:

  • setColor() doesn't have to waste time checking if value is a valid color value or not. The compiler has already done that.
  • You can write setColor(Color::Red) instead of setColor(0). I believe the enum class feature in modern C++ even lets you force people to always write the former instead of the latter.
  • Usually not important, but most enums can be implemented with any size integral type, so the compiler can choose whatever size is most convenient without forcing you to think about such things.

However, using an enum for color is questionable because in many (most?) situations there's no reason to limit the user to such a small set of colors; you might as well let them pass in any arbitrary RGB values. On the projects I work with, a small list of colors like this would only ever come up as part of a set of "themes" or "styles" that's supposed to act as a thin abstraction over concrete colors.

I'm not sure what your "polymorphic flexibility" question is getting at. Enums don't have any executable code, so there's nothing to make polymorphic. Perhaps you're looking for the command pattern?

Edit: Post-edit, I'm still not clear on what kind of extendability you're looking for, but I still think the command pattern is the closest thing you'll get to a "polymorphic enum".

  • Where can I find out more about not allowing 0 to pass as an enum? – TankorSmash Mar 16 '15 at 23:08
  • 5
    @TankorSmash C++11 introduced the "enum class", also called "scoped enums" which cannot be implicitly converted to their underlying numeric type. They also avoid polluting the namespace as the old C-style "enum" types did. – Matthew James Briggs Mar 16 '15 at 23:16
  • 2
    Enums are usually backed by integers. There are plenty of strange things that can happen in serialization / deserialization or casting between integers and enums. It is not always safe to assume that an enum will always have a valid value. – Eric Mar 17 '15 at 14:08
  • 1
    You're right Eric (and this is an issue I've encountered a number of times myself). However, you only have to worry about a possibly invalid value at the deserialization stage. All the other times that you use enums, you can assume the value is valid (at least for enums in languages like Scala -- some languages don't have very strong type checking for enums). – Kat Mar 18 '15 at 2:53
  • 1
    @Ixrec "because in many (most?) situations there's no reason to limit the user to such a small set of colors" There are legitimate cases. The .Net Console emulates the old-style windows console, which can only have text in one of 16 colours (the 16 colours of the CGA standard) msdn.microsoft.com/en-us/library/… – Pharap Mar 19 '15 at 6:48
15

Any change to the ColorChoice enum affects all IWindowColor subclasses.

No it doesn't. There are two cases: implementers will either

  • store, return and forward enum values, never operating on them, in which case they are unaffected by changes in the enum, or

  • operate on individual enum values, in which case any change in the enum must of course, naturally, inescapably, necessarily, be accounted for with a corresponding change in the logic of the implementer.

If you put "Sphere", "Rectangle", and "Pyramid" in a "Shape" enum, and pass such an enum to some drawSolid() function which you have written to draw the corresponding solid, and then one morning you decide to add an "Ikositetrahedron" value to the enum, you cannot expect the drawSolid() function to remain unaffected; If you did expect it to somehow draw icositetrahedrons without you first having to write the actual code to draw icositetrahedrons, that's your fault, not the enum's fault. So:

Do enums tend to cause brittle interfaces?

No, they don't. What causes brittle interfaces is programmers thinking of themselves as ninjas, trying to compile their code without having sufficient warnings enabled. Then, the compiler does not warn them that their drawSolid() function contains a switch statement which is missing a case clause for the newly added "Ikositetrahedron" enum value.

The way it is supposed to work is analogous to adding a new pure virtual method to a base class: you then have to implement this method on every single inheritor, or else the project does not, and should not, build.


Now, truth be told, enums are not an object-oriented construct. They are more of a pragmatic compromise between the object-oriented paradigm and the structured programming paradigm.

The pure object-oriented way of doing things is not having enums at all, and instead having objects all the way.

So, the purely object-oriented way to implement the example with the solids is of course to use polymorphism: instead of having a single monstrous centralized method which knows how to draw everything and having to be told which solid to draw, you declare a "Solid" class with an abstract (pure virtual) draw() method, and then you add "Sphere", "Rectangle" and "Pyramid" subclasses, each having its own implementation of draw() which knows how to draw itself.

This way, when you introduce the "Ikositetrahedron" subclass, you will just have to provide a draw() function for it, and the compiler will remind you to do that, by not letting you instantiate "Icositetrahedron" otherwise.

  • Any tips on throwing a compile time warning for those switches? I usually just throw runtime exceptions; but compile time could be great! Not quite as great.. but unit tests come to mind. – Vaughan Hilts Mar 17 '15 at 2:53
  • 1
    It has been a while since I last used C++, so I am not sure, but I would expect that supplying the -Wall parameter to the compiler and omitting the default: clause should do it. A quick search on the subject did not yield any more definite results, so this might be suitable as the subject of another programmers SE question. – Mike Nakis Mar 17 '15 at 7:13
  • In C++ there can be a compile time check if you enable it. In C# any check would have to be at run time. Anytime I take a non-flag enum as a parameter, I make sure to validate it with Enum.IsDefined, but that still means that you have to update all uses of the enum manually when you add a new value. See: stackoverflow.com/questions/12531132/… – Mike Mar 17 '15 at 12:33
  • 1
    I would hope that an object-oriented programmer would never make their data transfer object, like Pyramid actually know how to draw() a pyramid. At best it might derive from Solid and have a GetTriangles() method and you could pass it to a SolidDrawer service. I thought we were getting away from the examples of physical objects as examples of objects in OOP. – Scott Whitlock Mar 17 '15 at 20:48
  • 1
    It's ok, just didn't like anyone bashing good old object oriented programming. :) Especially since most of us combine functional programming and OOP more than strictly OOP anymore. – Scott Whitlock Mar 17 '15 at 22:33
14

Enums do not create brittle interfaces. Misuse of enums does.

What are enums for?

Enums are designed to be used as sets of meaningfully named constants. They are to be used when:

  • You know that no values will be removed.
  • (And) You know it is highly unlikely that a new value will be needed.
  • (Or) You accept that a new value will be needed, but rarely enough to warrant fixing all the code that breaks because of it.

Good uses of enums:

  • Days of the week: (as per .Net's System.DayOfWeek) Unless you are dealing with some incredibly obscure calender, there will only ever be 7 days of the week.
  • Non-extensible colours: (as per .Net's System.ConsoleColor) Some may disagree with this, but .Net chose to do this for a reason. In .Net's console system, there are only 16 colours available for use by the console. These 16 colours correspond to the legacy colour palette known as CGA or 'Color Graphics Adapter'. No new values will ever be added, which means that this is in fact a reasonable application of an enum.
  • Enumerations representing fixed states: (as per Java's Thread.State) The designers of Java decided that in the Java threading model there will only ever be a fixed set of states that a Thread can be in, and so to simplify matters, these different states are represented as enumerations. This means that many state-based checks are simple ifs and switches which in practise operate on integer values, without the programmer having to worry about what the values actually are.
  • Bitflags representing non-mutually exclusive options: (as per .Net's System.Text.RegularExpressions.RegexOptions) Bitflags are a very common use of enums. So common in fact, that in .Net all enums have a HasFlag(Enum flag) method built in. They also support the bitwise operators and there's a FlagsAttribute to mark an enum as being intended to be used as a set of bitflags. By using an enum as a set of flags, you can represent a group of boolean values in a single value, as well as having the flags clearly named for convenience. This would be extremely beneficial for representing the flags of a status register in an emulator or for representing the permissions of a file (read, write, execute), or just about any situation where a set of related options are not mutually exclusive.

Bad uses of enums:

  • Character classes/types in a game: Unless the game is a one-shot demo that you are unlikely to use again, enums should not be used for character classes because chances are you will want to be adding a lot more classes. It is better to have a single class representing character and have an in-game character 'type' represented otherwise. One way to handle this is the TypeObject pattern, other solutions include registering character types with a dictionary/type registry, or a mixture of the two.
  • Extensible colours: If you're using an enum for colours that may later be added to it is not a good idea to represent this in enum form, else you will be left forever adding colours. This is similar to the issue above, thus a similar solution should be used (i.e. a variation of TypeObject).
  • Extensible state: If you've got a state machine that may end up introducing many more states, it is not a good idea to represent these states via enumerations. The preferred method is to define an interface for the machine's state, provide an implementation or class that wraps the interface and delegates its method calls (akin to the Strategy pattern), and then alters its state by changing which provided implementation is currently active.
  • Bitflags representing mutually exclusive options: If you're using enums to represent flags and two of those flags should never occur together then you have shot yourself in the foot. Anything that is programmed to react to one of the flags will suddenly react to whichever flag it's programmed to respond to first - or worse, it might respond to both. This sort of situation is just asking for trouble. The best approach is to treat the absence of a flag as the alternative condition if possible (i.e. the absence of the True flag implies False). This behaviour can be further supported through the use of specialised functions (i.e. IsTrue(flags) and IsFalse(flags)).
  • I will add bitflag examples if anyone can find a working or well known example of enums being used as bitflags. I am aware they exist but unfortunately I cannot recall any at the moment. – Pharap Mar 17 '15 at 2:35
  • 1
    .NET's RegexOptions msdn.microsoft.com/en-us/library/… – BLSully Mar 17 '15 at 13:55
  • @BLSully Excellent example. Can't say I've ever used them, but they exist for a reason. – Pharap Mar 19 '15 at 6:04
4

Enums are a great improvement over magic identification numbers for closed sets of values that do not have a lot of functionality associated with them. Usually you don't care about what number is actually associated with the enum; in this case it is easy to extend by adding new entries at the end, no brittleness should result.

The problem is when you have significant functionality associated with the enum. That is, you have this kind of code lying around:

switch (my_enum) {
case orc: growl(); break;
case elf: sing(); break;
...
}

One or two of these is ok, but as soon as you have this kind of meaningful enum, these switch statements tend to multiply like tribbles. Now every time you extend the enum, you need to hunt down all the associated switches to make sure you've covered everything. This is brittle. Sources such as Clean Code will suggest that you should have one switch per enum, at most.

What you should do instead in this case is to use OO principles and create an interface for this type. You may still keep the enum for communicating this type, but as soon as you need to do anything with it, you create an object associated with the enum, possibly using a Factory. This is much easier to maintain, as you only need to look for one place to update: your Factory, and by adding a new class.

1

If you are careful in how you use them, I'd not consider enums harmful. But there are a few things to consider if you want to use them in some library code, as opposed to a single application.

  1. Never remove or reorder values. If you at some point have an enum value in your list, that value should be associated with that name for all eternity. If you want to, you can at some point rename values to deprecated_orc or whatever, but by not removing them you can more easily maintain compatibility with old code compiled against the old set of enums. If some new code can't deal with old enum constants, either generate a suitable error there or make sure no such value reaches that piece of code.

  2. Don't do arithmetic on them. In particular, don't do order comparisons. Why? Because then you might encounter a situation where you can't keep existing values and preserve a sane ordering at the same time. Take for example an enum for compass directions: N=0, NE=1, E=2, SE=3, … Now if after some update you include NNE and so on between the existing directions, you can either add them to the end of the list and thus break the ordering, or you interleave them with existing keys and thus break established mappings used in legacy code. Or you deprecate all the old keys, and have a completely new set of keys, along with some compatibility code which translates between old and new keys for the sake of legacy code.

  3. Choose a sufficient size. By default the compiler will use the smallest integer that can contain all enum values. Which means that if, at some update, your set of possible enums expands from 254 to 259, you suddenly need 2 bytes for every enum value instead of one. This could break structure and class layouts all over the place, so try to avoid this by using a sufficient size in the first design. C++11 gives you a lot of control here, but otherwise specifying an entry LAST_SPECIES_VALUE=65535 should help as well.

  4. Have a central registry. Since you want to fix the mapping between names and values, it is bad if you let third party users of your code add new constants. No project using your code should be allowed to alter that header to add new mappings. Instead they should bug you to add them. This means that for the human and dwarf example you mentioned, enums are indeed a poor fit. There it would be better to have some kind of registry at run-time, where users of your code can insert a string and get back a unique number, nicely wrapped in some opaque type. The “number” might even be a pointer to the string in question, it doesn't matter.

Despite the fact that my last point above makes enums a poor fit for your hypothetical situation, your actual situation where some spec might change and you'd have to update some code seems to fit in quite well with a central registry for your library. So enums should be appropriate there if you take my other suggestions to heart.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.