1. August 2004

Komponentenentwicklung mit Logging und Konfiguration

Jede ernstzunehmende Programmierung braucht zwei Basisdienste: Logging und Konfiguration. Über den Code verstreut findet man Loggingausgaben und Konfigurationsstatements. Meistens wird dabei auf globale Objekte zugegriffen. Das heisst, es gibt in jeder Methode eines Moduls anwendungsspezifischen Code. Ein Albtraum bei der Wiederverwendung in anderen Projekten. Das ist das Logging- und Konfigurationsproblem. Für das Problem gibt es verschiedene Lösungskonzepte.

Jede ernstzunehmende Programmierung braucht zwei Basisdienste: Logging und Konfiguration. Logging sollte in jeder Methode auf jeder Ebene Zustandsinformationen ausgeben. Das ist besonders im Fehlerfall wichtig als Diagnosemittel. Daneben sind aber auch Zustandsinformationen zur Laufzeit wichtig für die Optimierung eines Systems. Flexible Anwendungen lassen sich in vielen Parametern konfigurieren. Dafür verwendet man typischerweise eine Konfigurationsschnittstelle, die im gesamten Programm zur Verfügung steht, ganz ähnlich der Loggingschnittstelle. Über das Modul verstreut findet man Loggingausgaben und Konfigurationsstatements oft als Einzeiler. Das heisst, anwendungsspezifischer Code ist potentiell in jeder Methode eines Moduls vorhanden.

Nur begibt es sich aber, dass oft Module geschrieben werden, die in verschiedenen Anwendungen Einsatz finden. Da passiert es häufig, dass die Logging- und Konfigurations-Schnittstellen (LC=Log/Config) des Moduls nicht zum Programm passen. Wird ein Modul in einer Anwendung entwickelt und dann in einer anderen Anwendung verwendet, kollidieren oft 2 verschiedene LC Schnittstellen.

Eigentlich sollte eine Komponente frei sein von anwendugsspezifischem Code. Exceptions versprechen da Abhilfe, weil diese in einem Modul bis zur Anwendung weitergeworfen werden können. Das hilft aber nicht wirklich, weil ein gutes Modul Fehler lokal behandeln und evtl. kompensieren muss. Ausserdem fehlt der Fehlerstack, wenn eine Exception von ganz unten erst ganz oben gefangen wird. Eine vergleichbare Scheinlösung gibt es auch für die Konfiguration. Module können bei der Initialisierung konfiguriert werden, dann entfällt die Abfrage der aktuellen Konfiguration innerhalb des Moduls. Das ist nicht gut, weil das Module beim Setup alle Parameter speichern muss, was redundant ist, da sie ja schon global gespeichert sind. Laufzeitkonfiguration ist dann auch nicht möglich.

Ergebnis in der Praxis: Module können oft nicht wiederverwendet werden. Natürlich werden in der Praxis Lösungen gefunden, sonst gäbe es keine wiederverwendeten Module. Mögliche Lösungen:

1. Kein Logging, keine Laufzeitkonfiguration: die häufigste Lösung, aber nicht akzeptabel.
2. Klassen mit Loggingsenke/Konfigurationsquelle als Konstruktionsparameter: bedeutet, jede Klasse bekommt und verwaltet Referenzen der globalen Log/Konfig-Objekte. das ist sehr unpraktisch.
3. Plattformdienste, wie syslog (Un*x), Enterprise Instrumentation Framework (Win), Registry (Win): nicht plattform-portabel.
4. Applikationsglobale Objekte, die über Einzeiler benutzt werden: Module funktionieren nicht ohne ihre Applikation und sind nicht einfach wiederverwendbar, da jede Methode angepasst werden muss.
5. Hooks: Einzeiler mit Adaptern

Lösung 5 ist das geringste Übel. Sie ermöglicht Einzeilerlogging/-konfiguration, verzichtet aber auf globale Objekte. Module werden mit modulspezifischen LC-Statements geschrieben und debuggt. Diese Statements werden dann über Adapter an die Applikation angepasst. Die Statements dürfen nicht direkt auf die jeweiligen Funktion gehen, sondern indirekt über einen Adapter, der angepasst werden kann. Erst der Adapter harmonisiert die LC Schnitstelle des Moduls mit der Applikation. Minimale Adapter werden mit den Modulen ausgeliefert, um die problemlose Wiederverwendung zu gewährleisten. Der einzige Nachteil dieser Methode ist, dass man sich auf einen Funktionsumfang einigen muss. Beim Logging muss festgelegt werden, dass ob und dass es Loggingklassen und/oder Kanäle gibt. Bei der Konfiguration muss man sich auf die Struktur der Konfigurationsdaten einigen.

Typische Loggingkonzepte sehen eine Loggingklasse, einen Kontext und eine Meldung vor. Möglicherweise auch noch einen Kanalnamen. Mit diesen Informationen kann man fast alle applikationsspezifischen Loggingsenken zufriedenstellen. Ein Logginstatement sieht dann so aus:
MyLog(ERROR, "Klasse oder Kanal", "Methode oder Context", "Meldung");

Typische Konfigurationsdaten sind hierarchisch aufgebaut. Mit bis zu 3 Ebenen sieht ein Konfigurationsstatement so aus:
value = MyConfig("Level1", "Level2", "Item", DEFAULTWERT);
Oder mit beliebig vielen Ebenen:
value = MyConfig("Level1" "/" "Level2" "/""Item", DEFAULTWERT);

Der minimale Logadapter wird die Meldung verwerfen während der minimale Konfigadapter den Defaultparameter der Bibiothek zurückgibt. Die Adapter werden als Funktionen implementiert, die evt. auch von aussen gesetzt werden können (=Hooks). Adapter müssen zwar implementiert werden. Sie sind aber meistens sehr schlicht und das ist allemal besser, als auf Logging zu verzichten oder jede Methode anzufassen.

Empfehlung: Die Logginklasse sollte Teil des Projektcodes sein. Dann kann man Module inklusive einem funktionierenden Logging weitergeben oder weiterverwenden. Ich verwende nur 2 Dateien, die ich von Projekt zu Projekt kopiere: MyLog.cpp und MyLog.h. Alle Loggingklassen und -instanzen sind mit einem Kuerzel geprefixt. Dann braucht man nur in 2 Dateien das Prefix ersetzen.

_happy coding_