Externe Abhängigkeiten beim Software Design vermeiden

Gutes Software-Design achtet darauf, die Komplexität auf das absolut notwendige Minimum zu beschränken.

Warum? Mit der Komplexität steigen Entwicklungs- und Wartungsaufwand und die Fehlerwahrscheinlichkeit. Das wiederum wirkt sich direkt auf die Sicherheit, die Kosten und die Entwicklungsgeschwindigkeit aus.

Komplexität durch externe Abhängigkeiten

Während den meisten Software-Entwicklern sofort einleuchtet, dass einfache Algorithmen die bessere Wahl sind solange sie die an sie gestellten Anforderungen erfüllen, vergessen viele, dass externe Abhängigkeiten die Komplexität einer Lösung stark beeinflussen.

Im ersten Moment erscheint es schnell und bequem, externe Komponenten passend für jedes Problem einzubinden. Eine kurze Internetsuche und schon liefert mir Stack Overflow jede Menge Vorschläge, welche Pakete ich einbauen könnte und wie ich sie verwende.

Dabei sind die Konsequenzen oft nicht bedacht.

So wird die eigene Software abhängig von der Qualität und Wartung der externen Komponente. Gegenüber unserer Kunden, fühlen wir uns als die Verantwortlichen für unsere Gesamtlösung. Dementsprechend stehen auch wir als Software-Hersteller in der Pflicht verantwortungsvoll mit externen Abhängigkeiten umzugehen.

Streng genommen gehört dazu zumindest in sicherheitskritischen Umgebungen auch ein Code-Review der externen Komponente. Wie anders ließe sich beurteilen, welchem Risiko unsere Kunden ausgeliefert sind. Das heißt im Klartext, dass externe Abhängigkeiten auf Closed Source Komponenten ein für Software-Designer unkalkulierbares Risiko darstellen. Kontrolle ist in diesem Fall unmöglich, was entweder den Einsatz verbietet, eine vertragliche Haftung des Herstellers notwendig macht oder blindes Vertrauen in den Hersteller erfordert.

Außerdem müssen sich Software-Designer im Klaren sein, dass externe Abhängigkeiten meist transitive Abhängigkeiten mit sich bringen. D.h. sie haben ihrerseits Abhängigkeiten auf weitere externe Komponenten. Auch für diese machen sich Software-Hersteller mitverantwortlich, wenn sie sie an ihre Kunden ausliefern.

Zu professionellem Abhängigkeitsmanagement gehört auch, alle Lizenzbedingungen zu externen Komponenten zu kennen und für deren Einhaltung zu sorgen. Zum Beispiel muss man wissen, dass bei einer kommerziellen Komponente Entwicklerlizenzen nicht für den produktiven Einsatz verwendet werden dürfen.

Kleine Dinge selber machen

Besonders für sehr kleine Funktionalitäten ist Selber-Machen oft eine sinnvolle Alternative. Damit bleibt die Wartung in der eigenen Organisation und damit besser unter Kontrolle. Außerdem steht so auf jeden Fall der Quellcode zur Verfügung.

Bewusste Entscheidung bei Abhängigkeiten treffen

Grundsätzlich gilt es unnötige Abhängigkeiten zu vermeiden, ganz gleich ob es sich um ganze Frameworks oder einzelne Bibliotheken handelt.

Am Anfang muss immer die Frage stehen: zahlt sich die Abhängigkeit gegenüber ihrem Nutzen aus?

Beispiel aus dem Alltag

Beim Code-Review eines Webservices fiel mir in einer der Klassen folgender Code auf:

import org.apache.commons.lang3.StringUtils;
// ...

if (!StringUtils.isBlank(authorizerName) && !StringUtils.isBlank(roleName)) {
   // ...
}

Und an anderer dieser:

import org.apache.commons.lang3.StringUtils;

// ...
logger.info("Data objects from services " + StringUtils.join(serviceNames, " ") + " have been requested");

Es gab im gesamten Code vier Stellen an denen die StringUtils zum Einsatz kamen. Zwei davon waren eine Code-Duplikation, die sich durch ein simples „Extract Method“ Refactoring beheben lässt.

