Smart Enum

Gdyby enumy miały metody

Piotr Wandycz

Typy wyliczeniowe w C# są przydatne i przeważnie z jeden czy dwa przewijają się w każdym projekcie. Niestety prawie zawsze idą w parze z instrukcją switch, a to z kolei może wiązać się z łamaniem zasady otwarte-zamknięte z SOLID. Pomocny może być tu samoopisujący się enum, ale do tego musielibyśmy mieć możliwość definiowania w nim metod. Jest to możliwe w Javie, a w C# jeszcze nie, chyba że użyjemy biblioteki Smart Enum.

Załóżmy, że istnieją u nas w kodzie takie dwa kawałki, związane ze sobą logicznie, ale w różnych miejscach:

public enum EmployeeType
{
    Manager,
    Assistant
}
public decimal CalculateBonusSize(EmployeeType employee)
{
    switch (employee)
    {
        case EmployeeType.Assistant:
            return 1_000m;
        case EmployeeType.Manager:
            return 10_000m;
        default:
            throw new ArgumentException();
    }
}

Chcemy tu zrobić małą refaktoryzację w kierunku wzorca strategii i spowodować, że każdy wariant enuma sam wskaże – ile wynosi BonusSize. Pozbędziemy się switcha i wprowadzimy możliwość dopisywania nowego kodu, bez ruszania starego, aby był “otwarty na rozszerzenie, zamknięty na modyfikację”. Wadą Smart Enum jest jego czytelność, gdyż ten prosty przypadek z góry po przeniesieniu jeden do jednego będzie wyglądał tak:

public sealed class EmployeeType : SmartEnum<EmployeeType>
{
    public static readonly EmployeeType Manager = new EmployeeType(nameof(Manager), 1);
    public static readonly EmployeeType Assistant = new EmployeeType(nameof(Assistant), 2);

    private EmployeeType(string name, int value) : base(name, value)
    {
    }
}

Nie twierdzę, że jest to kod niemożliwy do zrozumienia, ale z 5 wyrazów deklaracji robi się około 40. Dodatkowo był to krok opcjonalny i pośredni, aby pomógł w lepszym zrozumieniu, co dzieje się po przeniesieniu naszej logiki wyliczania bonusu. Gdyby zmiksować dwa kawałki kodu z góry strony, powstanie coś takiego:

public abstract class EmployeeType : SmartEnum<EmployeeType>
{
    public static readonly EmployeeType Manager = new ManagerType();
    public static readonly EmployeeType Assistant = new AssistantType();

    private EmployeeType(string name, int value) : base(name, value)
    {
    }

    public abstract decimal BonusSize { get; }

    private sealed class ManagerType : EmployeeType
    {
        public ManagerType() : base("Manager", 1) { }

        public override decimal BonusSize => 10_000m;
    }

    private sealed class AssistantType : EmployeeType
    {
        public AssistantType() : base("Assistant", 2) { }

        public override decimal BonusSize => 1_000m;
    }
}

Uzyskujemy tu twór działający jak enum – można przypisać EmployeeType.Manager – a jednocześnie zostaje rozszerzony o kawałek spójnej logiki. Oczywiście używać trzeba tego bardzo ostrożnie, bo zrobi się większy śmietnik w kodzie niż przedtem. Jednak trzymanie domeny w jednym miejscu może być benefitem, zwłaszcza że prosto jest to testować. Nic nie stoi na przeszkodzie, aby wprowadzić parametry do tych funkcji. Zaprezentowane przykłady nie są spektakularne, mają na celu pokazanie możliwości i mam nadzieję – skłonienie do refleksji.

public abstract class EmployeeType : SmartEnum<EmployeeType>
{
    public static readonly EmployeeType Manager = new ManagerType();
    public static readonly EmployeeType Assistant = new AssistantType();

    private EmployeeType(string name, int value) : base(name, value)
    {
    }

    public abstract decimal CalculateBonusSize(byte yearsWorked);

    private sealed class ManagerType : EmployeeType
    {
        public ManagerType() : base("Manager", 1) { }

        public override decimal CalculateBonusSize(byte yearsWorked) 
            => yearsWorked > 5 ? 10_000m : 7_000m;
    }

    private sealed class AssistantType : EmployeeType
    {
        public AssistantType() : base("Assistant", 2) { }

        public override decimal CalculateBonusSize(byte yearsWorked)
            => yearsWorked > 2 ? 2_000m : 1_000m;
    }
}

Temat przedstawiam jako ciekawostkę, a nie jako absolutne rozwiązanie konkretnego problemu. Podczas projektowania systemu warto jednak pamiętać, że taka biblioteka istnieje i może kiedyś nam się przydać.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *