Enums and classes
One general piece of advice which I sometimes see is to avoid enums entirely and instead prefer the use of classes to represent what would otherwise be enum values. (This is not meaningful in Java where enums are classes with full extensibility.) Instead of
enum class OfficeNames
{
LAUDS = 0,
PRIME = 1,
TERCE = 2,
SEXT = 3,
NONE = 4,
VESPERS = 5,
COMPLINE = 6
};
one would define an abstract class as a parent:
class OfficeName {
virtual ~OfficeName();
};
and then implement a set of inheriting concrete classes, which should be stateless:
class LaudsName: public OfficeName {
~LaudsName() override;
};
// etc.
Even if you don't have any virtual functions which do something, this makes it impossible to cast an integer directly to the enum type (which can create surprises in handling what you may think is a strictly limited set of values).
The bigger driver of the advice is that it allows the removal of switch statements. Instead of:
void MyClass::performUpdate(const OfficeNames inOfficeName) {
switch (inOfficeName) {
using enum OfficeNames;
case LAUDS:
doSomethingForLauds(m_val);
break;
// etc.
}
}
you can move the functionality into the class:
class OfficeName {
virtual ~OfficeName();
virtual void doSomething(const MyParam inVal) const = 0;
};
and then implement a set of inheriting concrete classes, which should be stateless:
class LaudsName: public OfficeName {
~LaudsName() override;
void doSomething(const MyParam inVal) const override { //.. }
};
// etc.
so that you would avoid the switch statement entirely:
void MyClass::performUpdate(const IOfficeName& inOfficeName) {
inOfficeName.doSomething(m_val);
}
Because the use of the class makes use of virtual functions, it allows for the addition of functionality elsewhere, either via the updating of the called function (a benefit if it is called more than once) or by the addition of new classes implementing the base type.
enum class OfficeNames
{
LAUDS = 0,
PRIME = 1,
TERCE = 2,
SEXT = 3,
NONE = 4,
VESPERS = 5,
COMPLINE = 6
};
one would define an abstract class as a parent:
class OfficeName {
virtual ~OfficeName();
};
and then implement a set of inheriting concrete classes, which should be stateless:
class LaudsName: public OfficeName {
~LaudsName() override;
};
// etc.
Even if you don't have any virtual functions which do something, this makes it impossible to cast an integer directly to the enum type (which can create surprises in handling what you may think is a strictly limited set of values).
The bigger driver of the advice is that it allows the removal of switch statements. Instead of:
void MyClass::performUpdate(const OfficeNames inOfficeName) {
switch (inOfficeName) {
using enum OfficeNames;
case LAUDS:
doSomethingForLauds(m_val);
break;
// etc.
}
}
you can move the functionality into the class:
class OfficeName {
virtual ~OfficeName();
virtual void doSomething(const MyParam inVal) const = 0;
};
and then implement a set of inheriting concrete classes, which should be stateless:
class LaudsName: public OfficeName {
~LaudsName() override;
void doSomething(const MyParam inVal) const override { //.. }
};
// etc.
so that you would avoid the switch statement entirely:
void MyClass::performUpdate(const IOfficeName& inOfficeName) {
inOfficeName.doSomething(m_val);
}
Because the use of the class makes use of virtual functions, it allows for the addition of functionality elsewhere, either via the updating of the called function (a benefit if it is called more than once) or by the addition of new classes implementing the base type.
The code as I have presented it in the examples above discusses passing the object as a parameter, but the same applies to the case where the value is used as a member of a class and we switch on the member to do something -- in this case replacing the enum with a class might be better described as replacing it with a Strategy. You might still have the functionality in the general enum-replacing class, or you might have a factory method for generating the strategy in question.
That is, the switch version would be:
void MyClass::performUpdate(const MyParam inVal) {
switch (m_officeName) {
using enum OfficeNames;
case LAUDS:
doSomethingForLauds(inVal);
break;
// etc.
}
}
and the class version would look like:
void MyClass::performUpdate(const MyParam inVal) {
m_strategy->doSomething(inVal);
}
So far, so good. But there is a wrinkle. If we have a strategy along a different axis, we have something like this
void MyClass::performUpdate(const MyParam inVal, const IOfficeName& inOfficeName) {
m_strategy->doYetAnotherThing(inVal, inOfficeName, m_otherVal);
}
In an enum version we might have:
void MyClass::performUpdate(const MyParam inVal, const OfficeName inOfficeName) {
switch (inOfficeName) {
using enum OfficeNames;
case LAUDS:
m_strategy->doYetAnotherThingForLauds(inVal, m_otherVal);
break;
// etc.
}
}
That is, we have a design choice. We can push the choices around OfficeName into the strategy -- which is the natural thing to do when it's a class -- or we can give the strategy a set of functions which know nothing about the enum but are specialized for the office. (That set of functions might be required just inside the outer call which does pass the enum in, in any case.)
Why would we use an enum, as in the model above? We lose the flexibility of the delayed binding inherent in the virtual call approach. We would do so for performance reasons. Quite apart from the overhead of virtual functions, there's another way of getting rid of the switch statement which allows for very fast dispatching.
As an intermediate step, consider that we could also turn that switch into a command pattern, with a map:
std::map<int, std::function<void(MyParam, MyMemberParam)>> m_commands;
// Assume setup in constructor, then we call:
void MyClass::performUpdate(const MyParam inVal, const OfficeName inOfficeName) {
m_commands[static_cast<int>(inOfficeName)](inVal, m_otherVal);
}
This could be done with the class version, if you give the class an ordering function.
What can't be done as easily with the class (or rather, what requires more overhead with the class than without) is:
std::array<std::function<void(MyParam, MyMemberParam)>, 7> m_commands;
// Again, assume setup in the constructor, with each function being a closure that references m_strategy
void MyClass::performUpdate(const MyParam inVal, const OfficeName inOfficeName) {
m_commands[static_cast<int>(inOfficeName)](inVal, m_otherVal);
}
This is still a command pattern, but it's using a jump table rather than a map for command lookup, based on the explicit conversion between the enum and its assigned integral value. The call site looks the same, but the difference between a map lookup and an array lookup can be significant if it's in a critical place.
You can use a jump table approach with the class, but ... you have to revert to using an enum and adding an extra function:
class OfficeName {
virtual ~OfficeName();
virtual OfficeNames getOfficeName() const = 0;
};
class LaudsName: public OfficeName {
~LaudsName() override;
OfficeNames getOfficeName() const override { return OfficeNames::LAUDS; }
};
void MyClass::performUpdate(const MyParam inVal, const IOfficeName& inOfficeName) {
m_commands[static_cast<int>(inOfficeName.getOfficeName())](inVal, m_otherVal);
}
We usually want a jump table when performance is critical. With some careful design, you can set up the functions so that they don't depend on polymorphism (customizing them either with different logic or with different parameters passed in at setup if we use full-scale functors), avoiding that source of inefficiency as well.
The other thing that may affect a choice is the scope of the concept being abstracted.
A concept with a really broad scope will be used in potentially hundreds of different contexts within even a moderate-sized project (consider "New Order Single" in a FIX protocol environment or "forward roll" in a derivatives environment). The first form of using a class as replacement, i.e.
void MyClass::performUpdate(const IOfficeName& inOfficeName) {
inOfficeName.doSomething(m_val);
}
means that you could end up with an extended interface for IOfficeName. Somewhere along the line, we just ran into a massive violation of the SRP.
There are lots of ways to finesse this, most of which amount to smaller classes or lambdas in a number of narrower domains which get created based on the more general type. We end up with a number of factories (which hide switch or if/else if chains), or we end up using the general command pattern above to do lookups based on object type, where we move the extended code into the lookup creation. If you find yourself doing this all over the place, it may still make more sense for the high-level abstraction to be modelled by a simple enum (possibly with a few related standalone functions, such as operator<< for printing) and deferring the use of classes to model the concept for more narrow contexts, in places where the modelling follows a Strategy, State, or Command pattern.
(When do you need such a class, then? Notionally, where you find yourself needing to stick getType() onto a class interface as a way to organize logic. That's a code smell, and it's one reason why the jump table approach with a class parameter starts to look suspicious.)
That is, the switch version would be:
void MyClass::performUpdate(const MyParam inVal) {
switch (m_officeName) {
using enum OfficeNames;
case LAUDS:
doSomethingForLauds(inVal);
break;
// etc.
}
}
and the class version would look like:
void MyClass::performUpdate(const MyParam inVal) {
m_strategy->doSomething(inVal);
}
So far, so good. But there is a wrinkle. If we have a strategy along a different axis, we have something like this
void MyClass::performUpdate(const MyParam inVal, const IOfficeName& inOfficeName) {
m_strategy->doYetAnotherThing(inVal, inOfficeName, m_otherVal);
}
In an enum version we might have:
void MyClass::performUpdate(const MyParam inVal, const OfficeName inOfficeName) {
switch (inOfficeName) {
using enum OfficeNames;
case LAUDS:
m_strategy->doYetAnotherThingForLauds(inVal, m_otherVal);
break;
// etc.
}
}
That is, we have a design choice. We can push the choices around OfficeName into the strategy -- which is the natural thing to do when it's a class -- or we can give the strategy a set of functions which know nothing about the enum but are specialized for the office. (That set of functions might be required just inside the outer call which does pass the enum in, in any case.)
Why would we use an enum, as in the model above? We lose the flexibility of the delayed binding inherent in the virtual call approach. We would do so for performance reasons. Quite apart from the overhead of virtual functions, there's another way of getting rid of the switch statement which allows for very fast dispatching.
As an intermediate step, consider that we could also turn that switch into a command pattern, with a map:
std::map<int, std::function<void(MyParam, MyMemberParam)>> m_commands;
// Assume setup in constructor, then we call:
void MyClass::performUpdate(const MyParam inVal, const OfficeName inOfficeName) {
m_commands[static_cast<int>(inOfficeName)](inVal, m_otherVal);
}
This could be done with the class version, if you give the class an ordering function.
What can't be done as easily with the class (or rather, what requires more overhead with the class than without) is:
std::array<std::function<void(MyParam, MyMemberParam)>, 7> m_commands;
// Again, assume setup in the constructor, with each function being a closure that references m_strategy
void MyClass::performUpdate(const MyParam inVal, const OfficeName inOfficeName) {
m_commands[static_cast<int>(inOfficeName)](inVal, m_otherVal);
}
This is still a command pattern, but it's using a jump table rather than a map for command lookup, based on the explicit conversion between the enum and its assigned integral value. The call site looks the same, but the difference between a map lookup and an array lookup can be significant if it's in a critical place.
You can use a jump table approach with the class, but ... you have to revert to using an enum and adding an extra function:
class OfficeName {
virtual ~OfficeName();
virtual OfficeNames getOfficeName() const = 0;
};
class LaudsName: public OfficeName {
~LaudsName() override;
OfficeNames getOfficeName() const override { return OfficeNames::LAUDS; }
};
void MyClass::performUpdate(const MyParam inVal, const IOfficeName& inOfficeName) {
m_commands[static_cast<int>(inOfficeName.getOfficeName())](inVal, m_otherVal);
}
We usually want a jump table when performance is critical. With some careful design, you can set up the functions so that they don't depend on polymorphism (customizing them either with different logic or with different parameters passed in at setup if we use full-scale functors), avoiding that source of inefficiency as well.
The other thing that may affect a choice is the scope of the concept being abstracted.
A concept with a really broad scope will be used in potentially hundreds of different contexts within even a moderate-sized project (consider "New Order Single" in a FIX protocol environment or "forward roll" in a derivatives environment). The first form of using a class as replacement, i.e.
void MyClass::performUpdate(const IOfficeName& inOfficeName) {
inOfficeName.doSomething(m_val);
}
means that you could end up with an extended interface for IOfficeName. Somewhere along the line, we just ran into a massive violation of the SRP.
There are lots of ways to finesse this, most of which amount to smaller classes or lambdas in a number of narrower domains which get created based on the more general type. We end up with a number of factories (which hide switch or if/else if chains), or we end up using the general command pattern above to do lookups based on object type, where we move the extended code into the lookup creation. If you find yourself doing this all over the place, it may still make more sense for the high-level abstraction to be modelled by a simple enum (possibly with a few related standalone functions, such as operator<< for printing) and deferring the use of classes to model the concept for more narrow contexts, in places where the modelling follows a Strategy, State, or Command pattern.
(When do you need such a class, then? Notionally, where you find yourself needing to stick getType() onto a class interface as a way to organize logic. That's a code smell, and it's one reason why the jump table approach with a class parameter starts to look suspicious.)
Comments
Post a Comment