(S)Pinnen
In meinem letzten Post habe ich die Idee der EBCs vorgestellt und eine grafische Notation (ich benutze dafür Dia) sowie eine Schnittstellendefinition angeboten, die ich für die weiteren Posts benutzen werde. Der Code, der dabei entstanden ist, ließ sich zwar kompilieren, aber nicht ausführen. Heute soll ein bischen “Butter bei die Fische” kommen, wie wir hier im Norden sagen – sprich: es gibt Code, der auch ausführbar ist. Als Ziel des Codings wird sich – wie man aus meinen grafischen Beispielen vielleicht schon erahnen konnte – eine kleine Adressbuch-Anwendung ergeben. Ich werde in späteren Posts noch etwas mehr auf das Design eingehen.
Ralf hat in seinem Kommentar zum ersten Post darauf hingewiesen, dass “Component” eigentlich kein passender Bezeichner mehr ist. In der Tat ist der Begriff Komponente in der Programmierung anders belegt. Sein Vorschlag, als Oberbegriff für Bauteile und Platinen Funktionseinheiten (FEs) zu verwenden, finde ich zwar ein wenig sperrig, auch wenn es die Sache vielleicht trifft. In Ermangelung eines besseren Vorschlags werde ich ihn aber erstmal übernehmen.
Pins
Allen FEs ist gemeinsam, dass sie Pins haben. Ohne diese ist kein Nachrichtenversand möglich. Art und Anzahl der Pins kann natürlich stark variieren. Es mag FEs geben, die nur Ausgabepins haben, solche, die nur Eingabepins haben und natürlich welche mit beidem. Das ist stark von der Aufgabe der FE abhängig. Ich möchte hier nochmal darauf hinweisen, dass der Pin vorrangig ein Design-Element ist, also nicht deine bestimmte Umsetzung impliziert. Auch die klassischen Java-Eventhandler erfüllen dieses Designmuster vorbildlich:
private javax.swing.JButton btnOK = new JButton(); public ListenerAsPinExample() { btnOK.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { // mach was } }); }
Hier stellt der Button einen Ausgangspin bereit, an den ein ActionListener als Eingangspin gehängt wird. Als Datum wird das ActionEvent übergeben. Das Muster ist sauber eingehalten: keine der FEs ist fest an die andere gebunden und es wird genau ein Datum übergeben. (source)
Will man Pins implementieren, so fällt auf, dass der InPin immer direkt funktional implementiert wird – er hat keinerlei Verwaltungsoverhead. Die direkteste Implementierung ist daher eine anonyme Klasse. (source)
OutPin<String> outPin; public Funktionseinheit() { outPin.wire(new InPin<String>() { public void receive(String message) { // mach was } }); }
Ein OutPin hat etwas Verwaltungsoverhead, da er für die Verbindung zum InPin verantwortlich ist. Dies ist ein simpler 1:1 Pin. Einen anderen Typen werden wir später kennen lernen. (source)
public void OutPinImpl<T> implements OutPin<T> { private InPin<T> inPin; public void wire(InPin<T> pin) { inPin = pin; } public void send(T message) { inPin.receive(message); } }
Logging klassisch

"Abfrage erstellen" mit Kontext
Logging ist ein wichtiges, aber (zumindest bei mir) ungeliebtes Thema. Logging müllt den Programmcode zu mit Dingen, die nichts mit der eigentlichen Aufgabe der Klasse zu tun haben. Und erzeugt neue Abhängigkeiten. Die Implementierung von “Abfrage erstellen” mit Logging könnte in etwa so aussehen (ich benutze die Logging-Fassade slf4j, um unabhängig vom konkreten Logging-Framework zu bleiben). (source)
Logger logger = LoggerFactory.getLogger(AbfrageErstellen.class); private Abfrage create(Schluessel key) { logger.info("erstelle Abfrage für {}", key.getId()); Abfrage result = new Abfrage(String.format("SELECT * FROM Adressen WHERE id = %1$s", key.getId())); logger.info("Abfrage ist: {}", result.getQuery()); return result; }
Der eigentliche Code ist kaum noch zu erkennen. Loggt man nun auch noch “Selektierte Adresse bestimmen” und “Abfrage ausführen” jeweils beim Ein- und Ausgang, so hat man etliche Infos auch noch doppelt geloggt.
Logging mit AOP
Logging ist ein klassischer Cross Cutting Concern, für den AOP erfunden wurde. Logging ist daher auch ein gerne genutztes Beispiel für AOP. Ich persönlich konnte mich mit AOP nie so richtig anfreunden, deswegen kenne ich es nicht allzu sehr, aber ein typisches Beispiel zum Thema Logging ist dieses:
public aspect AutoLog{ pointcut publicMethods() : execution(public * de.jebc..*(..)); pointcut logObjectCalls() : execution(* Logger.*(..)); pointcut loggableCalls() : publicMethods() && ! logObjectCalls(); before() : loggableCalls(){ Logger.entry(thisJoinPoint.getSignature().toString()); } after() : loggableCalls(){ Logger.exit(thisJoinPoint.getSignature().toString()); } }
Damit wird für jede öffentliche Methode (außer beim Logger selber) der Beginn und das Ende der Methode protokolliert mitsamt der Signatur. Leider werden nicht die Parameter ausgegeben, nur ihre Typen. Und selbst wenn, könnte man, weil es ein genereller Ansatz ist, nur toString() bei den Parametern aufrufen, womit man dann zwar Code aus den Geschäftsklassen herausgeholt hat, aber potentiell Analysecode in die toString()-Methode der Datenklassen verschiebt.
Logging mit EBC
Wie sieht’s denn dann mit EBCs aus? Nun, hier ist die Logging-Funktionalität in einfachen Bauteilen versteckt, die einfach in die Drähte zwischen den Pins eingehängt werden:

Log-Bauteile werden in den Draht gehängt und "belauschen" das Datum
Damit kann Logging ganz explizit an gewünschten Stellen eingesetzt werden, ohne den Code der eigentlichen Geschäftsklasse zu beeinträchtigen. Auch das übetragene Datum wird nicht beeinträchtigt. Trotzdem hat die Loggingklasse Zugriff auf die komplette Schnittstelle des Datums und kann so durchaus komplexe Informationen loggen. Eine Loggingklasse sähe dann z.B. so aus:
public class LogSchluessel { private final Logger log; private OutPin<Schluessel> out = new OutPinImpl<Schluessel>(); private InPin<Schluessel> in = new InPin<Schluessel>() { @Override public void receive(Schluessel message) { log.debug("erstelle Abfrage für {}", message.getId()); out.send(message); } }; public LogSchluessel(Logger log) { this.log = log; } public InPin In() { return in; } public OutPin Out() { return out; } }
Die Klasse zum Loggen der erstellten Abfrage sähe entsprechend aus. In AdresseErstellen werden jetzt die Logging-Klassen wie andere Bauteile verdrahtet. (source)
public AdresseEinlesen() { logSchluessel.Out().wire(abfrageErstellen.Start()); abfrageErstellen.Result().wire(logAbfrage.In()); logAbfrage.Out().wire(abfrageAusfuehren.Start()); abfrageAusfuehren.Result().wire(adresseErstellen.Start()); } public InPin<Schluessel> Start() { return logSchluessel.In(); } public OutPin<Adresse> Result() { return adresseErstellen.Result(); }
Hier bietet sich etwas Refaktorierung an, um die ewige Wiederholung der Pins zu vermeiden. Im Prinzip beobachten die Log-Klassen den Datenstrom, ohne ihn zu verändern. Dieses Muster kann sicher auch anders einen Zweck erfüllen. Deswegen möchte ich es ganz allgemein Watcher nennen. Alle Logging-unabhängigen Teile können dort rein.
public abstract class Watcher<T> { private OutPin<T> out = new OutPinImpl<T>(); private InPin<T> in = new InPin<T>() { @Override public void receive(T message) { inspect(message); out.send(message); } }; public InPin<T> In() { return in; } public OutPin<T> Out() { return out; } protected abstract void inspect(T message); }
Die Logging-Klassen bleiben dann unaufwändige Restklassen. Verschoben in ein eigenes Package, ist das Logging fast vollständig aus dem Weg und verschmutzt die eigentliche Geschäftslogik nicht mehr. Hier kann man jetzt auch sehr gut sehen, warum die Pin-Schnittstelle auf einen Parameter beschränkt bleiben sollte: Generische Klassen wie der Watcher wären sonst nicht möglich. (source)
public class LogSchluessel extends Watcher<Schluessel> { private final Logger log; public LogSchluessel(Logger log) { this.log = log; } protected void inspect(Schluessel message) { log.info("erstelle Abfrage für {}", message.getId()); } }
Im Diskussionsforum zu den EBC kam die Anmerkung, dass es ja nichts nützen würde, nur die Parameter (sprich: Daten) zu loggen. Schließlich sei es ja nötig, auch innerhalb der FE zu loggen. Ich denke aber, dass es nicht nötig ist. FEs sollten kompakte, durchgetestete Einheiten sein, die mit allen gültigen Daten umgehen können. Insofern kann das Problem nur darin liegen, dass das übergebene Datum nicht valide ist und das kann ja grade am Pin geloggt werden. Ausnahmen mögen die Regel bestätigen, aber dann bleibt immer noch – wie im Logging-Bauteil selber – die Constructor Injection. Ich denke aber, dass das die Ausnahme bleiben wird.
Ah, ich höre es schon: wie kann man denn z.B. Exceptions loggen? Oder sie überhaupt sinnvoll behandeln? An einem Pin geht das ja eigentlich nicht? Wir werden sehen…
Quelltexte
Der aktuelle Commit ist 68034f. Die kleinen (source) Links innerhalb des Posts verweisen auf den jeweils im dazugehörigen Beispiel gezeigten Stand. Durch das Refactoring und schrittweise Entwickeln findet man nicht immer alle Codebeispiele im letzten zu diesen Post gehörenden Commit.