Zurück

Mythos Wiederverwendbarkeit – eine andere Perspektive

von Jan Müller

Oft begegnet mir in meinen Kundenprojekten die mehr oder weniger unreflektierte Haltung, es sei generell gut und günstig, einmal geschriebene Software auch an anderen Stellen wiederzuverwenden, weil es zeitaufwändig und teuer ist, “das Rad neu zu erfinden”, d. h. durch Wiederverwendung entstehen Synergien. Außerdem ist Duplikation als große Fehlerquelle und Qualitätsmangel auf jeden Fall zu vermeiden. Zusammengefasst: Wiederverwendung ist gut, Duplikation ist schlecht. 

Kurz gesagt: Das stimmt prinzipiell auch! Nur nicht so uneingeschränkt wie häufig postuliert. Hier soll es vor allem um die Grenzen dieses Prinzips gehen. 

Zur Veranschaulichung und Einführung in das Thema Wiederverwendung schildere ich ein einfaches, nachvollziehbares Beispiel: 

Ich stelle Frisbee-Scheiben her und habe mir dafür eine Produktionsanlage bauen lassen. Jetzt möchte ich mein Geschäft erweitern, weitere Produkte herstellen und dafür meine bereits vorhandene Produktionsanlage verwenden, um möglichst kostensparend Neues zu produzieren. Die Idee: Teller! Die sind ja auch flach und rund – das lässt sich sicher in einem Produkt vereinen. Leider ist das falsch – gute Flugeigenschaften und Leichtigkeit auf der einen und Festigkeit und Kratzresistenz auf der anderen Seite sind nahezu unmöglich zusammenzubringen. Die Erweiterung der Produktionsanlage und das notwendige Engineering würden sehr aufwändig und teuer werden. Und das, ohne dass ich mir davon ein gutes Produkt erhoffen darf – ich muss meine Kunden überzeugen, einen schlecht fliegenden Frisbee-Teller zu kaufen, von dem auch niemand freiwillig essen möchte. Da wäre ich besser bei Frisbee-Scheiben geblieben bzw. hätte besser eine eigene Teller-Sparte aufgemacht. 

Zurück zum Thema Wiederverwendbarkeit in der Softwareentwicklung. 

Man kann sie auf verschiedenen Ebenen betrachten: Sowohl im Kleinen (einzelne Code-Fragmente) als auch im Großen (ganze Module, Schnittstellen, Produkte …): 

Wiederverwendbarkeit im Kleinen 

Bei der täglichen Arbeit in einem Team ist es sehr sinnvoll, Clean Code zu schreiben, duplizierten Code zu vermeiden bzw. abzubauen und bei Bedarf auf externe Libraries zurückzugreifen. Das sollte ganz selbstverständlich zum Handwerk des Programmierens dazugehören. 

Doch bereits hier kann man es übertreiben: Nicht jeder Fall von gleich aussehendem Code ist auch eine fachliche Duplikation – manchmal sehen zwei Stellen im Code auch nur “zufällig” gleich aus. Sie können sich aber bei der nächsten Änderung gut und gerne wieder auseinanderbewegen, eben weil sie aus keinem fachlichen Grund gekoppelt sind. 

Auch kann man sich in übertrieben generischem Code verkünsteln, der zwar wiederverwendbar, aber nicht mehr verständlich ist. Oder anders ausgedrückt: Code, der theoretisch oft benutzt werden könnte, aber praktisch nie benutzt werden wird. 

Duplikationsarmut ist ein wichtiges Code-Qualitätsmerkmal, aber eben nicht das einzige. Wenn es mit anderen kollidiert, gilt es abzuwägen. Manchmal braucht es auch Mut zur Duplikation. 

Hier ein Beispiel, wie man Code schlechter machen kann, indem man Duplikation entfernt: Einer von vielen ähnlich aussehenden Tests vor dem Refactoring:  

@Test
void testTempolimitOhneGurt() {
    blitzer.setTempolimit(100); 
    auto.beschleunigeAuf(104); 
    fahrer.benutzeGurt(false);
	
    Knolle knolle = blitzer.sehe(auto) 
        .orElseThrow(AssertionError::new); 
    assertThat(knolle.getBetrag()).isEqualTo(123); 
}

