Problem
An enum
is a special value type that lets you specify a group of named numeric constants. They can help make code more readable, as opposed to using int
to represent constants or flags. However, using them to represent domain abstractions, within the domain layer, could lead to:
- Poor encapsulation of domain concepts
- Duplication of business rules
- Difficult to incorporate changes
- Exceptions due to invalid state
Let’s look at an example, imagine we’re developing a payroll application that calculates National Insurance (NI) for employees. Every employee has NI Letter that represent the percentage of their contribution (deduction). We could represent the set of NI Letters as an enum
:
public enum NationalInsuranceLetters
{
A,
B,
C,
J,
H,
M,
Z,
X
}
Then within our application we’ll use Employee’s NI Letter to decide:
- Whether an employee can pay NI
- Percentage of contribution
- Can the employee defer NI
- Will employee be classified as apprentice for NI purposes
Note: this is not a definite list of business rules related to National Insurance Letters, they are too many to list here. I am keeping the rules and sample code simple to keep our focus on enums and their use within domain logic.
To implement these business rules we’ll use the NationalInsuranceLetters enum
in our application, for example:
public void CalculateNationalInsurance(NationalInsuranceLetters letter)
{
if (letter == NationalInsuranceLetters.C ||
letter == NationalInsuranceLetters.X)
Console.WriteLine($"don't calculate national insurance for {letter}");
else
Console.WriteLine($"calculate national insurance for {letter}");
}
By default underlying value of enum
is of type int
, hence in order to avoid exceptions thrown in case of invalid value passed to this function, we’ll need to add a guard clause:
public void CalculateNationalInsurance(NationalInsuranceLetters letter)
{
if (!Enum.IsDefined(typeof(NationalInsuranceLetters), letter))
throw new ArgumentException($"invalid letter being passed {letter}");
if (letter == NationalInsuranceLetters.C ||
letter == NationalInsuranceLetters.X)
Console.WriteLine($"don't calculate national insurance for {letter}");
else
Console.WriteLine($"calculate national insurance for {letter}");
}
This seems quite harmless and is probably OK for demo or simple applications. However, as the complexity grows, this little piece of logic (i.e. to check for valid value and if employee is exempt from NI) would be duplicated throughout your application. Several other business rules related to NI Letters would be spread throughout your code in a similar way.
You could solve the duplication issue by creating methods in common library/service/utility class:
public static class NationalInsuranceService
{
public static bool IsExempt(NationalInsuranceLetters letter)
{
if (!Enum.IsDefined(typeof(NationalInsuranceLetters), letter))
throw new ArgumentException($"invalid letter being passed {letter}");
return letter == NationalInsuranceLetters.C ||
letter == NationalInsuranceLetters.X;
}
}
Now the calling method could use this service:
public void CalculateNationalInsurance(NationalInsuranceLetters letter)
{
if (NationalInsuranceService.IsExempt(letter))
Console.WriteLine($"don't calculate national insurance for {letter}");
else
Console.WriteLine($"calculate national insurance for {letter}");
}
Although this is an improvement, I am still not happy with NationalInsuranceService
:
- Method signature for
IsExcept()
method is not honest, it’s promising to return Boolean but could throw exception too. - Details of the service must be understood by programmer consuming it e.g. in order understand why the exception was thrown, this leads to poor encapsulation.
- It is only at runtime that an invalid value for NI Letter is caught, the design is not defensive i.e. allows a programmer to write code that could potentially fail.
- This is not an object-oriented design, instead a procedural design implemented using object-oriented constructs.
How can we improve on this? I’ll provide one solution that I prefer – domain abstractions.
Solution
When we think in terms of enum
, int
or string
etc, we’re thinking in terms of programming abstractions. There is nothing wrong with that, when writing code.
However, when designing application or with a designer hat on during coding, we need to think in terms of domain abstractions. That is, thinking in terms of concepts present in business domain for which we’re writing the application e.g. NI Letter, Gender, Marital Status are not just enumeration lists, they are concepts within the domain of payroll processing.
Thus to represent the abstraction of NI Letter, you could create a class NationalInsuranceLetter
to encapsulate business rules:
public sealed class NationalInsuranceLetter
{
private readonly NationalInsuranceLetters letter;
private NationalInsuranceLetter(NationalInsuranceLetters letter)
{
this.letter = letter;
}
public static NationalInsuranceLetter A =
new NationalInsuranceLetter(NationalInsuranceLetters.A);
public static NationalInsuranceLetter B =
new NationalInsuranceLetter(NationalInsuranceLetters.B);
public static NationalInsuranceLetter C =
new NationalInsuranceLetter(NationalInsuranceLetters.C);
public static NationalInsuranceLetter H =
new NationalInsuranceLetter(NationalInsuranceLetters.H);
public static NationalInsuranceLetter J =
new NationalInsuranceLetter(NationalInsuranceLetters.J);
public static NationalInsuranceLetter M =
new NationalInsuranceLetter(NationalInsuranceLetters.M);
public static NationalInsuranceLetter X =
new NationalInsuranceLetter(NationalInsuranceLetters.X);
public static NationalInsuranceLetter Z =
new NationalInsuranceLetter(NationalInsuranceLetters.Z);
public bool IsExempt() =>
this.letter == NationalInsuranceLetters.C ||
this.letter == NationalInsuranceLetters.X;
}
This could be used by the caller like:
public void CalculateNationalInsurance(NationalInsuranceLetter letter)
{
if (letter.IsExempt())
Console.WriteLine($"don't calculate national insurance for {letter}");
else
Console.WriteLine($"calculate national insurance for {letter}");
}
Note that this class can’t be instantiated, its constructor is private
:
var letter = new NationalInsuranceLetter();
This means that you’ve made it impossible for anyone using this class to instantiate in an invalid state. Thanks to the factory methods (implemented as static properties), the usage of this would be similar to enums:
CalculateNationalInsurance(NationalInsuranceLetter.X);
Now that you have an abstraction, you could add related behaviour to it, encapsulating domain logic, increasing re-use and making changes easy. For instance, some of the other rules could be implemented like:
public bool CanDefer() =>
this.letter == NationalInsuranceLetters.J ||
this.letter == NationalInsuranceLetters.Z;
public bool ForUnder21() =>
this.letter == NationalInsuranceLetters.M ||
this.letter == NationalInsuranceLetters.Z;
public bool ForApprenticeUnder25() =>
this.letter == NationalInsuranceLetters.H;
Encapsulation is a fundamental tenant of good software design. Wrapping an enum into an object and providing cohesive behaviour to it would lead you down the path of well encapsulated and designed applications.
Note: this is not the only way to design your code, even if you don’t agree with my solution, hopefully it will add a technique to your repertoire.
Source Code
GitHub: https://github.com/TahirNaushad/Fiver.Design.EnumToObject