Ein Lichtblick im Kampf gegen globale Variablen

20 April 2017
Alexander Baier

Ich habe vor kurzem mit Methoden arbeiten müssen, welche von globalem Zustand abhängen. Das heißt, dass die Eingaben einer dieser Methoden nicht ausschließlich durch deren Parameter definiert ist. Zusätzlich hängt die Ausgabe der Methode noch von gewissen global erreichbaren Variablen ab. Im fogenden möchte ich eine minimalinvasive Lösung präsentiern.

Um die Situation anschaulicher zu machen, ist im folgenden ein kleines Beispiel gegeben.

class Document
{
    public string FilePath { get; set; }
}

enum Role
{
    Anonymous, Member
}

class User
{
    public Role Role { get; set; }
}

class Context
{
    public static Context Instance { get; } = new Context();

    public User CurrentUser { get; set; }
}

class PermissionService
{
    private Context Context { get; }

    public PermissionService(Context context)
    {
        this.Context = context;
    }

    public bool IsAllowedToAccessDocument(Document doc)
    {
        // Some arbitrary "domain logic".
        return Context.Instance.CurrentUser.Role == Role.Member
            && doc.FilePath.EndsWith(".xls");
    }
}

class Main
{
    public static int Main(string[] args)
    {
        Context.Instance.CurrentUser = new User()
        {
            Role = Role.Member
        };

        var permissionService = new PermissionService(Context.Instance);

        var someExcelFile = new Document()
        {
            FilePath = "documents\\data.xls"
        };

        bool result = permissionService.IsAllowedToAccessDocument(someExcelFile);
    }
}

Im Beispiel sehen wir, wie der Rückgabewert der Methode PermissionService#IsAllowedToAccessDocument(Document) mitunter von der Rolle des aktuellen Benutzers abhängt. Diese Rolle wird allerdings nicht als Parameter übergeben. Stattdessen wird ein Property auf der Singleton-Instanz der Klasse Context abgefragt.

Sollte nun das Bedürfnis bestehen besagte Methode mit einer anderen Context-Instanz aufzurufen, ist dies nicht möglich. IsAllowedToAccessDocument wird immer die fest kodierte globale Instanz ansprechen.

Sind wir in einer Situation, die es uns erlaubt die betreffende Methode und deren Call-Sites anzupassen, ist es uns möglich einen Parameter vom Typ Role einzuführen, um die Abhängigkeit von der globalen Context-Instanz zu lösen. Sollte dies allerdings nicht der Fall sein, sind wir gezwungen uns einer anderen Lösung zu bedienen.

Indem wir den Context temporär verändern, haben wir indirekten Einfluss auf das Ergebnis von IsAllowedToAccessDocument.

Role previousRole = Context.Instance.CurrentUser.Role;
// Change the global variable to the desired value
Context.Instance.CurrentUser.Role = Role.Anonymous;

// Make the method call
bool result = permissionService.IsAllowedToAccessDocument(someExcelFile);

// Reset the global variable to the state it was before we came along
Context.Instance.CurrentUser.Role = previousRole;
  

Dies ist nur sinnvoll wenn wir uns in einem sequentiellen Kontext befinden. Wird nebenläufig auf die globale Variable zugegriffen, so kann dies ungewollte Konsequenzen für andere Threads haben.

Um es den Aufrufern von IsAllowedToAccessDocument einfacher zu machen, können wir das temporäre setzen der Role-Eigenschaft kapseln und als eine Funktion bereitstellen.

static T WithContextUserRole<T>(Role tempRole, Func<T> body)
{
    Role previousRole = Context.Instance.CurrentUser.Role;
    Context.Instance.CurrentUser.Role = tempRole;

    T result = body();

    Context.Instance.CurrentUser.Role = previousRole;

    return result;
}
  

WithContextUserRole merkt sich die aktuell gesetzte Rolle der Context-Instanz, setzt diese dann auf den gegebenen Wert (tempRole) und führt die gegebene Funktion (body) aus. Dessen Rückgabewert wird zurückgegeben, nachdem die Rolle-Eigenschaft der Context-Instanz wieder auf ihren ursprünglichen Wert zurückgesetzt wurde. Der Aufruf dieser Hilfsfunktion sieht dann wie folgt aus.

bool IsAllowedToAccessDocument(Document doc, Role role)
{
    WithContextUserRole(role, () => permissionService.IsAllowedToAccessDocument(doc));
}
  

Auf diese Art und Weise haben wir eine elegante Möglichkeit gefunden, um die Arbeit mit einer von globalem Zustand abhängigen API zu erleichtern.