Da es noch viele solche Tests mit nur geringfügig anderen Parametern gibt, erkennt ein refactoring-wütiger Entwickler Duplikation und extrahiert alles in eine Methode: 

@Test
void testTempolimitOhneGurt() {
    runTest(100, Collections.emptyList(), 104, false, true, 123); 
}

void runTest(int tempolimit, List<Einstellung> einstellungen, int tempo, boolean gurt, boolean erwarteKnolle, int betrag) { 
    blitzer.setTempolimit(tempolimit);
    einstellungen.forEach(blitzer::addEinstellung);
    auto.beschleunigeAuf(tempo);
    fahrer.benutzeGurt(gurt); 

    Optional<Knolle> optionalKnolle = blitzer.sehe(auto);
 
    if (erwarteKnolle) {
        Knolle knolle = optionalKnolle.orElseThrow(AssertionError::new); 
        assertThat(knolle.getBetrag()).isEqualTo(betrag); 
    } else { 
        assertThat(optionalKnolle).isEmpty();
    }
}

Jetzt besteht jeder Test aus nur einer Zeile, die leider völlig unverständlich ist. Um den Code zu verstehen, muss man trotzdem zur extrahierten Methode navigieren. Die ist ihrerseits abstrakter und komplizierter als das Original, weil sie alle Fälle abdecken muss. Und es ist nur eine Frage der Zeit, bis ein weiterer Aspekt hinzukommt, der diese Methode noch weiter aufbläst. Das war leider kein guter Refactoring-Schritt. 

Wiederverwendbarkeit im Großen

Spätestens wenn es darum geht, Module / laufende Schnittstellen / ganze Produkte (sagen wir einfach: Komponenten) über Teamgrenzen hinweg wiederverwendbar zu machen, sollte man sehr genau hinschauen. Das ist eine potenziell weitreichende Projektentscheidung. Denn dadurch entstehen neue, nicht-triviale Anforderungen an die wiederzuverwendende Komponente, sprich, an das Team, das sie entwickelt. Dieses muss mehr in Qualität und teamübergreifende Kommunikation investieren und mehr schwer zu vereinbarende Anforderungen unter einen Hut bringen. Gleichzeitig müssen sich die Anwender mit einer Lösung zufriedengeben, die nicht optimal auf ihre Bedürfnisse passt und bei der sie nur teilweise mitreden können – zugunsten einer gewissen Generalität, von der sie fachlich nichts haben. 

Diese Aspekte bilden eine neue Dimension und können die Kosten-Nutzen-Rechnung zum Kippen bringen. Das Team, das die Komponente entwickelt, sollte das bedenken und ein paar Fragen im Vorfeld beleuchten: 

Bevor ich mich als Team vorschnell darauf einlasse, meine Komponente für andere wiederverwendbar zu machen:

1.     Wie gut kenne ich die vorgesehenen Einsatzzwecke meiner Komponente? 

2.     Für wen mache ich meine Komponente wiederverwendbar? 

3.     Wie sehen die das eigentlich? Von wem geht der Wunsch eigentlich aus? 

Zu 1.: Ist die fachliche Überschneidung der verschiedenen Verwendungszwecke groß genug? Kenne ich sie überhaupt? Wenn nein, dann muss ich tun, was wohl niemand will: Gegen unscharfe Anforderungen ohne Produktvision entwickeln. Je unterschiedlicher bzw. diffuser die Verwendungszwecke sind, desto schwieriger wird die Entwicklung, desto eher laufen zukünftige Anforderungen unvereinbar auseinander, desto unzufriedener werden die Anwender sein und desto größer ist das Potenzial zu scheitern. 

Im obigen Frisbee-Teller-Beispiel wurde diese Frage offenbar nicht ernstgenommen, sonst hätte man erkannt, dass die Einsatzzwecke Frisbee-Spielen und Essen viel zu verschieden sind. 

Zu 2.: Wer wird meine Komponente nachher benutzen? 

Mein Team? Das ist unser tägliches Handwerk, s. Wiederverwendbarkeit im Kleinen. 

