Ich habe Probleme mit dem, was ich für zu viel Abstraktion in der Codebasis halte (oder zumindest damit zu tun habe). Die meisten Methoden in der Codebasis wurden abstrahiert, um das höchste übergeordnete Element A in der Codebasis aufzunehmen, aber das untergeordnete Element B dieses übergeordneten Elements verfügt über ein neues Attribut, das die Logik einiger dieser Methoden beeinflusst. Das Problem ist, dass diese Attribute in diesen Methoden nicht überprüft werden können, da die Eingabe nach A abstrahiert ist und A dieses Attribut natürlich nicht hat. Wenn ich versuche, eine neue Methode zu erstellen, um B anders zu behandeln, wird sie zur Codeduplizierung aufgerufen. Der Vorschlag meines technischen Leiters besteht darin, eine gemeinsam genutzte Methode zu erstellen, die boolesche Parameter berücksichtigt. Das Problem dabei ist jedoch, dass einige Leute dies als "versteckten Kontrollfluss" betrachten, bei dem die gemeinsam genutzte Methode eine Logik aufweist, die für zukünftige Entwickler möglicherweise nicht erkennbar ist , Außerdem wird diese gemeinsam genutzte Methode einmal zu komplex / kompliziert, wenn zukünftige Attribute hinzugefügt werden müssen, selbst wenn sie in kleinere gemeinsam genutzte Methoden unterteilt ist. Dies erhöht auch die Kopplung, verringert den Zusammenhalt und verstößt gegen das Prinzip der Einzelverantwortung, auf das jemand in meinem Team hingewiesen hat.
Im Wesentlichen hilft ein Großteil der Abstraktion in dieser Codebasis dabei, die Codeduplizierung zu reduzieren, aber es erschwert das Erweitern / Ändern von Methoden, wenn sie die höchste Abstraktion annehmen. Was soll ich in einer solchen Situation tun? Ich bin im Zentrum der Schuld, obwohl sich alle anderen nicht darauf einigen können, was sie für gut halten, also tut es mir am Ende weh.
Antworten:
Nicht alle Codeduplikationen sind gleich.
Angenommen, Sie haben eine Methode, die zwei Parameter verwendet, die aufgerufen werden
total()
. Angenommen, Sie haben einen anderen angerufenadd()
. Ihre Implementierungen sehen völlig identisch aus. Sollten sie zu einer Methode zusammengeführt werden? NEIN!!!Beim Don't-Repeat-Yourself- oder DRY-Prinzip geht es nicht darum, Code zu wiederholen. Es geht darum, eine Entscheidung, eine Idee, zu verbreiten, damit Sie, wenn Sie Ihre Idee jemals ändern, sie überall neu schreiben müssen, wo Sie diese Idee verbreiten. Blegh. Das ist schrecklich. Tu es nicht. Verwenden Sie stattdessen DRY, um Entscheidungen an einem Ort zu treffen .
DRY kann jedoch zur Gewohnheit werden, Code nach einer ähnlichen Implementierung zu durchsuchen, die wie das Kopieren und Einfügen von einem anderen Ort aussieht. Dies ist die hirntote Form von DRY. Zum Teufel, Sie könnten dies mit einem statischen Analysewerkzeug tun. Es hilft nicht, weil es den Punkt von DRY ignoriert, der darin besteht, den Code flexibel zu halten.
Wenn sich meine Gesamtanforderungen ändern, muss ich möglicherweise meine
total
Implementierung ändern . Das bedeutet nicht, dass ich meineadd
Implementierung ändern muss . Wenn ein Goober sie zu einer Methode zusammengefügt hat, habe ich jetzt ein bisschen unnötigen Schmerz.Wie viel Schmerz? Sicherlich könnte ich den Code einfach kopieren und eine neue Methode erstellen, wenn ich sie brauche. Also keine große Sache, oder? Malarky! Wenn nichts anderes kostet dich mich einen guten Namen! Gute Namen sind schwer zu finden und reagieren nicht gut, wenn Sie an ihrer Bedeutung herumspielen. Gute Namen, die die Absicht klar machen, sind wichtiger als das Risiko, dass Sie einen Fehler kopiert haben, der offen gesagt leichter zu beheben ist, wenn Ihre Methode den richtigen Namen hat.
Mein Rat ist also, aufhören zu lassen, dass Knie-Ruck-Reaktionen auf ähnlichen Code Ihre Codebasis in Knoten binden. Ich sage nicht, dass Sie die Tatsache, dass Methoden existieren, ignorieren und stattdessen wohl oder übel kopieren und einfügen können. Nein, jede Methode sollte einen verdammt guten Namen haben, der die eine Idee unterstützt, um die es geht. Wenn die Implementierung mit der Implementierung einer anderen guten Idee übereinstimmt, wen interessiert das heute?
Auf der anderen Seite, wenn Sie eine
sum()
Methode haben, die eine identische oder sogar andere Implementierung hat alstotal()
Ihre, aber immer wenn sich Ihre Gesamtanforderungen ändern, müssen Sie diese ändern,sum()
dann besteht eine gute Chance, dass dies dieselbe Idee unter zwei verschiedenen Namen ist. Der Code wäre nicht nur flexibler, wenn er zusammengeführt würde, es wäre auch weniger verwirrend, ihn zu verwenden.Was boolesche Parameter betrifft, ja, das ist ein übler Codegeruch. Dieses Kontrollflussmaterial ist nicht nur ein Problem, es zeigt auch, dass Sie eine Abstraktion an einem schlechten Punkt eingeschnitten haben. Abstraktionen sollen die Verwendung einfacher und nicht komplizierter machen. Das Übergeben von Bools an eine Methode zur Steuerung ihres Verhaltens ist wie das Erstellen einer geheimen Sprache, die entscheidet, welche Methode Sie wirklich aufrufen. Au! Tu mir das nicht an. Geben Sie jeder Methode ihren eigenen Namen, es sei denn, Sie haben einen ehrlichen Polymorphismus .
Jetzt scheinen Sie von der Abstraktion ausgebrannt zu sein. Das ist schade, denn Abstraktion ist eine wunderbare Sache, wenn sie gut gemacht wird. Sie benutzen es oft, ohne darüber nachzudenken. Jedes Mal, wenn Sie ein Auto fahren, ohne das Zahnstangensystem verstehen zu müssen, jedes Mal, wenn Sie einen Druckbefehl verwenden, ohne an Betriebssystemunterbrechungen zu denken, und jedes Mal, wenn Sie Ihre Zähne putzen, ohne an jede einzelne Borste zu denken.
Nein, das Problem, mit dem Sie anscheinend konfrontiert sind, ist eine schlechte Abstraktion. Abstraktion, die geschaffen wurde, um einem anderen Zweck als Ihren Bedürfnissen zu dienen. Sie benötigen einfache Schnittstellen zu komplexen Objekten, mit denen Sie die Erfüllung Ihrer Anforderungen anfordern können, ohne diese Objekte jemals verstehen zu müssen.
Wenn Sie Client-Code schreiben, der ein anderes Objekt verwendet, wissen Sie, welche Anforderungen Sie haben und was Sie von diesem Objekt benötigen. Das tut es nicht. Aus diesem Grund besitzt Client-Code die Schnittstelle. Wenn Sie der Kunde sind, kann Ihnen nichts sagen, was Sie brauchen, außer Sie. Sie stellen eine Schnittstelle zur Verfügung, die Ihre Anforderungen zeigt, und fordern, dass alles, was Ihnen übergeben wird, diese Anforderungen erfüllt.
Das ist Abstraktion. Als Kunde weiß ich nicht einmal, womit ich spreche. Ich weiß nur, was ich davon brauche. Wenn das bedeutet, dass Sie etwas einpacken müssen, um die Benutzeroberfläche zu ändern, bevor Sie es mir übergeben. Es ist mir egal. Tu einfach was ich tun muss. Hör auf, es kompliziert zu machen.
Wenn ich in eine Abstraktion schauen muss, um zu verstehen, wie man sie verwendet, ist die Abstraktion fehlgeschlagen. Ich sollte nicht wissen müssen, wie es funktioniert. Nur dass es funktioniert. Gib ihm einen guten Namen und wenn ich hineinschaue, sollte mich das, was ich finde, nicht überraschen. Lassen Sie mich nicht weiter nach innen schauen, um mich daran zu erinnern, wie man es benutzt.
Wenn Sie darauf bestehen, dass die Abstraktion auf diese Weise funktioniert, spielt die Anzahl der dahinter stehenden Ebenen keine Rolle. Solange Sie nicht hinter die Abstraktion schauen. Sie bestehen darauf, dass die Abstraktion Ihren Bedürfnissen entspricht und sich nicht an ihre anpasst. Damit dies funktioniert, muss es einfach zu bedienen sein, einen guten Namen haben und nicht auslaufen .
Das ist die Einstellung, die Dependency Injection hervorgebracht hat (oder nur Referenzübergabe, wenn Sie wie ich in der alten Schule sind). Es funktioniert gut, wenn Komposition und Delegierung der Vererbung vorgezogen werden . Die Einstellung hat viele Namen. Mein Favorit ist erzählen, nicht fragen .
Ich könnte dich den ganzen Tag in Prinzipien ertränken. Und es hört sich so an, als ob Ihre Mitarbeiter es bereits sind. Aber hier ist die Sache: Im Gegensatz zu anderen technischen Bereichen ist diese Software-Sache weniger als 100 Jahre alt. Wir alle finden es immer noch heraus. Lassen Sie sich also nicht von jemandem mit viel einschüchterndem Hörbuch dazu zwingen, schwer lesbaren Code zu schreiben. Hören Sie ihnen zu, aber bestehen Sie darauf, dass sie Sinn ergeben. Nimm nichts vom Glauben. Leute, die auf irgendeine Weise codieren, nur weil ihnen gesagt wurde, dass dies der Weg ist, ohne zu wissen, warum sie die größten Probleme von allen machen.
quelle
Das übliche Sprichwort, das wir alle hier und da lesen, lautet:
Nun, das ist nicht wahr! Ihr Beispiel zeigt es. Ich würde daher die leicht modifizierte Aussage vorschlagen (zögern Sie nicht, sie wiederzuverwenden ;-)):
In Ihrem Fall gibt es zwei verschiedene Probleme:
Beide sind miteinander verbunden:
Shape
man essurface()
auf spezielle Weise berechnen kann .Wenn Sie eine Operation abstrahieren, bei der es ein allgemeines Verhaltensmuster gibt, haben Sie zwei Möglichkeiten:
Darüber hinaus kann dieser Ansatz zu einem abstrakten Kopplungseffekt auf Entwurfsebene führen. Jedes Mal, wenn Sie ein neues spezialisiertes Verhalten hinzufügen möchten, müssen Sie es abstrahieren, das abstrakte übergeordnete Element ändern und alle anderen Klassen aktualisieren. Das ist nicht die Art von Veränderungsausbreitung , die man sich wünschen kann. Und es ist nicht wirklich im Geiste der Abstraktionen, unabhängig von der Spezialisierung (zumindest im Design).
Ich kenne Ihr Design nicht und kann nicht mehr helfen. Vielleicht ist es wirklich ein sehr komplexes und abstraktes Problem, und es gibt keinen besseren Weg. Aber wie stehen die Chancen? Die Symptome einer Übergeneralisierung sind hier. Kann es wieder ist es Zeit zu sehen sein, und betrachtet Zusammensetzung über Verallgemeinerung ?
quelle
Immer wenn ich eine Methode sehe, bei der das Verhalten den Typ ihres Parameters einschaltet, überlege ich sofort, ob diese Methode tatsächlich zum Methodenparameter gehört. Zum Beispiel, anstatt eine Methode wie:
Ich würde das tun:
Wir verschieben das Verhalten an den Ort, der weiß, wann es verwendet werden soll. Wir erstellen eine echte Abstraktion, bei der Sie die Typen oder Details der Implementierung nicht kennen müssen. In Ihrer Situation ist es möglicherweise sinnvoller, diese Methode aus der ursprünglichen Klasse (die ich aufrufen werde
O
) zu verschieben,A
um sie einzugeben und in type zu überschreibenB
. Wenn die MethodedoIt
für ein Objekt aufgerufen wird, wechseln SiedoIt
zuA
dem anderen Verhalten in und überschreiben Sie esB
. Wenn es Datenbits gibt, von denendoIt
ursprünglich aufgerufen wurde, oder wenn die Methode an genügend Stellen verwendet wird, können Sie die ursprüngliche Methode verlassen und delegieren:Wir können jedoch etwas tiefer tauchen. Schauen wir uns den Vorschlag an, stattdessen einen booleschen Parameter zu verwenden, und sehen wir, was wir über die Denkweise Ihres Kollegen lernen können. Sein Vorschlag ist zu tun:
Dies sieht sehr ähnlich aus wie
instanceof
in meinem ersten Beispiel, außer dass wir diese Prüfung externalisieren. Dies bedeutet, dass wir es auf zwei Arten nennen müssten:oder:
Erstens hat der Callpoint keine Ahnung, welchen Typ
A
er hat. Sollten wir deshalb Boolesche Werte ganz nach unten weitergeben? Ist das wirklich ein Muster, das wir überall in der Codebasis haben wollen? Was passiert, wenn es einen dritten Typ gibt, den wir berücksichtigen müssen? Wenn die Methode so aufgerufen wird, sollten wir sie in den Typ verschieben und das System die Implementierung für uns polymorph auswählen lassen.Zweitens müssen wir den Typ des Anrufpunkts bereits kennen
a
. Normalerweise bedeutet dies, dass wir entweder die Instanz dort erstellen oder eine Instanz dieses Typs als Parameter verwenden. Das Erstellen einer MethodeO
, dieB
hier benötigt wird, würde funktionieren. Der Compiler würde wissen, welche Methode er wählen soll. Wenn wir solche Änderungen durchlaufen, ist Duplizieren besser, als die falsche Abstraktion zu erstellen , zumindest bis wir herausfinden, wohin wir wirklich gehen. Natürlich schlage ich vor, dass wir nicht wirklich fertig sind, egal was wir bis jetzt geändert haben.Wir müssen uns die Beziehung zwischen
A
und genauer ansehenB
. Im Allgemeinen wird uns gesagt, wir sollten die Zusammensetzung der Vererbung vorziehen . Dies ist nicht in jedem Fall der Fall, aber es ist in einer überraschenden Anzahl von Fällen der Fall, wenn wir uns eingegraben haben.B
Erbt vonA
, was bedeutet, dass wir glauben, dass es sichB
um eine handeltA
.B
sollte genauso verwendet werdenA
, nur dass es etwas anders funktioniert. Aber was sind diese Unterschiede? Können wir den Unterschieden einen konkreteren Namen geben? Ist es nichtB
einA
, aber hat wirklichA
einX
das könnteA'
oder seinB'
? Wie würde unser Code aussehen, wenn wir das tun würden?Wenn wir die Methode
A
wie zuvor vorgeschlagen auf verschieben, können wir eine Instanz vonX
in einfügenA
und diese Methode an Folgendes delegierenX
:Wir können
A'
und implementieren undB'
loswerdenB
. Wir haben den Code verbessert, indem wir einem möglicherweise impliziteren Konzept einen Namen gegeben haben, und uns erlaubt, dieses Verhalten zur Laufzeit anstatt zur Kompilierungszeit festzulegen.A
ist eigentlich auch weniger abstrakt geworden. Anstelle einer erweiterten Vererbungsbeziehung werden Methoden für ein delegiertes Objekt aufgerufen. Dieses Objekt ist abstrakt, konzentriert sich jedoch nur auf die Unterschiede in der Implementierung.Es gibt jedoch noch eine letzte Sache zu beachten. Kehren wir zum Vorschlag Ihres Kollegen zurück. Wenn wir an allen Anrufstellen explizit wissen, welchen Typ
A
wir haben, sollten wir Anrufe tätigen wie:Wir gingen davon aus früher beim Komponieren , dass
A
eine hat ,X
die entwederA'
oderB'
. Aber vielleicht ist auch diese Annahme nicht richtig. Ist dies der einzige Ort, an dem dieser Unterschied zwischenA
undB
wichtig ist? Wenn ja, können wir vielleicht einen etwas anderen Ansatz wählen. Wir haben immer noch einX
, das entwederA'
oder istB'
, aber es gehört nicht dazuA
. NurO.doIt
Sorgen darüber, also lassen Sie uns geben sie nur anO.doIt
:Jetzt sieht unsere Anrufseite so aus:
Wieder einmal
B
verschwindet, und die Abstraktion bewegt sich in die mehr konzentriertX
. DiesmalA
ist es jedoch noch einfacher, wenn man weniger weiß. Es ist noch weniger abstrakt.Es ist wichtig, die Duplizierung in einer Codebasis zu reduzieren, aber wir müssen uns überlegen, warum die Duplizierung überhaupt auftritt. Duplizierung kann ein Zeichen für tiefere Abstraktionen sein, die versuchen, herauszukommen.
quelle
Abstraktion durch Vererbung kann ziemlich hässlich werden. Parallele Klassenhierarchien mit typischen Fabriken. Refactoring kann zu Kopfschmerzen werden. Und auch spätere Entwicklung, der Ort, an dem Sie sich befinden.
Es gibt eine Alternative: Erweiterungspunkte mit strengen Abstraktionen und abgestufte Anpassungen. Sagen wir eine Anpassung von Regierungskunden, basierend auf dieser Anpassung für eine bestimmte Stadt.
Eine Warnung: Leider funktioniert dies am besten, wenn alle (oder die meisten) Klassen erweitert werden. Keine Option für Sie, vielleicht in kleinen.
Diese Erweiterbarkeit funktioniert, indem eine erweiterbare Objektbasisklasse Erweiterungen enthält:
Intern erfolgt eine verzögerte Zuordnung von Objekten zu erweiterten Objekten nach Erweiterungsklassen.
Für GUI-Klassen und -Komponenten die gleiche Erweiterbarkeit, teilweise mit Vererbung. Hinzufügen von Schaltflächen und dergleichen.
In Ihrem Fall sollte eine Validierung prüfen, ob sie erweitert ist, und sich anhand der Erweiterungen validieren. Das Einführen von Erweiterungspunkten nur für einen Fall fügt unverständlichen Code hinzu, der nicht gut ist.
Es gibt also keine Lösung, als zu versuchen, im aktuellen Kontext zu arbeiten.
quelle
'Hidden Flow Control' klingt für mich zu handgewellt.
Jedes Konstrukt oder Element, das aus dem Kontext genommen wurde, kann diese Eigenschaft haben.
Abstraktionen sind gut. Ich temperiere sie mit zwei Richtlinien:
Besser nicht zu früh abstrahieren. Warten Sie auf weitere Beispiele für Muster, bevor Sie abstrahieren. "Mehr" ist natürlich subjektiv und spezifisch für die schwierige Situation.
Vermeiden Sie zu viele Abstraktionsebenen, nur weil die Abstraktion gut ist. Ein Programmierer muss diese Ebenen für neuen oder geänderten Code im Kopf behalten, während er die Codebasis auslotet und 12 Ebenen tief geht. Der Wunsch nach gut abstrahiertem Code kann zu so vielen Ebenen führen, dass es für viele Menschen schwierig ist, ihnen zu folgen. Dies führt auch dazu, dass nur Ninja-Codebasen verwaltet werden.
In beiden Fällen sind "mehr" und "zu viele" keine festen Zahlen. Es hängt davon ab, ob. Das macht es schwer.
Ich mag auch diesen Artikel von Sandi Metz
https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstraction
Duplizierung ist weitaus billiger als die falsche Abstraktion
und
zieht Duplizierung der falschen Abstraktion vor
quelle