Discriminants are Stinky
I recently came across the suggestion of implementing a ColorValue type that uses an enum field to differentiate different color spaces that can be represented in C# as the following:
enum ColorSpace
{
Cmyk,
Rgb,
CieLab
}
class ColorValue
{
public ColorSpace ColorSpace { get; init; }
public double[]? Component { get; init; }
}
The above concept has been used for discriminated unions found in C code when unions were introduced to the language in 19731. Bjarne Stroustrup finds weakness in this approach when discussing “type fields” as he calls them2:
Any addition of a new kind of [type] involves a change to all the key functions in the system — the ones containing the tests on the type field. The programmer must consider every function that could conceivably need a test on the type field after a change. […] In other words, use of a type field is an error-prone technique that leads to maintenance problems. The problems increase in severity as the size of the program increases because the user of a type field causes a violation of the ideals of modularity and data hiding. (310)
Although Stroustrup goes on to talk about moving away from “type fields” towards C++’s derived types and polymorphism, software engineering concepts have been debated and evolved through the decades since Stroustrup provided methods of localizing implementation knowledge to specific types. Compilers have also since been updated to report when an enum value is not handled in a switch statement.
The use of discriminants or “type fields” is a stinky code smell, because it potentially violates the open-closed principle, which Robert C. Martin presented around the time Stroustrup’s book was published and remains popular today. The issue can be exemplified with the following function:
void MyFunctionA(ColorValue v)
{
switch(v.ColorSpace)
{
case ColorSpace.Cmyk:
// handle CMYK ColorValue
break;
case ColorSpace.Rgb:
// handle RGB ColorValue
break;
default:
throw new ApplicationException();
}
}
Now, imagine that there are tens or hundreds of similar functions that examine the ColorSpace field before performing operations that are specific for a particular type. Each of these functions would need to be reviewed or updated when a new ColorSpace, such as CIEXYZ, is introduced. These functions need to remain open for modification when functionality is extended.
Refactoring toward an implementation consistent with SOLID principles might include grouping these functions into interfaces. Then, new classes can be introduced that implement these interfaces to operate with new types of color space objects, such as those of type ColorCieXyz.
- History of C – cppreference.com
- Stroustrup, Bjarne. The C++ Programming Language. Addison-Wesley, 1997.