Müssen Abstraktionen die Lesbarkeit von Code verringern?

19

Ein guter Entwickler, mit dem ich zusammenarbeite, erzählte mir kürzlich von Schwierigkeiten bei der Implementierung einer Funktion in einem von uns geerbten Code. Er sagte, das Problem sei, dass der Code schwer zu befolgen sei. Danach habe ich einen genaueren Blick auf das Produkt geworfen und festgestellt, wie schwierig es ist, den Codepfad zu erkennen.

Es wurden so viele Schnittstellen und abstrakte Ebenen verwendet, dass es ziemlich schwierig war zu verstehen, wo die Dinge begannen und endeten. Ich musste an die Zeiten denken, in denen ich mir frühere Projekte angeschaut hatte (bevor ich mich mit den Prinzipien sauberen Codes befasst hatte) und fand es äußerst schwierig, mich im Projekt zurechtzufinden, hauptsächlich, weil meine Code-Navigationstools mich immer an einer Schnittstelle landeten. Es wäre sehr aufwändig, die konkrete Implementierung zu finden oder zu ermitteln, wo etwas in einer Plug-in-Architektur verkabelt ist.

Ich weiß, dass einige Entwickler Abhängigkeitsinjektionsbehälter aus genau diesem Grund strikt ablehnen. Dies verwirrt den Pfad der Software so sehr, dass die Schwierigkeit der Code-Navigation exponentiell zunimmt.

Meine Frage ist: Wenn ein Framework oder Muster so viel Aufwand verursacht, ist es das wert? Ist es ein Symptom für ein schlecht umgesetztes Muster?

Ich denke, ein Entwickler sollte einen Blick auf das Gesamtbild werfen, was diese Abstraktionen für das Projekt bedeuten, damit sie die Frustration überwinden können. Normalerweise ist es jedoch schwierig, das Gesamtbild zu erkennen. Ich weiß, dass ich die Bedürfnisse von IOC und DI nicht mit TDD verkaufen konnte. Für diese Entwickler wird die Lesbarkeit des Codes durch die Verwendung dieser Tools nur viel zu stark eingeschränkt.

Martin Blore
quelle

Antworten:

17

Dies ist wirklich eher ein langer Kommentar zur Antwort von @kevin cline.

Auch wenn die Sprachen selbst dies nicht unbedingt verursachen oder verhindern, hat seine Vorstellung meiner Meinung nach doch etwas mit Sprachen (oder zumindest Sprachgemeinschaften) zu tun. Insbesondere, obwohl Sie in verschiedenen Sprachen auf das gleiche Problem stoßen können, treten in verschiedenen Sprachen häufig recht unterschiedliche Formen auf.

Wenn Sie beispielsweise in C ++ darauf stoßen, ist dies wahrscheinlich weniger ein Ergebnis von zu viel Abstraktion als vielmehr ein Ergebnis von zu viel Klugheit. Beispielsweise hat der Programmierer die entscheidende Transformation, die gerade stattfindet (die Sie nicht finden können), in einem speziellen Iterator versteckt. Das Kopieren von Daten von einem Ort an einen anderen hat also eine Reihe von Nebenwirkungen, die nichts zu bedeuten haben tun mit dem Kopieren der Daten. Um die Dinge interessant zu halten, wird dies mit der Ausgabe verschachtelt, die als Nebeneffekt beim Erstellen eines temporären Objekts beim Umwandeln eines Objekttyps in einen anderen erstellt wird.

Wenn Sie in Java darauf stoßen, ist die Wahrscheinlichkeit sehr viel höher, dass Sie eine Variante der bekannten "Enterprise-Hallo-Welt" sehen, bei der Sie anstelle einer einzelnen Trivialklasse, die etwas Einfaches tut, eine abstrakte Basisklasse erhalten und eine konkrete abgeleitete Klasse, die die Schnittstelle X implementiert und von einer Factory-Klasse in einem DI-Framework usw. erstellt wird. Die 10 Codezeilen, die die eigentliche Arbeit erledigen, sind unter 5000 Zeilen Infrastruktur vergraben.

