Die Retusche - Exception Handling mit EBCs


Circus Tiger
Wenn alles gut läuft, fragt auch keiner, ob es auch sicher ist. Oder etwa doch.

Als Entwickler sind wir immer daran gehalten guten Code abzuliefern. Dieser Code wird durch allerlei Tests gesichert. Dennoch sind wir immer etwas unruhig, ob wir auch an alle Eventualitäten gedacht haben.

Ich sage euch, haben wir nicht.

Wir sind einfach nicht in der Lage alle Eventualitäten, Szenarien oder mögliche Impacts zu bedenken. Daher bauen wir in unseren Code Fehlerbehandlungen ein, die unser Sicherungsseil oder -netz bilden.

Leider sind wir wirklich nur Menschen, die es bequem mögen. Ein Exception Handling, das sperrig im Code lungert und den Blick aufs Wesentliche verschleiert oder gar versperrt, ist ein schlechtes Handling. Aber zum Glück gibt es ja AOP, das dieses Manko nahezu entfernt.
Mit ein paar Attributen oder Dekorationen ist das Exception Handling schön weggekapselt und stört den Code nicht mehr.
Wir können uns beruhigt nach hinten lehnen und uns anderen schönen Dingen widmen, sofern wir uns nicht im Umfeld der Event based Components befinden. Hier werden nur Signale zwischen Komponenten ausgetauscht, die über ein Board “verdrahtet” wurden.
Genug der vielen theoretischen Worte, bringen wir ein wenig Farbe ins Spiel.

Das Szenario

Ich möchte einen Bezahldienstleister nutzen, um dem Kunden die Möglichkeit zu nutzen, die bestellten Produkte online zu bezahlen. Problematisch ist, das ich unterschiedliche Dienstleister für unterschiedliche Bezahlarten und -regionen nutzen muss, weil es keinen intergalaktisch globalen gibt, der alles kann.

Zwei klassische Lösungen

Die erste Lösung wird über die Constructor Injection realisiert. Der Exception Handler wird entweder per DI Container oder händisch programmiert in die Komponente übergeben, in der das Exception Handling stattfinden soll.

public class PaymentService : IPaymentService {
    IPaymentProvider _paymentProvider;

    public PaymentService(IPaymentProvider paymentProvider) {
   
    this._paymentProvider = paymentProvider;
    }
    ... 
}

public class PaymentProvider : IPaymentProvider{
    IExceptionHandler  _exceptionHandler;
    public PaymentProvider (IExceptionHandler exceptionHandler) {
        this._exceptionHandler = exceptionHandler;
    }

    public void DoWhat() {
         try {

             ...
         }
         catch (Exception ex) {
        
    this._exceptionHandler.HandleException(ex);
         }
    }
}


Das ist einfach und jeder versteht was passiert. Aber tatsächlich lenkt der Code von der eigentlichen Aufgabe ab und senkt damit die Lesbarkeit. Das ist nicht im Sinne der Clean Code Developer.
Die zweite Lösung zeigt das gleiche Szenario, aber jetzt mit dem AOP Prinzip.

public class
PaymentService : IPaymentService {
    IPaymentProvider _paymentProvider;

    public PaymentService(IPaymentProvider paymentProvider) {
   
    this._paymentProvider = paymentProvider;
    }
    ... 
}

public class PaymentProvider : IPaymentProvider{

    public PaymentProvider () {
    }

    [HandleException]
    public void DoWhat() {
 
        ...
    }

}

Am Service hat sich nichts geändert, der Provider ist schön klein geworden und Try-Catch-Anweisungen verschwinden aus dem Code. Das finde ich wirklich gut. Aber was passiert, wenn in der Methode DoWhat unterschiedliche Exceptions unterschiedlich behandelt werden sollen?
Ganz einfach: für jede Behandlung wird ein Aspekt angefügt

public class PaymentProvider : IPaymentProvider{
    public PaymentProvider () {
    }

