Dass dieser Lösungsweg für Embedded-Systeme mit knappen Ressourcen suboptimal ist, liegt auf der Hand, denn die Lösung enthält ungenutzten Code, sie belegt also mehr Codespeicher als nötig, weil sie sämtliche konfigurierbare Varianten (blockieren/überschreiben sowie mit/ohne Synchronisation) enthält. Zudem wird für die Fallunterscheidungen zur Laufzeit Rechenzeit belegt und die beiden Member-Variablen »mMutex« sowie »mBlockIfFull« belegen Datenspeicher.
Außerdem birgt dieser Ansatz den Nachteil, dass die Softwarekomponente sämtliche Varianten umsetzen muss, so dass ein schwer zu wartender Software-Monolith entsteht, der mit jeder Konfigurationsoption komplexer wird.
Eine architektonisch saubere Problemlösung zeigt das UML-Klassendiagramm in Bild 2. Dort sind die konfigurierbaren Teile aus der Komponente »Queue« ausgelagert. Die Klasse »Queue« verwendet nun jeweils eine Instanz vom Typ »ISyncStrategy« und »IMemStrategy«, mit denen sie die parallele Ausführung kritischer Codeteile verhindert (Methoden »lock« und »unlock«) oder blockiert oder Platz schafft, wenn die Queue voll ist (Methode »requestSpace«).
Die unterschiedlichen Varianten der beiden Interfaces implementieren die »Strategy«-Klassen. Das gewünschte Verhalten der Klasse Queue wird definiert, indem bei der Instanziierung die richtigen Strategy-Instanzen per Referenz an den Konstruktor übergeben werden: Eine blockierende Queue ohne Synchronisation erhält man, indem man die Queue-Instanz mit »BlockingQueueMemStrategy« und »NoSyncQueueSyncStrategy« kombiniert usw. Der Code der Methode »write« sieht dann aus wie in Listing 2 gezeigt.
Man erkennt, dass in der Klasse Queue zwar die Hook-Methodenaufrufe (mit * gekennzeichnet) für die Synchronisation und das Platzschaffen vorhanden sein müssen, jedoch ist das jeweilige Verhalten der gerufenen Methoden ausgelagert. Die Queue ist nun kein Monolith mehr, ihr Verhalten kann durch Konfiguration mit der jeweiligen Strategie beeinflusst werden. Leider hat die so geschaffene Modularität ihren Preis: Da der Compiler zum Zeitpunkt der Übersetzung noch nicht wissen kann, mit welchen Strategy-Instanzen die Queue zur Laufzeit arbeiten soll, kann er praktisch keine Optimierungen durchführen.
Wird z. B. die »NoSyncQueueSyncStrategy« verwendet, werden zur Laufzeit tatsächlich deren leere Methoden lock und unlock aufgerufen, was unnötig Codespeicher und Rechenzeit kostet.
Das Konzept der Codegenerierung
Beide bisher gezeigten Lösungen sind so flexibel, dass das Verhalten der Queue, also mit welchen Strategien die Warteschlange arbeiten soll, erst zur Laufzeit festgelegt werden braucht. Häufig ist diese Flexibilität indes gar nicht notwendig, weil bereits zur Compilierungszeit bekannt ist, wie die Queue betrieben werden soll. Programmierer wären also in der Lage, den für die gewünschte Lösung jeweils optimalen Code hinzuschreiben. Für eine blockierende Warteschlange ohne Synchronisierung sollte der Code der Methode write wie in Listing 3 aussehen, für eine nicht blockierende Warteschlange mit Synchronisierung ist der Code in Listing 4 abgebildet.
Da die gemeinsamen Teile des Codes jedoch nicht dupliziert werden sollen, erfordert dies eine Möglichkeit, den jeweiligen Code vor oder während der Compilierung abhängig vom gewünschten Verhalten erzeugen zu lassen.
Eine Möglichkeit der Codegenerierung wäre die Verwendung des C-Präprozessors: Mit bedingter Compilierung lässt sich tatsächlich das gewünschte Resultat erzielen. Allerdings machen die dafür erforderlichen Präprozessoranweisungen den Quellcode schwierig lesbar, ein Wartung des Quellcodes scheidet praktisch aus. Letztlich entsteht dadurch eine monolithische Lösung wie beim Ansatz der Fallunterscheidungen zur Laufzeit (siehe Listing 1). Was benötigt wird, ist also ein Codegenerator, der aus konfigurierbaren Einzelteilen das jeweils gewünschte Endergebnis erzeugt (Bild 3).