Einiges davon hängt von der Umgebung ab, mindestens jedoch von der Sprache. Die direkte Arbeit mit Windows-Umgebungen wie X11 und MS Windows ist dafür bekannt, dass aus einem trivialen "Hallo Welt" -Programm mehr als 300 Zeilen nahezu unleserlichen Müll gemacht werden. Im Laufe der Zeit haben wir auch verschiedene Toolkits entwickelt, um uns davon zu isolieren - aber 1) diese Toolkits sind selbst nicht trivial und 2) das Endergebnis ist immer noch nicht nur größer und komplexer, sondern in der Regel auch weniger flexibel als ein Textmodus-Äquivalent (auch wenn nur ein Teil des Texts ausgedruckt wird, ist eine Umleitung in eine Datei selten möglich / wird nur selten unterstützt).

Um die ursprüngliche Frage (zumindest teilweise) zu beantworten: Zumindest, wenn ich sie gesehen habe, handelte es sich weniger um eine schlechte Implementierung eines Musters als um die bloße Anwendung eines Musters, das für die jeweilige Aufgabe ungeeignet war - am meisten Oft wird versucht, ein Muster anzuwenden, das in einem Programm, das unvermeidlich umfangreich und komplex ist, nützlich sein könnte. Wenn es jedoch auf ein kleineres Problem angewendet wird, wird es auch umfangreich und komplex, obwohl in diesem Fall die Größe und Komplexität wirklich vermeidbar war .

Jerry Sarg
quelle
7

Ich stelle fest, dass dies häufig darauf zurückzuführen ist, dass kein YAGNI- Ansatz gewählt wurde. Alles, was Schnittstellen durchläuft, obwohl es nur eine konkrete Implementierung gibt und derzeit keine Pläne für die Einführung anderer, ist ein hervorragendes Beispiel für das Hinzufügen von Komplexität, die Sie nicht benötigen. Wahrscheinlich ist es eine Irrlehre, aber ich sehe den Einsatz von Abhängigkeitsinjektionen genauso.

Carson63000
quelle
+1 für die Erwähnung von YAGNI und Abstraktionen mit einzelnen Referenzpunkten. Die Hauptaufgabe einer Abstraktion besteht darin, den gemeinsamen Punkt mehrerer Dinge herauszufiltern. Wenn eine Abstraktion nur von einem Punkt aus referenziert wird, können wir nicht vom Ausklammern allgemeiner Dinge sprechen, eine Abstraktion wie diese trägt nur zum Jojo-Problem bei. Ich würde dies erweitern, weil dies für alle Arten von Abstraktionen gilt: Funktionen, Generika, Makros, was auch immer ...
Calmarius
3

Nun, nicht genug Abstraktion und Ihr Code ist schwer zu verstehen, weil Sie nicht isolieren können, welche Teile was tun.

Zu viel Abstraktion und Sie sehen die Abstraktion, aber nicht den Code selbst, und dann ist es schwierig, dem eigentlichen Ausführungsthread zu folgen.

Um eine gute Abstraktion zu erzielen, sollte man KISSEN: Sehen Sie sich meine Antwort auf diese Fragen an, um zu wissen, was zu beachten ist , um solche Probleme zu vermeiden .

Ich denke, dass das Vermeiden tiefer Hierarchien und Benennungen der wichtigste Punkt ist, den Sie für den von Ihnen beschriebenen Fall betrachten sollten. Wenn die Abstraktionen gut benannt wären, müssten Sie nicht zu tief gehen, sondern nur auf die Abstraktionsebene, auf der Sie verstehen müssen, was passiert. Mit der Benennung können Sie feststellen, wo sich diese Abstraktionsebene befindet.

Das Problem entsteht im Low-Level-Code, wenn Sie wirklich alle Prozesse verstehen müssen. Dann hilft nur noch die Kapselung über klar isolierte Module.

Klaim
quelle
3
Nun, nicht genug Abstraktion und Ihr Code ist schwer zu verstehen, weil Sie nicht isolieren können, welche Teile was tun. Das ist Verkapselung, keine Abstraktion. Sie können Teile in konkreten Klassen ohne viel Abstraktion isolieren.
Erklärung
Klassen sind nicht die einzigen Abstraktionen, die wir verwenden: Funktionen, Module / Bibliotheken, Services usw. In Ihren Klassen abstrahieren Sie normalerweise jede Funktionalität hinter einer Funktion / Methode, die andere Methoden aufrufen kann, die sich gegenseitig in ihrer Funktionalität abstrahieren.
Klaim
1
@Statement: Das Einkapseln von Daten ist natürlich eine Abstraktion.
Ed S.
Namespace-Hierarchien sind jedoch sehr schön.
JAB
2