Wegen dieser drei Stellen hat der Code das Packet org.apache.commons/commons-lang3 als Abhängigkeit angezogen, dabei lässt sich das gleiche auch mit der Java-Standardbibliothek erreichen.

if ((authorizerName != null &&  authorizerName.matches("^\s*$)) &&
    (roleName != null &&  roleName.matches("^\s*$)))
{
    // ...
}

Das machtes("^\s+$") ist hier verwendet, weil isBlank(...) auch true liefert, wenn die Zeile nur aus Whitespaces besteht.

Natürlich würde man auch in diesem Fall eine gemeinsame Methode extrahieren. Performanzunterschiede spielen hier keine Rolle, da der Code nicht in einer Schleife steht und nur selten verwendet wird.

Vermutlich hätte dem Autor im Beispiel oben sogar folgendes gereicht, denn ein fälschlich angegebener Name für Authorizer und Rolle hat nicht unbedingt Nachsicht verdient.

if ((authorizerName != null && authorizerName.isEmpty()) &&
    (roleName != null && roleName.isEmpty("")))
{
  // ...
}

Den Join hätte man mit der Java-Standardbibliothek sogar noch ein klein wenig eleganter schreiben können.

logger.info("Data objects from services {} have been requested", String.join(" ", serviceNames);

Platzhalter in Loggern konsequent zu verwenden hat zwar nichts mit dem Thema des vorliegenden Artikels zu tun, sollte man sich aber dennoch grundsätzlich angewöhnen, da es zum Beispiel Mehrsprachigkeit erleichtert.

Der eigentliche Punkt ist, dass Java bereits Methoden enthält, die Arrays und Collections zu Strings zusammenfügen können.

Die Abhängigkeit auf die StringUtils lässt sich also in diesem Fall (wie in den meisten) leicht vermeiden.

Transitive Abhängigkeiten nicht vergessen

Zu ordentlichem Abhänigkeitsmanagement gehört auch, dass man sich über transitive Abhängigkeiten im Klaren ist.

Bei dem Webservice unter Code Review sahen die insgesamt 22 direkten Abhängigkeiten so aus:

Screenshot direkter Abhängigkeiten
Screenshot direkter Abhängigkeiten

Wem das viel erscheint, sollte sich vor dem ersten Druck auf die Schaltfläche, die alle Abhängigkeiten anzeigt besser hinsetzen. Es sind hunderte, daher zeigt der Screenshot unten nur einen kleinen Ausschnitt. Dabei gibt es Doppelungen wie im Beispiel unten zu sehen. Um die Mehrfachnennungen bereinigt bleiben 106 Abhängigkeiten.

Transitive Abhängigkeiten
Transitive Abhängigkeiten

Das Beispiel zeigt auch deutlich ein weiteres Dilemma, dass man sich unweigerlich mit derart komplexen Abhängigkeiten einkauft:

Da viele transitive Abhängigkeiten sich überschneiden kommt es zu Versionskonflikten. Ein Paket ist mit einer Abhängigkeit in einer bestimmten Version getestet, andere Pakete verlangen aber die gleiche Abhängigkeit in einer anderen Version. Das Paketverwaltungswerkzeug löst das im vorliegenden Fall dadurch, dass es die neueste Version nimmt und auf Rückwärtskompatibilität hofft.

Das Beispiel zeigt auch, dass das commons-lang3 Paket auch dann in das Projekt Einzug hält, wenn ich die direkte Abhängigkeit wie oben beschrieben beseitige. Ist damit der Aufwand, den ich treibe umsonst gewesen?

Nein. Denn wenn bei Code-Änderungen die direkte Abhängigkeiten entfallen, die das Paket referenzieren, dann wird es nicht mehr benötigt. Unsere direkten Abhängigkeiten reduzieren wir als verantwortliche Software-Designer schließlich auf ein Mindestmaß.

Fazit

Bei nicht-trivialen Software-Projekten handelt man sich mit externen Abhängigkeiten Komplexität und einen Zoo an transitiven Abhängigkeiten ein. Gegenüber unseren Kunden sind wir für das Gesamtergebnis verantwortlich – das schließt alle Abhängigkeiten ein. Es lohnt sich daher, die Abhängigkeiten auf das absolut notwendige Mindestmaß zu beschränken.

Kommentar schreiben

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.