Click here to Skip to main content
15,888,984 members
Articles / All Topics

Design Tip – Avoid Enum Types in Domain Layer

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
7 May 2018CPOL3 min read 14K   4   6
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..Continue reading

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(); // 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:

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



Comments and Discussions

 
GeneralGuard Clause? Pin
Kirk Wood8-May-18 18:09
Kirk Wood8-May-18 18:09 
GeneralRe: Guard Clause? Pin
User 10432649-May-18 11:17
User 10432649-May-18 11:17 
GeneralRe: Guard Clause? Pin
Chad3F10-May-18 9:29
Chad3F10-May-18 9:29 
How is this any different from a null value? If a language has type checked non-nullable arguments, then never passing such methods null should be enforced by the caller (automatically by the compiler/runtime, if needed), rather than make each method do an explicit check for "garbage input".

Ideally, it would a compile-time error to just cast an arbitrary int to an enum. Instead it might use a hypothetical method like:

var value = get from DB
CalculateNationalInsurance_Enum(Enum.FromValue(typeof(NationalInsuranceLetters), value));

In this example, Enum.FromValue() would throw an explicit exception. Then it is more apparent to the developer to handle an exception on bad data. If it was done as you suggested (i.e. using a class), such a convert function would need to validate bad DB values too (likely generating an exception itself).
GeneralCan't agree with you Pin
Klaus Luedenscheidt7-May-18 19:24
Klaus Luedenscheidt7-May-18 19:24 
GeneralRe: Can't agree with you Pin
User 10432649-May-18 11:22
User 10432649-May-18 11:22 
GeneralRe: Can't agree with you Pin
Klaus Luedenscheidt9-May-18 19:34
Klaus Luedenscheidt9-May-18 19:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.