Für mich ist es ein Kopplungsproblem und hängt mit der Granularität des Designs zusammen. Selbst die lockerste Form der Kopplung führt Abhängigkeiten von einer Sache zur anderen ein. Wenn das für Hunderte bis Tausende von Objekten gemacht wird, auch wenn sie alle relativ einfach sind, sich an SRP halten und selbst wenn alle Abhängigkeiten zu stabilen Abstraktionen fließen, ergibt sich eine Codebasis, die als zusammenhängendes Ganzes nur sehr schwer zu erklären ist.

Es gibt praktische Dinge, die Ihnen dabei helfen, die Komplexität einer Codebasis einzuschätzen, die in der theoretischen SE nicht häufig erörtert wird, z. B. wie tief Sie in den Aufrufstapel vordringen können, bevor Sie das Ende erreichen, und wie tief Sie gehen müssen, bevor Sie es können Mit viel Vertrauen sollten Sie alle möglichen Nebenwirkungen verstehen, die auf dieser Ebene des Aufrufstapels auftreten können, auch im Falle einer Ausnahme.

Und ich habe nur nach meiner Erfahrung festgestellt, dass flachere Systeme mit flacheren Aufrufstapeln viel einfacher zu überlegen sind. Ein extremes Beispiel wäre ein Entity-Component-System, bei dem Komponenten nur Rohdaten sind. Nur Systeme sind funktionsfähig, und bei der Implementierung und Verwendung eines ECS war es für mich das mit Abstand einfachste System überhaupt, darüber nachzudenken, wann komplexe Codebasen, die sich über Hunderttausende von Codezeilen erstrecken, auf ein paar Dutzend Systeme zusammenwachsen enthalten alle Funktionen.

Zu viele Dinge bieten Funktionalität

Als ich in früheren Codebasen gearbeitet habe, war die Alternative ein System mit Hunderten bis Tausenden von größtenteils winzigen Objekten im Gegensatz zu ein paar Dutzend sperrigen Systemen, bei denen einige Objekte nur zum Weitergeben von Nachrichten von einem Objekt an ein anderes verwendet wurden ( Messagez. B. ein Objekt , das seine hatte) eigene öffentliche Schnittstelle). Das erhalten Sie im Grunde genommen analog, wenn Sie das ECS auf einen Punkt zurücksetzen, an dem Komponenten Funktionalität haben und jede eindeutige Kombination von Komponenten in einer Entität einen eigenen Objekttyp ergibt. Und das wird tendenziell zu kleineren, einfacheren Funktionen führen, die von endlosen Kombinationen von Objekten geerbt und bereitgestellt werden, die jugendliche Ideen modellieren ( ParticleObjekt vs.Physics System, z.B). Es kann jedoch auch zu einem komplexen Diagramm von gegenseitigen Abhängigkeiten führen, das es schwierig macht, zu überlegen, was auf breiter Ebene passiert, einfach weil es so viele Dinge in der Codebasis gibt, die tatsächlich etwas tun und daher etwas falsch machen können - - Typen, die keine "Datentypen", sondern "Objekttypen" mit zugehöriger Funktionalität sind. Typen, die als reine Daten ohne zugehörige Funktionalität dienen, können möglicherweise nichts falsch machen, da sie selbst nichts tun können.

Reine Schnittstellen helfen diesem Verständlichkeitsproblem nicht so sehr, denn selbst wenn dies die "Kompilierzeitabhängigkeiten" weniger kompliziert macht und mehr Spielraum für Änderungen und Erweiterungen bietet, werden die "Laufzeitabhängigkeiten" und Interaktionen dadurch nicht weniger kompliziert. Das Client-Objekt ruft weiterhin Funktionen für ein konkretes Kontoobjekt auf, selbst wenn diese aufgerufen werden IAccount. Polymorphismus und abstrakte Schnittstellen haben ihre Verwendung, aber sie entkoppeln die Dinge nicht so, wie es Ihnen wirklich hilft, über alle Nebenwirkungen zu urteilen, die zu einem bestimmten Zeitpunkt auftreten können. Um diese Art der effektiven Entkopplung zu erreichen, benötigen Sie eine Codebasis, die viel weniger Funktionen enthält.

