Warum hat die Softwareentwicklung das mit der Modularität bisher nicht so richtig hingekriegt? Mir scheint eine wesentliche Ursache die zu große Abhängigkeit zwischen Modulen. Wir müssen anders über Abhängigkeiten nachdenken als bisher.
Darüber habe ich auch schonmal geschrieben, z.B. grad neulich hier. Doch mein Denken kreis immer wieder um das Problem. Deshalb hier nochmal eine etwas andere Perspektive, über die ich schon lange schreiben wollte. Anstoß hat jetzt ein Artikel in der Geo über synthetische Biologie gegeben. Dabei geht es darum, neue Lebewesen (Bakterien) zu produzieren, indem man ihre DNA aus Bausteinen (BioBricks) zusammensetzt, die jeder eine gewünschte Eigenschaft beschreiben. Ein Comic erklärt recht plakativ, wie das zu verstehen ist und funktioniert. Nein, das ist kein Science Fiction! Es wird schon getan.
Die Elektroniker können es also, die Bauingenieure, ebenso die Möbelbauer können es – und nun auch noch die Biologen. Alle können ihre Produkte aus Bausteinen, Bricks, Komponenten zusammensetzen. Nur die Softwareentwicklung tut sich immer noch schwer. Warum?
Naja, ganz grundsätzlich können wir das natürlich auch. Softwarebausteine gibt es schon lange. Früher mussten sie statisch gelinkt werden, heute geht das dynamisch. DLLs oder Bibliotheksassemblies sind kein Hexenwerk. Allemal die User Control Industrie hat das in den letzten 18 Jahren seit VB 1.0 bewiesen.
Und dennoch: irgendwas fehlt, habe ich das Gefühl.
Was das ist, ist mir jetzt nochmal wieder bewusst geworden bei Lektüre des BioBricks-Artikels. Es fehlt uns der rechte Umgang mit Abhängigkeiten.
Ich stelle mal ein paar ganz unterschiedliche erfolgreiche Bauteile nebeneinander:
Warum sind Bauteile von Lego über Moto bis Steuerelement so erfolgreich? Warum funktioniert Wiederverwendung hier?
Ich glaube, erfolgskritisch ist die Entkopplung von ihrer Umwelt. Klingt nicht neu, dass zur Modularisierung irgendwie lose Kopplung gehört. Aber es hilft ja nichts: Wenn wir wider diesen Wissens es nicht tun, dann kommen wir nicht dort an, wo wir hin wollen.
All den erfolgreichen Bauteilen ist gemeinsam, dass sie keine internen funktionalen Abhängigkeiten haben. Was meine ich mit internen funktionalen Abhängigkeiten?
Ich unterscheide zwischen Abhängigkeiten. Natürlich ist die Funktionalität eines Motors davon abhängig, dass er mit Treibstoff versorgt wird. Ein UI Control muss mit Daten gefüttert werden. In einen Transistor muss Strom fließen.
Das sind für mich aber externe oder gar triviale Abhängigkeiten. Ich möchte sie mit dem Begriff Input belegen. Ohne Input erfüllt ein Baustein seine Zweck nicht. Doch dieser Input kommt von außen. Er fließt sozusagen in den Baustein hinein. An der Tonerkartusche wird von außen ein Zahnrad bewegt, ein Legostein wird auf eine Platte gesetzt, ein Treeview Control wird von außen mit Daten befüllt.
Solchem Input, d.h. äußerer Abhängigkeit, stelle ich interne Abhängigkeit gegenüber. Intern ist eine Abhängigkeit, wenn ein Baustein beim Einbau intern verändert werden muss. Dann muss er nämlich seine Umgebung kennen.
Ein unabhängiger, entkoppelter, echter und erfolgreicher Baustein sieht so aus:
Alle oben angeführten Bausteine entsprechen diesem Bild. Man steckt sie mit anderen zusammen zu einem größeren Ganzen – das dann wieder ein Baustein auf einer höheren Abstraktionsebene sein kann. Ein Motor als Baustein eines Autos besteht z.B. aus Zündkerzen, Zylindern, Rohren, Kolben usw. die alle für sich wiederum Bausteine in Bezug auf den Motor sind.
Dieses Zusammenstecken geschieht entweder direkt wie beim Motor oder indirekt über ein Medium wie eine Leiterplatte:
Indirekter Zusammenbau ist flexibler. Die Bauteile müssen nicht so gut aufeinander abgestimmt sein wie beim direkten Zusammenbau, wo wirklich eines in das andere passen muss.
Was aber tun wir, wenn wir unsere “Bausteine”, die Klassen und Assemblies unserer Software entwerfen? Wir koppeln sie ganz fundamental eng, weil wir sie intern (!) voneinander abhängig machen. Da machen auch Interfaces und Adapter keinen Unterschied. Das sieht dann so aus:
Unsere Bausteine sind allermeistens keine Black Boxes, sondern höchstens Grey Boxes. Denn wo wir etwas über die Interna wissen, da herrscht zumindest etwas Durchsicht durch die Bausteinwand. Und nichs anderes als Interna sind es, wenn wir wissen, dass ein Baustein zur Adressdublettenprüfung von einem Persistenzbaustein abhängig ist:
Früher war diese Abhängigkeit statisch und dynamisch in der Klasse, heute ist sie nur noch statisch, weil die konkrete Implementation des Persistenzbausteins dynamisch zur Laufzeit per Dependency Injection (DI) in die Dublettenprüfung “gespritzt” wird. Ein Baustein A ist nicht mehr von einem konkreten Baustein B abhängig, sondern nur noch von Bausteinen, die irgendwie wie B aussehen und funktionieren.
class EinfacheDublettenprüfung : IDublettenprüfung
{
private IPersistenz db;public Einfache Dublettenprüfung(IPersistenz db) { this.db = db; }
…
}
Das ist ein netter Fortschritt, der uns schonmal erlaubt, die Dublettenprüfung isoliert zu bauen und zu testen. Eine Komponentenwerkbank ist damit möglich, auf der ich immer nur an einem Baustein zur Zeit konzentriert arbeite.
Je länger ich darüber jedoch nachdenken, desto mehr komme ich zu der Überzeugung, dass das nur eine Symtomkur ist oder ein Übergangsstadium. Wir sollten bei DI nicht stehenbleiben. Früher waren statische Abhängigkeiten, jetzt sind noch dynamische Abhängigkeiten. Morgen sollten es keine (internen) Abhängigkeiten mehr sein.
Ja, das meine ich so. Wenn ich die erfolgreichen Bausteine anschaue, dann lehren sie uns, dass sie nur praktikabel sind, weil sie eben keine internen Abhängigkeiten haben. Wir stecken nichts in sie hinein. Es gibt keine Dependency Injection in eine Tonerkartusche hinein. Alle Abhängigkeiten sind an der Oberfläche und heißen Input. Ihnen stehen gegenüber andere “Oberflächlichkeiten”, nämlich der Output – der wiederum Input für andere Bauteile ist.
Solange wir unsere Softwarebausteine noch so entwerfen, dass wir sie (dynamisch) immer wieder anpassen müssen, solange haben wir ein Kompositionsproblem. Denn solange ist der Zusammenbau, die Komposition von etwas größerem immer schwierig. Solange gibt es nämlich kein Halten was die Kompliziertheit der “Steckverbindungen” angeht. Eine interne Abhängigkeit ist – so möchte ich mal sagen – per se eine enge Kopplung.
Sobald wir aber dahinkommen, unsere Bausteine nur über Input- und Output-“Pins” zu verbinden, werden wir erleben, dass die Entkopplung steigt und die Wiederverwendbarkeit. Dann werden wir nämlich anfangen, wirklich hierarchische System mit Bausteinen auf unterschiedlichen Ebenen zu bauen, die insbesondere auf niederer Abstraktionsebene besser wiederverwendbar sind.
Und dass sie das auf höherer Ebene nicht sind, macht dann nichts. Das liegt erstens in der Natur der Sache – ein Motherboard kann in weniger Zusammenhängen gebraucht werden als ein Kondensator, der darauf steckt – und zweitens können wir auf einer neuen “Leiterplatte” viel einfacher eine neue Baugruppe zusammenstecken.
Für mich ich also eine fundamentale Separation of Concerns (SoC) überfällig, die noch klarer als bisher mit DI Containern zwischen Baustein und Leiterplatte unterscheidet. Voraussetzung dafür: die Bausteine werden intern unabhängig.
Was könnte das für die Dublettenprüfung bedeuten? Sie wüsste nichts mehr von ihrer konkreten Umgebung. Sie würde von keinem Persistenzbaustein mehr abhängen:
Stattdessen hätte sie weitere oder anders gestaltete Inputs und Outputs, die dann mit beliebigen Inputquellen und Outputsenken verbinden ließen. Die Dublettenprüfung hätte damit die volle Hoheit über ihre “Oberfläche”. Sie wäre von keiner weiteren Schnittstelle abhängig.
Die Veränderung des Programmier- bzw. Denkmodells gegenüber dem heutigen mit seinen internen Abhängigkeiten wird noch deutlicher, wenn ich das obigen Zusammenspiel zwischen Dublettenprüfung und Persistenz “auseinanderziehe”:
Das bedeutet vielleicht etwas mehr “Leiterplattenaufwand”, aber das finde ich nicht schlimm. Der Gewinn an Entkopplung und Wiederverwendbarkeit und Klarheit (SoC) wiegt ihn mehr als auf.
Allerdings: dafür ist ein Umdenken nötig. Das will geübt sein. Ich fange grad auch erst damit an. Aber ich bin guter Dinge, dass in dieser Richtung Erfolg zu finden ist. Mehr Evolvierbarkeit tut Not. Denn wenn die biologischen System evolvierbar sind und jetzt auch noch mit echten Bausteinen “programmiert” werden können, dann sollte das doch auch ein Rezept für die Softwareentwicklung sein.