    [HandleException(typeof(ArgumentNullException), typeof(MapedException)]
    [HandleException(typeof(ArithmeticException)]
    [HandleException(typeof(ApplicationException), "Andere Exception Message"]
    public void DoWhat() {

        ...
    }
}


Das soll nur ein kurzes Beispiel sein, ich habe schon viel schlimmeres gesehen. Die Sache wird sogar noch schlimmer, denn das Ändern der Exception Message oder das Mapping von Exceptions passiert früher oder später mehrfach und verletzt damit das DRY-Prinzip.
Eine sinnvolle Lösung währe ein Exception Handling, das tatsächlich zentral an einer Stelle geschieht. Das hat den netten Nebeneffekt, dass das SoC-Prinzip umgesetzt wird und befreit den Code wieder von den Aspekten.


Die EBC Lösung

EBC Payment Ich habe mich für ein Mainboard entschieden, auf das eine MessageFactory und ein  PaymentProvider verdrahtet werden. Die MessageFactory übersetzt die Service Message in eine spezialisierte Message für den Payment Provider. Der Payment Provider erledigt seine Aufgabe und sendet ein Resultat über das Getane zurück.

Soweit so gut.
Wenn nun aber eine Exception auftritt, soll der Applikation Bescheid gegeben werden, dass ein Problem auftrat. Wie soll das mit den EBCs geschehen? 

Ganz einfach: jede Komponente kann standardmäßig Out-Pins erhalten, die die Exceptions ausgeben und ein Board verdrahtet diese Out-Pins mit ein oder mehreren Fehlerbehandlungskomponenten.

Diese Komponente kann nun mit den Exceptions Dinge tun, die mit der klassischen Try-Catch-Behandlung nicht unbedingt oder nur sperrig möglich gewesen wären. 
Hier nun der Code dazu:


public class PaymentBoard : IMainBoard {
    public event Action<IMessageResult> Out_Result;
    public event Action<Exception> Out_Exception;

    private Action<IMessage> _StartSequenz;
    public PaymentBoard(
        IPaymentProvider paymentProvider,
        IMessageFactory messageFactory,
        IExceptionHandler exceptionHandler) {

        // Verdrahten
        this._StartSequenz = messageFactory.In_Message;
        messageFactory.Out_ProviderMessage = paymentProvider.In_SendMessage;
        messageFactory.Out_MessageResult = this.Out_Result;

        paymentProvider.Out_Result = messageFactory.In_ProviderMessageResult;
        paymentProvider.Out_Exception += exceptionHandler.In_HandleException;

        exceptionHandler.OutHandledException += this.OnException;
    }

    private void OnException(Exception exception) {
        if (this.Out_Exception != null)
            this.Out_Exception(exception);
    }

    public void In_SendMessage(IMessage message) {
        this._StartSequenz(message);
    }
}


Ich gebe der Komponente also einen Exception Handler mit, das ist nichts neues. Aber was neu ist, ich verbinde den Ausgang paymentProvider.Out_Exception mit dem Eingang des Exception Handlers

    paymentProvider.Out_Exception +=
        new Action<Exception>(exceptionHandler.In_HandleException);

Das bedeutet, dass alle Exceptions der Payment Provider Instanz an den Exception Handler übergeben werden. Was er dann daraus macht ist nicht mehr von Interesse für das MainBoard oder die Payment Provider Instanz. Ich habe erfolgreich meine Exception Handling weggekapselt.
Was passiert im Payment Provider?
Im Prinzip das gleiche.
public class PaymentProvider : IPaymentProvider {
    public Action<IProviderMessageResult> Out_Result { get; set; }
    public event Action<Exception> Out_Exception;

    public void In_SendMessage(IProviderMessage message) {
        try {
            ...
           
        }
        catch (Exception ex) {
            this.OnException(ex);
        }
    }
 
    private void OnException(Exception ex) {
        if (this.Out_Exception != null)
            this.Out_Exception(ex);
    }
}


Definieren eines Out Pins für die Exceptions und anlegen einer OnException Methode, über die die Exceptions versendet werden.
Wenn in der Methode In_SendMessage eine Exception auftritt, dann wird diese an den Out Pin gesendet, um dann an andere Stelle weiter verarbeitet zu werden.
Leider ist hier wieder der berühmte try-catch-Block, aber den bekommen wir auch noch weg.

Die Exception Handler Komponente

public interface IExceptionHandler {
        void In_HandleException(Exception exceptionToHandle);
        event Action<Exception> OutHandledException;
}

public class DefaultExceptionHandler : IExceptionHandler {

    public event Action<Exception> OutHandledException;
    public void In_HandleException(Exception exceptionToHandle) {
        /// Tue Dinge mit der Exception
         
        this.OnHandledException(exception)
    }

    private void OnHandledException(Exception exception) {
        if (this.OutHandledException != null)
            this.OutHandledException(exception);
    }
}

In dieser Komponente kann nun die Exception behandelt werden. Wie das geschehen soll kann auf die unterschiedlichsten Arten definiert werden. Wichtig ist, dass die Behandlung einer Exception anschließend an den Ausgang gesendet wird, damit in weiteren Schritten Trace, Console oder Logger die Behandlung weiter geführt werden kann.

Fazit

Ich habe mit Hilfe einer Komponente die Verarbeitung von Exceptions im EBC Konzept weit weg organisiert.
Was ist die Besonderheit dabei. Tja, im EBC Umfeld ist das Exception Handling eine Bauteil wie jedes andere auch und wird mit Nachrichten, den Exceptions, versorgt. Diese werden entkoppelt von der Applikation behandelt und an weitere Metaverwaltungsschritte weitergegeben. Dies geschieht völlig transparent von der Lösungsdomäne und lenkt somit nicht von der eigentlichen Aufgabe der Applikation ab.
In weiteren Entwicklungsschritten kann man über ganze Exception Handling Platinen nachdenken, solche, die ganze nachgelagerte Prozesse durchführen und dann auch Einfluss auf die Applikation nehmen. Doch lassen wir dieses kleine Kind erst einmal größer werden.

Jan(ek)