Ein kleiner, bekannter Kreis von Teams? Schon schwieriger. Ich entwickle nicht mehr nur für mein Produkt, sondern auch für andere. Das schafft neue Abhängigkeiten. Ich arbeite nicht mehr autonom, sondern bin von anderen Teams abhängig. Hoffentlich kann ich gut und unkompliziert mit ihnen reden. Sitzen sie im Nachbarbüro, sollte das gut machbar sein. Arbeiten sie hingegen in anderen Unternehmen mit anderer Arbeitskultur etc., kann die Kommunikation beliebig schwierig werden. 

Mein ganzes (möglicherweise großes) Unternehmen? Achtung, ich kann nicht mehr überblicken, wer meine Komponente wie benutzen wird. Ich muss viel in sauber geschnittene, stabile und selbsterklärende Schnittstellen investieren und werde dennoch ständig mit Problemen konfrontiert sein, die ich vorher nicht absehen konnte. 

Die ganze Welt? Ich muss mich zusätzlich um Vermarktung, umfangreiches Feedback-Management, Kommunikation etc. kümmern. 

Zu 3.: Im besten Fall ist der Kreis der Anwender, die meine Komponente nutzen wollen, bekannt. Dann sollte ich in den Dialog gehen und fragen: 

  • Aus welchen Gründen möchten sie meine Komponente überhaupt wiederverwenden? Warum halten sie das für sinnvoll?  Tun sie das überhaupt oder woher kommt die Anforderung?
  • Kann ich mit ihnen gemeinsam darüber sprechen, wie wir uns das technisch vorstellen? Beispielsweise kann man sich für Wiederverwendung zur Compilezeit (Code oder Artefakte) entscheiden, oder für Wiederverwendung zur Laufzeit (laufende Schnittstelle etc.). Ein weiteres Thema ist Versionierung. Letztendlich geht es um einen frühzeitigen Konsens über die Art und Weise der Wiederverwendung. 
  • Sind sie einverstanden, dass sie bei Anpassungswünschen immer darauf angewiesen sind, dass ich sie hoffentlich bald umsetze? Und dass die Komponente nicht genau auf ihre Bedürfnisse zugeschnitten ist? Und dass immer wieder Breaking Changes auftreten werden, mit denen sie umgehen müssen? 

Das sind Fragen, die in unserer Situation natürlich bereits mit “ja” beantwortet sein sollten. Idealerweise haben die anderen Teams von meiner Komponente gehört und selbst danach gefragt, ob sie sie wiederverwenden können. Es lohnt sich aber dennoch, das nochmal zu überprüfen, da die Anforderung der Wiederverwendung auch gerne mal vom Management kommt. Mit der Begründung, dass Wiederverwendung ja bekanntermaßen effizient ist. 

Egal von wem die Initiative kommt: Eine solche Entscheidung ist sowohl politisch als auch technisch und betrifft sowohl Management als auch Entwicklungsteams. Letztere sind diejenigen, die sie umsetzen. Daher sollten sie und die POs (oder vergleichbare Rollen) auch maßgeblich an der Entscheidung beteiligt werden. 

Fazit:

Durch Wiederverwendung Synergien zu schaffen, statt zu duplizieren – das ist aus bekannten Gründen ein essenzielles Prinzip in der Softwareentwicklung. Manchmal jedoch darf man auch mal den Mut haben, das heilige Gesetz der Wiederverwendung infrage zu stellen und Code zu schreiben, den es an anderer Stelle scheinbar schon gibt. 

Wiederverwendbarkeit einer größeren Komponente ist eine nicht-triviale zusätzliche Anforderung an das Team. Dabei entstehen Entwicklungskosten und hoher Abstimmungsaufwand – potenzielle Gründe zum Scheitern eines Projekts. Außerdem ist die Komponente nicht perfekt auf jeden ihrer Einsatzzwecke zugeschnitten. Diese Nachteile handelt man sich für die gewonnenen Synergien ein. Bevor eine Entscheidung für oder gegen Wiederverwendung leichtfertig getroffen wird, sollte beides gegeneinander abgewogen werden. Eine Abwägung über die Teams hinweg – gemeinsam mit POs und einigen erfahrenen Softwareentwickler*innen kann dabei helfen, bewusste und tragfähige Entscheidungen zu treffen. 

Zurück