Mehr Daten, weniger Funktionalität

Daher habe ich den ECS-Ansatz, auch wenn Sie ihn nicht vollständig anwenden, als äußerst hilfreich empfunden, da er Hunderte von Objekten in reine Rohdaten mit umfangreichen, gröber konzipierten Systemen umwandelt, die all das bieten Funktionalität. Es maximiert die Anzahl der "Datentypen" und minimiert die Anzahl der "Objekttypen" und minimiert daher absolut die Anzahl der Stellen in Ihrem System, an denen tatsächlich Fehler auftreten können. Das Endergebnis ist ein sehr "flaches" System ohne komplexe Abhängigkeitsdiagramme, nur Systeme zu Komponenten, niemals umgekehrt und niemals Komponenten zu anderen Komponenten. Grundsätzlich sind es viel mehr Rohdaten und viel weniger Abstraktionen, die die Funktionalität der Codebasis auf Schlüsselbereiche, Schlüsselabstraktionen, zentralisieren und reduzieren.

30 einfachere Dinge sind nicht notwendigerweise einfacher zu überlegen als eine komplexere Sache, wenn diese 30 einfacheren Dinge miteinander zusammenhängen, während die komplexe Sache für sich allein steht. Mein Vorschlag ist also, die Komplexität von den Wechselwirkungen zwischen Objekten auf voluminösere Objekte zu übertragen, die mit nichts anderem interagieren müssen, um eine Massenentkopplung zu erreichen, und zwar auf ganze "Systeme" (wohlgemerkt nicht auf Monolithen und Gottobjekte) keine Klassen mit 200 Methoden, sondern etwas wesentlich Höheres als a Messageoder a Particle(trotz minimalistischer Oberfläche). Bevorzugen Sie einfachere alte Datentypen. Je mehr Sie von diesen abhängig sind, desto weniger Kopplung erhalten Sie. Auch wenn dies einigen SE-Vorstellungen widerspricht, ich habe festgestellt, dass es sehr hilfreich ist.


quelle
0

Meine Frage ist, ob es sich lohnt, wenn ein Framework oder Muster so viel Overhead verursacht? Ist es ein Symptom für ein schlecht umgesetztes Muster?

Vielleicht ist es ein Symptom für die Wahl der falschen Programmiersprache.

Kevin Cline
quelle
1
Ich verstehe nicht, wie das mit der Sprache der Wahl zu tun hat. Abstraktionen sind ein sprachunabhängiges Konzept auf hohem Niveau.
Ed S.
@Ed: Einige Abstraktionen sind in einigen Sprachen einfacher zu realisieren als in anderen.
Kevin Cline
Ja, aber das heißt nicht, dass Sie in diesen Sprachen keine perfekt verwaltbare und leicht verständliche Abstraktion schreiben können. Mein Punkt war, dass Ihre Antwort die Frage nicht beantwortet oder dem OP in irgendeiner Weise hilft.
Ed S.
0

Ein schlechtes Verständnis der Entwurfsmuster ist in der Regel eine Hauptursache für dieses Problem. Eines der schlimmsten Dinge, die ich bei diesem Yo-Yo'ing und Hüpfen von Schnittstelle zu Schnittstelle ohne sehr konkrete Daten dazwischen gesehen habe, war eine Erweiterung für Oracle's Grid Control.
Es sah ehrlich gesagt so aus, als hätte jemand eine abstrakte Factory-Methode und einen Dekorateur-Muster-Orgasmus in meinem gesamten Java-Code gehabt. Und ich fühlte mich genauso hohl und allein.

Jeff Langemeier
quelle
-1

Ich würde auch davor warnen, IDE-Funktionen zu verwenden, die es einfach machen, Dinge zu abstrahieren.

Christopher Mahan
quelle