Design Tip - Avoid Enum Types In Domain Layer

Problem

An enum is a special value type that lets you specify a group of named numeric constants. They can help make the code more readable, as opposed to using it to represent the 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 the 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 represents the percentage of their contribution (deduction). We could represent the set of NI Letters as an enum,

  1. public enum NationalInsuranceLetters {  
  2.     A,  
  3.     B,  
  4.     C,  
  5.     J,  
  6.     H,  
  7.     M,  
  8.     Z,  
  9.     X  
  10. }  

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 the employee be classified as an 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 in the domain logic.

To implement these business rules, we’ll use the NationalInsuranceLetters enum in our application. For example,

  1. public void CalculateNationalInsurance(NationalInsuranceLetters letter) {  
  2.     if (letter == NationalInsuranceLetters.C || letter == NationalInsuranceLetters.X) Console.WriteLine($ "don't calculate national insurance for {letter}");  
  3.     else Console.WriteLine($ "calculate national insurance for {letter}");  
  4. }  

By default, underlying the 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.

  1. public void CalculateNationalInsurance(NationalInsuranceLetters letter) {  
  2.     if (!Enum.IsDefined(typeof(NationalInsuranceLetters), letter)) throw new ArgumentException($ "invalid letter being passed {letter}");  
  3.     if (letter == NationalInsuranceLetters.C || letter == NationalInsuranceLetters.X) Console.WriteLine($ "don't calculate national insurance for {letter}");  
  4.     else Console.WriteLine($ "calculate national insurance for {letter}");  
  5. }  

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,

  1. public static class NationalInsuranceService {  
  2.     public static bool IsExempt(NationalInsuranceLetters letter) {  
  3.         if (!Enum.IsDefined(typeof(NationalInsuranceLetters), letter)) throw new ArgumentException($ "invalid letter being passed {letter}");  
  4.         return letter == NationalInsuranceLetters.C || letter == NationalInsuranceLetters.X;  
  5.     }  
  6. }  

Now, the calling method could use this service,

  1. public void CalculateNationalInsurance(NationalInsuranceLetters letter) {  
  2.     if (NationalInsuranceService.IsExempt(letter)) Console.WriteLine($ "don't calculate national insurance for {letter}");  
  3.     else Console.WriteLine($ "calculate national insurance for {letter}");  
  4. }  

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 it could throw an exception too.
  • Details of the service must be understood by the programmer consuming it; e.g., in order to 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 the code.

However, when designing applications 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 the 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 NationalInsuranceLetterto encapsulate business rules.

  1. public sealed class NationalInsuranceLetter {  
  2.     private readonly NationalInsuranceLetters letter;  
  3.     private NationalInsuranceLetter(NationalInsuranceLetters letter) {  
  4.         this.letter = letter;  
  5.     }  
  6.     public static NationalInsuranceLetter A = new NationalInsuranceLetter(NationalInsuranceLetters.A);  
  7.     public static NationalInsuranceLetter B = new NationalInsuranceLetter(NationalInsuranceLetters.B);  
  8.     public static NationalInsuranceLetter C = new NationalInsuranceLetter(NationalInsuranceLetters.C);  
  9.     public static NationalInsuranceLetter H = new NationalInsuranceLetter(NationalInsuranceLetters.H);  
  10.     public static NationalInsuranceLetter J = new NationalInsuranceLetter(NationalInsuranceLetters.J);  
  11.     public static NationalInsuranceLetter M = new NationalInsuranceLetter(NationalInsuranceLetters.M);  
  12.     public static NationalInsuranceLetter X = new NationalInsuranceLetter(NationalInsuranceLetters.X);  
  13.     public static NationalInsuranceLetter Z = new NationalInsuranceLetter(NationalInsuranceLetters.Z);  
  14.     public bool IsExempt() => this.letter == NationalInsuranceLetters.C || this.letter == NationalInsuranceLetters.X;  
  15. }  

This could be used by the caller like this:

  1. public void CalculateNationalInsurance(NationalInsuranceLetter letter) {  
  2.     if (letter.IsExempt()) Console.WriteLine($ "don't calculate national insurance for {letter}");  
  3.     else Console.WriteLine($ "calculate national insurance for {letter}");  
  4. }  

Note that this class can’t be instantiated, its constructor is private.

  1. var letter = new NationalInsuranceLetter(); // won't compile  

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.

  1. CalculateNationalInsurance(NationalInsuranceLetter.X);  

Now, that you have an abstraction, you could add related behavior to it, encapsulating domain logic, increasing reuse and making changes easy. For instance, some of the other rules could be implemented, like:

  1. public bool CanDefer() => this.letter == NationalInsuranceLetters.J || this.letter == NationalInsuranceLetters.Z;  
  2. public bool ForUnder21() => this.letter == NationalInsuranceLetters.M || this.letter == NationalInsuranceLetters.Z;  
  3. 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 behavior to it will 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


Similar Articles