Refactoring to Patterns: Enum durch Strategy ersetzen mit verschiedenen Typen

Written on November 04, 2009

Einer der Klassiker beim Refactoring ist das Ersetzen von Enums durch das Strategy-Pattern.

Wikipedia schreibt zum Enum:

Ein Aufzählungstyp (englisch enumerated type) ist ein Datentyp mit einem endlichen Wertebereich. Alle Werte des Aufzählungstyps werden bei der Deklaration des Typs mit Namen definiert. Dabei wird auch eine Reihenfolge festgelegt, die eine Ordnung der einzelnen Werte bestimmt, die Werte können also sortiert werden.

Die Wurzel des Übels

Und genau hier fängt auch das Problem an: Der Enum wird irgendwann eingeführt, um z.B. einen Status für eine Applikation abzubilden, als da wären AppState.Started, AppState.Stopped.

Irgendwann im Laufe des Lebenszyklus' der Anwendung benötigen wir noch AppState.Paused und nochmal später AppState.Restarted.

Warum ist dies nun schlecht?

Der Grund ist, dass wir irgendwo (d.h. ziemlich sicher: an zig Stellen innerhalb unserer Applikation) AppState auch auswerten. Wenn wir nun neue AppStates einführen, müssen diese Stellen (Klassen) ebenfalls angepasst werden.

Dies ist ein Verstoß gegen das Open-Closed-Principle (OCP), das besagt, dass Software-Einheiten offen sein sollten für Erweiterungen, aber geschlossen für Modifikationen.

Besser ist es, deshalb, den Enum z.B. durch das Strategy-Pattern oder State-Pattern zu ersetzen.

Man sollte auch nochmal das Zitat oben beachten -- der Enum hat einen endlichen Wertebereich, weshalb nicht alle Enums evil sind, z.B. hat ein Enum Wochentage sehr wohl seine Berechtigung.

Open. Closed. Principle

Bei den Arbeiten an einem Wrapper für ein internes Projekt ist mir folgendes Problem untergekommen:

Die Klasse CustomProperty besitzt eine Eigenschaft PropertyType, welche durch einen Enum abgebildet wird:

enum PropertyType { 
    CustomInfoUnknown, 
    CustomInfoText, 
    CustomInfoDate, 
    CustomInfoNumber, 
    CustomInfoDouble, 
    CustomInfoYesOrNo 
}

Abhängig von PropertyType soll die Ausgabe von CustomProperty anders formatiert werden -- z.B. soll beim Double die Formatierung so aussehen:

string.Format("Der Wert lautet {0}", propertyValue");

Nun möchte ich natürlich nicht für jeden Datentyp eine eigene CustomProperty-Klasse entwickeln, weshalb sich hier Generics anbieten.

Außerdem ist die Formatierung eigentlich auch nicht Aufgabe der Property, denn ich möchte die Formatierung ja auch beliebig austauschen können.

Deshalb wird hierfür ein generischer CustomPropertyFormatter eingeführt, der an die CustomProperty bei der Instanzierung übergeben wird.

Somit werden wir auch noch einem weiteren SOLID-Principle gerecht: Single Responsibility Principle, kurz SRP.

Letztlich bin ich bei folgender Implementierung angekommen:

public class CustomProperty<T>
{
    private CustomPropertyFormatter<T> _formatter;
    private T _value;

    public CustomProperty(T propertyValue, CustomPropertyFormatter<T> formatter)
    {
        _formatter = formatter;
        _value = propertyValue;
    }

    T Value { get { return _value; } }

    public new string ToString()
    {
        return _formatter.ToString(_value);
    }
}

public interface ICustomPropertyFormatter<T>
{
    string ToString(T value);
}

public abstract class CustomPropertyFormatter<T> : ICustomPropertyFormatter<T> {
    public abstract string ToString(T value);

}

public class IntCustomPropertyFormatter : CustomPropertyFormatter<int> {
    public override string ToString(int value)
    {
        return string.Format("Die Zahl lautet {0}",value);
    }
}

Der Aufruf erfolgt über:

CustomProperty<int> property = 
    new CustomProperty<int>(5, new IntCustomPropertyFormatter());

Da ich mich selbst noch auf dem Weg zum CCD befinde, muss dieser Code nicht der Weisheit letzter Schluß sein -- Anregungen und Kritik sind deshalb gerne willkommen ;-)

Danke an Chris und Peter fürs Zuhören / Diskutieren / Pair-Programming ;-)