Individualisierung von Standardsoftware – Gib jedem, was er will
Um sich Wettbewerbsvorteile zu verschaffen, werden von Kunden individuelle Lösungen benötigt und gefordert. Gleichzeitig soll eine solche Lösung kostengünstig umgesetzt und fortlaufend weiterentwickelt werden. Handelt es sich dabei wirklich um ein unlösbares Problem?
Entsprechend der gegebenen Rahmenbedingungen sind verschiedene Wege denkbar, um diesen Spagat zu schaffen. Sie reichen von einem erweiterten Branch-Konzept mit Projekt-Templates über eine Modularisierung mittels Paketverwaltung und Dependency-Injection bis hin zu skriptbasierten Laufzeiterweiterungen. In dem Artikel werden die genannten Lösungsansätze beschrieben und es wird erläutert, wann welcher Ansatz sinnvoll ist.
Agilität ist der Schlüssel zum Erfolg. Zumindest scheint es so, wenn man sich die Zahlen des Standish Group Reports von 2015 ansieht [1]. Die Krux an der Sache ist, dass die Offenheit gegenüber Veränderung ein zentraler Wert der agilen Softwareentwicklung ist. Dies wiederum stellt direkt die Anforderung einer leichten Änderbarkeit und Erweiterbarkeit an die Software selber.
Die Anforderung, eine Software in Abhängigkeit von Kundenwünschen ändern zu können, ist dabei gewiss nicht neu. Es ist kein Geheimnis, dass ein Produkt nur dann am Markt bestehen kann, wenn es die Anforderungen der Kunden erfüllt. Im Consumer-Markt ist dies noch mit einem Standardprodukt machbar. Im industriellen Umfeld hingegen sind Anforderungen meist sehr spezieller Natur, wobei jedoch in vielen Fällen gleichartige Muster und Abläufe zugrunde liegen. So rückt eine zentrale Frage in den Mittelpunkt: Wie kann ich eine leicht anpassbare Software mit einem möglichst hohen Grad an Standardisierung realisieren?
Die Lösung für die Herausforderung besteht aus mehreren Bestandteilen. Die grundlegende Softwarearchitektur ist dabei genauso wichtig wie der Prozess zur Verwaltung von Abhängigkeiten. Denn eines haben alle Lösungsansätze gemein: Der Schlüssel zum Erfolg ist die richtige Modularisierung der Anwendung. Doch bevor mit der Bearbeitung der einzelnen Punkte begonnen werden kann, gilt es die Frage zu klären, welche Bestandteile der Software einem hohen Änderungsgrad unterliegen, und welche Bestandteile über alle Kundenprojekte bzw. über den Lebenszyklus der Software hinweg konstant sind. Bei der Beantwortung kann zwischen zwei grundlegenden Szenarien unterschieden werden:
Variante 1 – Die Hauptanwendung ist der Standard und die Komponenten variieren
Bei dieser Variante existiert eine Basisapplikation, welche für alle Nutzer identisch ist. Diese Basisapplikation kann nun mithilfe von ebenfalls standardisierten oder individuellen Erweiterungen auf den Kunden angepasst werden. Dieses Muster ist im Consumer-Markt bekannt und zum Beispiel bei Webbrowsern wie Google Chrome oder Mozilla Firefox wiederzufinden.
Variante 2 – Standardisierte Komponenten werden in individuellen Anwendungen aggregiert
Es existiert eine bestimmte Menge an Standardfunktionen, die in Abhängigkeit der Kundenanforderungen individuell miteinander kombiniert werden. Im Gegensatz zur Variante 1 kann kein allgemeingültiger Funktionsablauf hinterlegt werden. Die aufrufende Anwendung hat einen sehr hohen Grad an Individualisierung.
Doch schauen wir uns zunächst die erste Variante etwas detaillierter an. Diese ist oft anwendbar, wenn durch die Anwendung wiederkehrende Abläufe abgebildet werden. Ein Beispiel hierfür wäre die Steuerungslogik für eine Produktionsanlage für Holzelemente. Der Basisablauf besteht aus dem Einlesen der Fertigungsdaten, dem Sägen der Platten und der Ausgabe des bearbeiteten Elements.
Bei einer einfachen sequentiellen Implementierung dieses Vorganges muss die gesamte Anwendung angepasst werden, sobald sich ein Kunde einen weiteren Bearbeitungsschritt wünscht. Durch die passende Modularisierung hingegen kann eine Anpassung in der bestehenden Software komplett verhindert werden, es sind lediglich neue Module hinzuzufügen. Der schematische Aufbau und Ablauf ist in Abb.5 abgebildet.
Dabei wird sofort ersichtlich, dass ein solches Konzept einen hohen initialen Aufwand erzeugt. Es sind die Basisanwendung und die einzelnen Komponenten zu implementieren. Hinzu kommt die Logik zum Laden und Konfigurieren der einzelnen Komponenten. Je nach eingesetzter Technologie kann dies sehr kompliziert werden. So bietet das .NET-Framework von Microsoft zum Beispiel fertige Funktionalitäten um einzelne Komponenten dynamisch zur Laufzeit der Anwendung laden zu können, wohingegen dies in anderen Programmiersprachen selbst implementiert werden muss oder eventuell gar nicht unterstützt wird.
Doch das Laden von zusätzlichen Modulen ist nur eine Möglichkeit zur Individualisierung der Anwendung. Basiskomponenten können durch sogenannte Erweiterungspunkte für zusätzliche Funktionsaufrufe vorbereitet werden. So kann zum Beispiel jede Komponente neben ihrer eigentlichen Ausführungslogik zwei weitere Methoden BeforeExecute und AfterExecute besitzen, welche kundenspezifische Befehle ausführen. Für die Implementierung dieser Befehle sind verschiedenste Wege denkbar. Im .NET-Bereich bieten sich folgende Möglichkeiten an:
- Implementierung einer eigenen Komponente welche von der Basiskomponente ableitet. Dadurch können die zwei Methoden überschrieben werden um sie mit eigener Logik zu füllen.
- In den Erweiterungsmethoden wird ein Script, welches in einer Konfigurationsdatei definiert wird, ausgeführt.
- In einer Konfigurationsdatei wird direkt Script-Code hinterlegt, welcher in den Erweiterungsmethoden ausgeführt wird.
- Laden von weiteren Komponenten via Dependency-Injection und Inversion of Control Container.
public interface ICustomizable
{
void Execute(object input);
}
public class CustomaizableComponent : ICustomizable
{
public void Execute(object input)
{
BeforeInternalExecute(input);
InternalExecute(input);
AfterInternalExecute(input);
}
protected void BeforeInternalExecute(object input)
{
// Run some script code from config file
// Read path to script file from config and execute
// Load other ICustomizable and implementations and call Execute
// Override in derived class
}
protected void AfterInternalExecute(object input)
{
// Run some script code from config file
// Read path to script file from config and execute
// Load other ICustomizable and implementations and call Execute
// Override in derived class
}
private void InternalExecute(object input)
{
// put your logic here
}
}
Existieren keine einheitlichen Abläufe in der Anwendung heißt es jedoch nicht, dass diese Struktur nicht auch anwendbar wäre. Die Erweiterungspunkte müssen nur an anderen Stellen vorgesehen werden. Handelt es sich bei der Anwendung zum Beispiel um eine Desktopanwendung können einzelne Oberflächendialoge durch Erweiterungspunkte individualisiert werden. Das Einblenden von zusätzlichen Icons in der Toolbar mit neuen Funktionen ist dabei nur eine von vielen Möglichkeiten.
Doch nun wird es Zeit, die Variante 2 etwas genauer unter die Lupe zu nehmen. Als Ausgangspunkt existieren mehrere standardisierte Komponenten. Die Menge der in einem Kundenprojekt verwendeten Komponenten variiert. Hinzu kommt, dass zur Verknüpfung der Komponenten jedes Mal kundenspezifische Logik zu entwickeln ist.
Ein konkretes Beispiel kommt hier aus dem Bereich der Ansteuerung von Elektromotoren. Die zu entwickelnde Embedded Software wird aus verschiedenen Standardkomponenten wie Stromregler, Drehzahlregler und Temperaturüberwachung zusammengesetzt. Die Logik zur Verknüpfung als auch die Plattform (der Prozessor) unterscheiden sich von Projekt zu Projekt. Dies bringt nun mehrere Herausforderungen mit sich.
- Die Anzahl an verschiedenen Plattformen ist begrenzt (weniger als zehn). Es soll nicht jedes Mal bei null begonnen werden. Eine gemeinsame Basis für Projekte der gleichen Plattform wäre wünschenswert.
- Für das Testen der einzelnen Komponenten sind Unit-Tests nicht ausreichend. Es müssen Systemtests durchgeführt werden, um die Funktionsfähigkeit auf den jeweiligen Plattformen sicherzustellen.
- Da jede Plattform über eigene Compiler verfügt, können die Komponenten nicht als Binaries bereitgestellt werden. Es ist nötig, den Quellcode der Komponenten einzubinden, um ihn plattformspezifisch zu kompilieren.
Um die ersten beiden Herausforderungen zu bewältigen, wird für jede Plattform ein sogenanntes Template-Projekt erstellt. Dabei handelt es sich um ein fiktives Kundenprojekt mit der minimalen Logik, die nötig ist, um die Komponenten einem Systemtest zu unterziehen. Dafür bindet jedes Template-Projekt alle Komponenten ein. Diese Templates haben jedoch einen weiteren Nutzen: Jedes neue Kundenprojekt wird auf Basis der Templates der jeweiligen Plattform erstellt. Dafür sind zwei Wege möglich. Wenn die Entwicklungsumgebung dies unterstützt, können Templates als Zip-Dateien oder in anderen Formaten zentral bereitgestellt und auf den Entwicklungsrechnern importiert werden. Ist dies nicht möglich oder sollen Änderungen aus Kundenprojekten eventuell in die Templates zurückfließen, werden Branches für neue Projekte in der Versionsverwaltung erstellt. Innerhalb der Kundenprojekte kann nun mit der Individualisierung begonnen werden, indem Referenzen auf nicht benötigte Komponenten entfernt und zusätzliche Logik implementiert wird.
Die Einbindung der einzelnen Komponenten als Quellcode ist damit jedoch noch nicht gelöst. Um dies realisieren zu können bedarf es des Einsatzes eines Paket-Managers zur Verwaltung von Abhängigkeiten. Zum Verteilen von Binaries gehören Paket-Manager längst zum Standard. Durch deren Einsatz lassen sich Abhängigkeiten zwischen einzelnen Komponenten abbilden. Auch die parallele Existenz einer Komponente in mehreren Versionen ist problemlos möglich, da in jeder referenzierenden Anwendung explizit die referenzierte Version für jede Komponente angegeben wird. Für das Referenzieren von Quellcode hingegen sind kundenspezifische Implementierungen nötig. So können in ein Paket zwar Quellcode-Dateien verpackt werden, es ist jedoch zusätzliche Logik nötig, um die Pakete beim Konsumieren auch wieder richtig zu entpacken. Mit dem AIT Dependency Manager existiert ein kostenloses Werkzeug welches in Verbindung mit dem Microsoft Team Foundation Server und Visual Studio eingesetzt werden kann, um derartige Quellcode-Referenzen zu realisieren. Der Paketmanager-Paket bietet die Möglichkeit, Quellcode direkt aus Git-Repositories zu referenzieren.
Bei diesem Vorgehen besteht jedoch eine grundlegende Problematik, welche immer wieder diskutiert werden muss. So wird sich bei vielen kundenspezifischen Anforderungen die Frage stellen, ob dies nicht eine Funktionalität für eine allgemeine Komponente ist. Handelt es sich um eine noch nicht existierende Komponente, ist es kein Problem, eine neue anzulegen. Schwierig wird es jedoch, wenn es sich um Änderungswünsche an einer bestehenden Komponente handelt. In diesen Fällen sind die Auswirkungen der Änderung sehr genau zu analysieren, da sie sich potentiell auf alle Konsumenten der Komponente auswirken.
Genau dieser Punkt zeigt, dass eine klare Trennung zwischen beiden Varianten nur sehr selten möglich ist, da im Vorfeld meist niemand ausschließen möchte, dass nicht hier oder da doch mal eine Änderung gemacht werden soll. In solchen Fällen bietet sich eine Kombination aus beiden Varianten an. Die Hauptanwendung wird als Template bereitgestellt. Komponenten werden über einen Paketmanager referenziert um die Hauptanwendung mit Funktionalität zu füllen. Die Komponenten wiederum bieten Erweiterungspunkte oder Parameter zur Konfiguration um kundenspezifische Anpassungen zu ermöglichen.
Fazit
Um sich Wettbewerbsvorteile zu verschaffen und am Markt bestehen zu können, muss in der Produktentwicklung auf die Bedürfnisse des Kunden eingegangen werden. Somit ist es nötig, individualisierte Softwarelösungen zu liefern. Gleichzeitig soll eine solche Lösung kostengünstig umgesetzt und fortlaufend weiterentwickelt werden. Dass dies mit dem passenden Architektur- und Verteilungskonzept umgesetzt werden kann wurde in diesem Artikel gezeigt. Der initiale Aufwand darf dabei auf keinen Fall unterschätzt werden. Die Vorteile überwiegen jedoch.
Also los geht’s – gib jedem was er will!