In der Landschaft der Programmiersprachen und Frameworks, die sich ohnehin schon schnell verändert, gab es im letzten Jahrzehnt einen Pradigmenwechsel, der vielen unbemerkt geblieben ist. Zwar sind OOP und die Unterstützung von Klassen und Objekten noch immer ein Grundpfeiler vieler Programmiersprachen. Doch die Semantik weicht in der Praxis von dieser scheinbar konstanten Syntax mittlerweile ab, und zwar in fast jedem Softwareprojekt.
Heute gibt es Services, DTOs und Komponenten, die häufig noch formal Objekte sind, aber ganz anders genutzt werden. Ein genauer Blick auf das Thema lohnt sich, denn so kann ein wichtiger Trend aufgedeckt werden, der viele andere Entwicklungen der letzten Jahre erklärt.
Eine Bestandsaufnahme
Viele einführende Programmierkurse an Universitäten und Fachhochschulen halten immer noch an der Vermittlung der klassischen objektorientierten Denkweise und Semantik fest. So werden Klassen für geometrische Formen, Tiere oder Fahrzeuge entworfen und einfache Spiele oder Simulationen implementiert, wo jedes Objekt der simulierten Welt auch eine Instanz einer Klasse ist. Das soll, so zumindest die Theorie, die Studenten auf die weit-verbreiteten OOP-Entwicklungstechniken vorbereiten.
Doch selbst in der Spieleentwicklung ist mittlerweile etwa das ECS-Pattern fest etabliert, das allenfalls oberflächlich mit OOP zu tun hat, indem es meist die Klassen-Syntax der Programmiersprache nutzt.
OOP im Backend
Noch deutlicher ist die Diskrepanz im Backend. Hier nutzen die meisten Systeme das Design Pattern (auch Entwurfsmuster genannt) der Dependency Injection. Klassische Beispiele hierfür sind ASP.NET (Core), Spring Boot, Nest.JS oder Laravel, im Frontend-Bereich ist auch Angular zu erwähnen. Dieses Pattern hat zweifellos seinen Ursprung in der Objektorientierung und wurde oft dazu genutzt, die SOLID-Prinzipien umzusetzen. Es überwindet jedoch, genauso wie SOLID, auch ein Stück weit die Einschränkungen und Probleme von OOP. Es ähnelt heute in der Praxis eher einer Art flexiblem Modulsystem und steht in Konkurrenz zu Konstrukten wie ES6-Modulen oder C- und C++-Includes.
Woran kann man diese gewagt erscheinende Behauptung festmachen? Ganz einfach: Ein typisches API-Projekt in ASP.NET Core oder Spring Boot besteht aus Controllern und Services. Diese besitzen meist keinen Zustand, kapseln aber dafür die gesamte Funktionalität. Die DTOs, also Datenobjekte für den externen Gebrauch und Entities, also Datenbank-Datenobjekte, enthalten dagegen alle Daten und dafür nahezu keine eigene Funktionalität. Genau also wie ordentlicher nicht-OOP-Code in C oder JavaScript.
Diese Trennung von Funktionalität und Daten wurde noch vor wenigen Jahren von OOPlern als „prozedural“ angeprangert. Damals galt prozeduraler Code als grundsätzlich schlecht. Objekte sollten immer Daten und Funktionalität enthalten und die Daten so gut wie möglich kapseln (wobei die Sprachen Java und C# dafür nie optimal geeignet waren).
Implizite Einschränkung der OOP-Nutzung
Das hat aufgehört, zumindest was die Nutzung der eben erwähnten Frameworks angeht. Natürlich gibt es auch Hybridlösungen, die meist auf einer Variante der Hexagonal Architecture beruhen, wo das Framework lediglich in einem Adapter verwendet wird. Der Applikationskern kann dann nach reiner OOP-Lehre geschrieben sein. Aber auch hier wird durch das System der Adapter implizit zugegeben, dass OOP nicht das Maß aller Dinge ist.
In der Praxis trifft man die vollständige hexagonale Architektur eher selten an. Die Applikation ist meist in einem Stil geschrieben, der zwar wie ECS in der Regel syntaktisch Klassen und Objekte verwendet. Semantisch jedoch ist sie eher eine Mischung aus funktionalem und prozeduralem und idealerweise auch deklarativem Code. Diese Systeme sind meist übersichtlicher, oft leichter testbar und brauchen insgesamt wenige Codezeilen für dieselbe Funktionalität als mit traditionellem OOP.
Eine interessante Parallele zwischen Dependency Injection und funktionaler sowie deklarativer Programmierung ist auch, dass die Abhängigkeiten explizit angegeben werden müssen. Das macht es leichter, zustandsbedingte Bugs zu vermeiden.
OOP im Frontend und UI
Sogar in der UI-Entwicklung hat die Objektorientierung ihren Platzhirsch-Status teilweise eingebüßt. Seit 2020 zeigt etwa React mit seinen Functional Components und Hooks, dass auch ein funktionaler Ansatz geeignet ist, um Komponenten und ganze Oberflächen mit viel State-Management zu entwickeln. Dieser Ansatz vermeidet sogar eine Reihe von Fehlerquellen. Auch Alternativen wie Svelte sind modulorientiert und deklarativ und haben nichts mehr mit Objektorientierung zu tun. Es ist zu erwarten, dass auch bei Desktop-UIs bald ähnliche Techniken Einzug halten.
Aber wie konnte die objektorientierte Programmierung nahezu unbemerkt von der Bildfläche verschwinden? Wie sind wir wieder bei der modularen Programmierung nach David Parnas gelandet? Und das trotz der ständigen Warnungen vor prozeduralem Code?
Gebrochene Versprechen von OOP
Die Antworten sind meines Erachtens schon in der Hochzeit der OOP zu suchen. Die realen und imaginären Versprechungen des neuen Programmierparadigmas waren groß. Viele hatten noch im Kopf, wie das altbekannte GOTO abgeschafft und gegen Schleifen und Funktionen getauscht wurde. Sie erhofften sich, dass der Wechsel von strukturierter und prozeduraler Programmierung auf OOP einen ähnlichen Produktivitätszuwachs bringen würde. Und tatsächlich: Die GUIs machten dank OOP massive Fortschritte und auch Spiele und Simulationen profitierten vom neuen Paradigma, wenn auch Klassen mit zehntausenden von Zeilen als Zeugen der neuen Komplexität keine Seltenheit waren.
Pragmatismus, Umsetzungsschwierigkeiten und Regeln
Doch im Bereich Geschäftsanwendungen und in der aufkommenden Webentwicklung hatte es OOP schwer. Die meisten Entwicklerteams wussten, dass sie OOP als das neue Paradigma nutzen sollten, doch viele empfanden die neue Technik als zu kompliziert und programmierten weiter prozedural und nutzten lediglich Framework-Klassen, ohne eigene zu erstellen, andere erstellten umfangreiche Objektmodelle mit viel Vererbung und vielen Zustandspermutationen, was sich dann in schwer nachzuverfolgenden Bugs äußerte. Design Patterns gab es zwar, jedoch herrschte noch die Meinung vor, dass Objekte in erster Linie die reale Welt abbilden sollten und Design Patterns gibt es dort eben nicht.
Mitte der 2000er wuchs die Popularität von Clean Code, TDD und Dependency-Injection-Frameworks an, getragen von OOP-Pionieren der ersten Stunde wie Kent Beck und Robert C. Martin. Der OOP-Trend bekam neuen Rückenwind. Sowohl die de-facto-prozeduralen Programmierer als auch die Ersteller der komplizierten Vererbungshierarchien wurden von dieser neuen Bewegung hart ins Gericht genommen. Ein dizipliniertes Vorgehen und strenge Regeln sollten OOP nun tatsächlich zum durchschlagenden Erfolg verhelfen. Gleichzeitig begann im Hintergrund der Aufstieg von JavaScript, einer Sprache mit stark funktionalen Elementen, und funktionalen Features wie LINQ in C# oder List Comprehensions in Python.
Tatsächlich haben die neuen OOP-Regeln, konsequent umgesetzt, einiges bewirkt. Doch war das komplex zu erreichen, und die ursprünglichen großen Versprechen der OOP verschwanden sang- und klanglos. Hier einige davon:
Wiederverwendbarkeit
Das Versprechen extremer Wiederverwendbarkeit ist der „Klassiker“ der nicht eingehaltenen Versprechen, dessen Nichteinhaltung auch viele eingefleischte OOP-Fans schnell eingestehen. Als in den 1990er-Jahren mit der großflächigen Einführung von C++ die große OOP-Welle über die Softwarebranche hereinbrach, wurde pausenlos von der massiv verbesserten Wiederverwendbarkeit objektorientierten Codes gesprochen, damals in erster Linie durch Implementierungsvererbung. Frameworks wie Embarcadero Delphi, das es heute noch gibt, zeugen von dieser Philosophie.
Die Realität war eher ernüchternd und der Prozentsatz an wiederverwendbarem Code stagnierte. Zu viel Zustand, der zu verwalten war und der nach der traditionellen OOP-Lehre geradezu wünschenswert war, machte es schwer, Objekte in verschiedenen Kontexten zu verwenden, ohne spukhafte Fernwirkungen zu erzeugen. Was uns die OOP-Community dafür aber geschenkt hat war eine neue Variante der Bibliotheken mit eigenen Vor- und Nachteilen: die Frameworks. Diese erleichtern viele Aufgaben, sind aber aufwändig in der Architektur und werden daher in der Regel zugekauft oder als Open-Source-Paket integriert. Für die interne Wiederverwendung von Code sind Frameworks dagegen oft wenig attraktiv.
Hier sind wiederum funktionale Lösungen überlegen. Seit der Einführung von Functional Components in React etwa hat so gut wie jedes komplexere Projekt Custom Hooks. Diese sind deutlich einfacher zu erstellen und viel modular als riesige, projekt- oder firmenspezifische Frameworks. Bei der Vorgängertechnik der Class-based components konnte man auch mit funktionalen Techniken arbeiten, aber deutlich eingeschränkter und vor allem mit weniger Wiederverwendungsmöglichkeiten.
Bessere Übereinstimmung mit dem menschlichen Denkmodell
Die Einführung von OOP und schon die Beschreibung der ersten objektorientierten Sprache, Simula, thematisierte immer wieder, dass die objektorientierte Programmierung viel näher an der natürlichen Denkweise des Menschen sei. Überall um uns herum befinden sich Objekte mit verschiedenen Identitäten und Eigenschaften, mit denen man etwas tun kann. Das Gehirn hat nach dieser Lesart also ständig mit Objekten zu tun. Das hört sich erst einmal schlüssig an.
Doch hier liegt ein Denkfehler vor. Denn OOP-Programme arbeiten mit Ausdrücken wie invoice.save()
oder fileSystemSaver.save(invoice)
. Kein Mensch käme von selbst auf die Idee, seine Rechnung anzuweisen, sich selbst zu speichern (ursprüngliche OOP-Idee) oder sich einen abstrakten „Dateisystem-Speicherer“ auszudenken (neue OOP-Schule), der abstrakte Dateien speichert. Die Syntax computer.save(invoice)
entspräche viel eher dem menschlichen Denken, das machte computer
aber zu einer „Gott-Klasse“, was gemeinhin als Anti-Pattern gilt.
Einem „Sprechen mit dem Computer“ kommen prozeduralen Sprachen oder auch praktisch alle Shell-Scriptsprachen wie Bash oder PowerShell mit ihrer Syntax bereits näher als funktionale Sprachen oder gar OOP. Auch deklarative Ansätze, wie man sie in ganz verschiedenen Sprachen erfolgreich einsetzen kann, ähneln der menschlichen Ausdrucksweise. In Zukunft könnten Large Language Models noch eine größere Annäherung daran bringen. Die Stärke von OOP ist das aber gerade nicht.
Wenn man (sinnvollerweise!) Implementierungsvererbung vermeidet und oft auf Interfaces setzt, ist die Diskrepanz noch extremer. Wer denkt schon von seinem Auto als IDrivable
? Wir sehen also, dass dieses Argument sehr zweifelhaft ist oder zumindest aus sehr unterschiedlichen Perspektiven betrachtet werden kann. Im Gegensatz zur Wiederverwendbarkeit wurde dieses Argument auch bis heute wenig von der Community dementiert.
Was bleibt von der OOP-Kultur?
War die Objektorientierung ein völliger Irrweg? Das wäre eine übertriebene Aussage. Denn viele Techniken wären ohne die OOPler und ihr Umfeld nicht oder womöglich erst viel später entstanden, und viele funktionieren sogar noch besser in einem nicht-OOP-Umfeld als mit OOP selbst.
Robert C. Martin, genannt „Uncle Bob“ und Erfinder der SOLID-Prinzipien, hat sich vor einigen Jahren wohlwollend über Clojure und das funktionale Paradigma geäußert und empfindet dieses offenbar als sehr kompatibel mit seinen Ideen.
Die folgenden Ideen aus dem OOP-Umfeld lohnt es sich auf jeden Fall zu studieren und in die Zukunft zu übernehmen.
Test Driven Development (TDD)
Auch wenn striktes TDD, vor allem für schlecht spezifizierte Probleme oder für Frontend-Logik, oft wenig zielführend ist, ist die Technik, zuerst einen Test zu schreiben und diesen dann zu erfüllen, ein wichtiges Werkzeug im Arsenal eines Entwicklers.
Extrem nützlich ist diese Technik auch als Vorbereitung auf ein umfangreiches Refactoring. Hier wird zuerst die aktuelle Implementierung validiert und dann nach jedem Refactoring-Schritt überprüft, ob immer noch alles funktioniert. OOPler haben zwar das automatisierte Testen nicht erfunden, aber enorm weiterentwickelt.
Auch um Bugs zu beheben, eignet sich eine leicht adaptierte Variante der Technik. Man tastet sich mit „grünen“, also erfolgreich durchlaufenden, Tests, an den Bug heran. Dann isoliert man ihn mit einem fehlschlagenden Test und findet dann in der Regel sofort die Stelle, wo sich der Bug versteckt und kann ihn dann beheben. Die bereits erfolgreichen Tests verringern die Wahrscheinlichkeit deutlich, dass die vemeintliche Behebung einen anderen Bug zum Vorschein bringt.
Interessanterweise geht TDD bei funktionalem Code meist leichter von der Hand als mit OOP, da weniger „Gerüstbau“ notwendig ist, um den Code zum Laufen zu bekommen.
Refactoring
Der Begriff Refactoring stammt ursprünglich von Martin Fowler und wurde von ihm eher eng definiert als „diszipliniertes Verfahren, um einen bestehenden Programmcode umzustrukturieren, wobei die interne Struktur geändert wird und das Verhalten nach außen gleich bleibt“ (Quelle). Martin Fowler war und ist eine wichtige Figur in der OOP-Community.
Ganz so streng wie Fowler nehmen viele die Definition von Refactoring nicht, so werden im Rahmen sogenannter Refactorings in der Praxis auch gerne einmal der Funktionsumfang erweitert, die Oberfläche verbessert oder Sicherheitslücken geschlossen. Fowler plädiert für einen kleinschrittigen Refactoring-Prozess und sieht daher eine Kombination mit Erweiterungen als fehleranfällig an.
Grundsätzlich ist dem auch beizupflichten, moderne Versionskontroll-Systeme, wie der derzeitige Monopolist Git, haben diese Problematik aber etwas entschärft. Denn solange man auf Commit-Ebene die Refactorings sauber von den Erweiterungen oder Fehlerbehebungen trennt, lassen sich auch alle Änderungen sauber voneinander entflechten, wenn das nötig sein sollte.
Refactoring als Best Practice
Regelmäßiges Refactoring, oder besser noch, Refactoring als Vorbereitungsschritt vor jeder Softwareänderung, ist wichtig. Und das völlig unabhängig vom Programmierparadigma. Es hat sich enorm als Instrument bewährt, um die Qualität von Software zu verbessern. Im Gegensatz zur Feature-Entwicklung geht es meist schneller von der Hand, als man denkt, vor allem, wenn Tests in der richtigen Granularität vorliegen.
Hier liegt aber auch die Crux. Sind Tests zu kleinteilig (viele „Solitary Unit Tests“ nach Fowlers Terminologie, vielerorts oft nur „Unit Tests“ genannt, wobei dann alles andere ein „Integration Test“ ist), muss man die Refactoring-Arbeit schnell doppelt machen, einmal im Code und einmal in den Tests, kann aber Fehler genau lokalisieren. Andererseits sind grobschlächtige End-To-End-Tests oder API-Tests aufwändig zu schreiben und helfen nicht so sehr bei der Fehlerfindung, schlagen aber selten falschen Alarm. Zu entscheiden, welche Tests man schreiben soll, ist teilweise eine „Wissenschaft für sich“ und wird in zukünftigen Beiträgen noch Thema sein.
Dependency Injection
Die Technik der Dependency Injection ist zwar, wie wir gesehen haben, nicht immer objektorientiert, stammt ursprünglich aber aus diesem Umfeld. Eine wichtige Errungenschaft durch Dependency Injection ist einfacheres Testen, was sich vor allem bemerkbar macht, wenn man kleinteilige Unit-Tests erstellen will. Auch hier lässt es sich trefflich übertreiben und dogmatisieren, aber es ist auf jeden Fall gut, die Möglichkeit, einzelne Module isoliert zu testen, immer in der Hinterhand zu haben.
Patterns
Hier wird bewusst nicht von Design Patterns gesprochen, denn da ist man schnell wieder in Gedanken bei den 23 Gang-of-Four-Patterns, und diese sind nicht unbedingt alle gut gealtert. Singleton und Visitor in ihrer ursprünglichen Form sind heute eher als Anti-Patterns verschrien. Was bleibt, ist aber der grundsätzliche Gedanke, dass man die Struktur hinter guten Programmierlösungen und gutem Code versteht und daraus eine Blaupause macht, die man wieder und wieder anwenden kann. Wichtig dabei ist nur, dass man nie davon ausgeht, bereits alle, oder auch nur die besten Patterns, gefunden zu haben.
Ein Beispiel: Momentan revolutioniert HTMX sowohl Frontend als auch Backend und löst ein bisher erfolgreiches Architektur-Pattern, clientseitiges State-Management in Kombination mit JSON-APIs, in Teilen ab. HTMX zeigt damit, dass wir uns bisher eventuell übermäßig auf dieses Pattern verlassen haben. Seit der offiziellen Einführung von HTML5 im Jahr 2014, wo auch React noch in den Kinderschuhen steckte, wäre eine Library wie HTMX bereits möglich gewesen. Aber erst jetzt setzt sich dieser Ansatz durch.
Object Relational Mapping (ORM)
Die Softwarekategorie der ORMs (Object Relational Mapper) kommt ganz klar aus der objektorientierten Community. Spätestens als sich Java durchsetzte, wuchs der Bedarf, objektorientierte Systeme an die schon länger gängigen relationalen Datenbanken anzubinden. Imagebasierte Systeme wie Smalltalk, wo die Daten einfach als Objekte im Image verwaltet wurden, waren im Verschwinden begriffen, und objektorientierte Datenbanken waren den meisten Entscheidern zu nischig und auch noch nicht lizenziert. Also SQL. Doch aus den Datenmengen, die ein SELECT
-Statement zurücklieferte, Objekte mit verknüpftem Verhalten zu machen und diese dann auch wieder korrekt in die Datenbank zurückzuspeichern, stellte sich als mühsam und fehleranfällig heraus. Also musste eine Bibliothek her, die das erleichterte. Daraus entwickelten sich die heutigen ORMs.
Vorteile von ORMs
Es ist nicht wegzudiskutieren, dass ORMs wie TypeORM, Prisma, Hibernate oder Entity Framework Core die Entwicklung von stark datenbankbasierten Anwendungen sehr beschleunigt und vereinfacht haben. Eigentlich ist die Bezeichnung ORM mittlerweile etwas ungenau, denn ORMs lassen sich sehr gut auch in rein prozeduralem oder in Code mit funktionalen Elementen einsetzen. Insbesondere Entity Framework Core ist hier hervorzuheben, da es nahtlos mit dem C#- und .NET-Feature LINQ zusammenarbeitet und funktionale Operationen wie Map (C#: Select
) oder Filter (C#: Where
) und viele andere direkt in SQL übersetzen kann. Diese Funktionalität ist sogar erweiterbar und wird auch vonseiten Microsofts immer weiter vervollständigt. Das ORM Prisma, geschrieben in TypeScript und Rust, zeichnet sich wiederum durch sein durchdachtes Migrations- und Schemasystem aus.
Nachteile und Einschränkungen von ORMs
Nichtsdestotrotz gibt es auch hier Schattenseiten. Man handelt sich etwa, insbesondere bei einem OOP-Ansatz, zwei oft sehr ähnliche Objektkategorien ein: Entities, also Datenbankobjekte und Domänen-Objekte oder bei einem weniger objektorientierten Ansatz DTOs. Diese beiden Objektkategorien ineinander umzusetzen kann, vor allem bei Sprachen wie Java oder C#, die kein Duck-Typing bzw. strukturelle Typisierung unterstützen, viel Boilerplate-Code oder eigene Libraries erfordern. Hier zeigt sich der „Object-relational impedance mismatch¨ dann doch wieder, was zeigt, das ORMs allenfalls syntaktisch Objekte erzeugen, aber nicht semantisch.
Manche ORMs, wie etwa Prisma, leiden noch unter dem 1+N-Problem, das schon aus den Frühzeiten der ORMs bekannt ist und selbst von Delphi schon besser umschifft wurde. Abfragen mit vielen Joins, die vor allem in Bereichen wie Datenanalyse- und Export oder der Transformation von Daten für eine andere Zielgruppe nicht selten sind, werden dadurch so langsam, dass das System nicht benutzbar ist. Abgesehen davon zeigen alle ORMs gewisse Schwächen bei Themen wie Transaktions-Handling und komplexen INSERT
– und UPDATE
-Befehlen. Jedes ORM bringt außerdem zwangsläufig wenn auch noch so geringe Performanceeinbußen und mehr Komplexität mit.
Nicht zuletzt besteht bei der Nutzung von ORMs bei Projekten „auf der grünen Wiese“ die Gefahr, dass sich Datenbank-Designfehler einschleichen, wenn neue Entity-Klassen erzeugt und miteinander verknüpft werden, ohne auf deren relationale Eigenschaften zu achten. Das äußert sich dann später, bei größeren Datenmengen, in plötzlichen Performanceproblemen, Bugs oder unerklärlichen Abstürzen.
Neue Regeln für die Performance
Das Moore’sche Gesetz, nach dem sich jedes Jahr die Anzahl der Transistoren und damit ungefähr die Rechenleistung von Mikrochips verdoppelt, ist bereits seit 2010 außer Kraft. Daher kann man sich nicht mehr so sehr darauf verlassen, dass steigende oder billigere Rechenleistung Performanceprobleme ausbügelt, zumindest nicht bei CPUs. Es ist wichtig, Datenbanken dafür zu verwenden, was sie gut können, nämlich große Datenmengen zu verarbeiten. Auch deswegen sind ORMs kein Allheilmittel. Wer sie einsetzt, sollte immer damit rechnen, auch einmal rohes SQL schreiben zu müssen. Ein hier zu erwähnendes Anti-Pattern ist das „Hacken“ des ORMs, indem so lange am Query herumgedoktert wird, bis effizientes SQL herauskommt, anstatt das SQL einfach selbst zu schreiben.
Agile Softwareentwicklungstechniken
Niemand möchte zurück zum Wasserfall-Prozess und bis auf wenige Branchen ist die reine Wasserfall-Entwicklung ohne Zwischenfeedback oder -releases auch zur Seltenheit geworden. Dass dem so ist, haben wir zu großen Teilen der agilen Bewegung zu verdanken. Diese war immer sehr eng mit der objektorientierten Community und mit Vertretern von Unit-Testing, TDD und SOLID verknüpft.
Doch auch hier gibt es Schattenseiten und Anlass zur Kritik. Die reflexhafte Reaktion „Das war kein richtiges Scrum/XP“, wenn auf Probleme mit der Umsetzung agiler Techniken hingewiesen wird, ist weder hilfreich, noch pragmatisch. Hochspezifizierte agile Prozesse, wie Scrum, sind in vielen Fällen noch zu bürokratisch. Sie erlauben es auch, Hierarchien offiziell wegzudefinieren und inoffiziell beizubehalten, was eine schlechtere Lösung ist, als eine offene Hierarchie. Weiters fehlt bei fast allen agilen Prozessmodellen eine Differenzierung zwischen produkt- und projektbasierter Entwicklung.
Auf welche Elemente kann man eher verzichten?
Hier sind einige Eigenschaften der OOP-Kultur aufgeführt, die eher überholt sind oder noch nie besonders hilfreich waren:
Detaillierte Architekturdiagramme und UML
Die umfangreiche Unified Modeling Language bietet Vorlagen für viele Diagramme, die eine detaillierte Planung von Softwaresystemen erlauben. Zwar kann UML in einer Basisvariante, und in der Regel handschriftlich, ein nützliches Mittel für die Verständigung zwischen Entwicklern sein, wenn man gemeinsam verschiedene Architekturvarianten ausbaldowert, eine Nutzung die darüber hinausgeht, zahlt sich aber meist nicht aus. Geänderte Anforderungen führen dazu, dass ein initiales Refactoring nötig wird, bevor man die neuen Anforderungen implementiert. Dann helfen die alten UML-Diagramme wenig. Oft verbirgt sich hinter der Nutzung von UML ein Anti-Pattern, wo nur ein Entwickler als Architekt fungiert und die anderen nur codieren. So bleibt aber der Lerneffekt aus oder ist viel geringer, als er sein könnte.
Dogmatismus
Dogmatismus ist, zumindest meines Erachtens, ein generelles Problem in der Softwarebranche. Es zeigt sich durch Aussagen wie „Das war kein echtes Scrum“ oder einer Überhöhung der eigenen Sprache oder des eigenen Frameworks, oder auch einem Elitismus gegenüber Technologien wie PHP oder Visual-Programming-Tools. Auch Vorwürfe, man sei nicht professionell, wenn man nicht einen bestimmten Prozess, wie etwa TDD, einsetze, fallen in diese Kategorie. Produktive Diskussionen sehen für uns anders aus.
Wegdefinieren von Problemen, ohne diese tatsächlich zu lösen
Die Tendenz, eigene Fehler nicht zuzugeben, hängt unmittelbar mit Dogmatismus zusammen und ist daher keineswegs ein Alleinstellungsmerkmal der OOP-Community. Dennoch hat die diese und ihr Umfeld viele vollmundige Versprechungen abgegeben, wie extreme Wiederverwendbarkeit, „Twice the work in half the time“ (Scrum), garantierte Bugfreiheit beim Refactoring und garantiert saubere Architektur (TDD). Diese wurden zu einem großen Teil dann nicht eingehalten und erst viel später wurden die Einschränkungen zerknirscht eingeräumt. Das widerspricht dem Grundsatz „Underpromise and overdeliver.“
In allen drei Fällen hat man lange an den Versprechungen festgehalten. Wenn sie nicht eingetreten sind, hat man das auf handwerkliche Fehler zurückgeführt. In der agilen Community ist das noch heute verbreitet. Die Taktik funktioniert, da handwerkliche Fehler in der Praxis natürlich vorkommen. Nur ist, selbst wenn es sich immer nur um handwerkliche Fehler handeln sollte, eine Technik, die kaum jemand richtig anwenden kann, keine gute Technik.
Diese Schwäche ist nicht zu verwechseln mit dem äußerst erfolgreichen Prinzip „Define errors out of existence“ von John Ousterhout und Fehlerreduktionsmethoden, die auf W. Edwards Deming zurückgehen. Diese Ansätze versprechen jedoch nur, dass die absolute Fehlerzahl immer weiter reduziert wird, nicht dass Fehler zur Gänze abgeschafft werden. Das macht sie, im Gegensatz zu vielen agilen und OOP-Ansätzen, realitätsnah und pragmatisch.
SOLID-Prinzipien
Die SOLID-Prinzipien führen, wenn man sie konsequent umsetzt, zu einer Codebasis, die einer funktional-prozeduralen ähnelt. Sie ist jedoch meist unübersichtlicher („everything happens somewhere else“) und lässt wenig Kapselung zu. Aufgrund der konsequenten Trennung aller Zuständigkeiten muss der Entwickler hier Klassen aufspalten. Das bedeutet zwangsläufig, dass mehr Daten hin- und herwandern.
Natürlich lässt sich das mit tiefen Klassenverschachtelungen in den Griff bekommen, aber da finden wir eine logisch strukturierte Codebasis, die funktionale Techniken nutzt, doch deutlich übersichtlicher.
Fazit
Der Trend weg von der Objektorientierung ist ungebrochen und es ist zweifelhaft, ob die reine Lehre der OOP jemals wieder zurückkommen wird. Die Entwickler-Community hat erkannt, dass variable Zustände zu Buganfälligkeit führen und dass viele Techniken aus dem agilen Umfeld im Post-OOP-Paradigma sogar besser funktionieren als mit OOP. Es wird Zeit, dass auch die akademische Lehre abseits von Machine-Learning oder Datenanalyse diese Erkenntnisse übernimmt.
Durch die neue Welt, in der wir leben und in der wir uns immer schneller anpassen müssen, können wir uns Struktur als Selbstzweck nicht mehr leisten. Die kommenden Jahre und womöglich Jahrzehnte sind die Zeit des Pragmatismus. Immer mehr lokale Maxima werden aufgebrochen und es wird gnadenlos optimiert. Nur die besten und flexibelsten Systeme überleben. Gehen wir gemeinsam in diese spannende Zeit!
Das Titelbild ist KI-generiert.