Ich habe mir einige Antworten angesehen und bei Google gesucht, konnte aber nichts Hilfreiches finden (dh das hätte keine unangenehmen Nebenwirkungen).
Mein abstraktes Problem ist, dass ich ein Objekt habe und eine lange Abfolge von Operationen daran ausführen muss. Ich betrachte es als eine Art Fließband, als würde man ein Auto bauen.
Ich glaube, diese Objekte würden Methodenobjekte genannt .
In diesem Beispiel hätte ich also irgendwann eine CarWithoutUpholstery, auf der ich dann installBackSeat, installFrontSeat, installWoodenInserts ausführen müsste (die Vorgänge stören sich nicht gegenseitig und könnten sogar parallel ausgeführt werden). Diese Operationen werden von CarWithoutUpholstery.worker () ausgeführt und ergeben ein neues Objekt, das eine CarWithUpholstery wäre, auf der ich dann möglicherweise cleanInsides (), verifyNoUpholsteryDefects () usw. ausführen würde.
Die Operationen in einer einzelnen Phase sind bereits unabhängig, dh ich habe bereits mit einer Teilmenge davon zu kämpfen, die in beliebiger Reihenfolge ausgeführt werden kann (vordere und hintere Sitze können in beliebiger Reihenfolge installiert werden).
Meine Logik verwendet derzeit Reflection, um die Implementierung zu vereinfachen.
Das heißt, sobald ich eine CarWithoutUpholstery habe, überprüft sich das Objekt selbst auf Methoden namens performSomething (). Zu diesem Zeitpunkt werden alle folgenden Methoden ausgeführt:
myObject.perform001SomeOperation();
myObject.perform002SomeOtherOperation();
...
beim Überprüfen von Fehlern und Sachen. Während der Reihenfolge der Operation ist unwichtig, habe ich eine lexikographische Ordnung im Fall zugewiesen immer ich entdecken einige Reihenfolge nach alle wichtig ist. Dies widerspricht YAGNI , kostet aber sehr wenig - eine einfache Sortierung () - und könnte eine massive Umbenennung der Methode (oder die Einführung einer anderen Methode zur Durchführung von Tests, z. B. einer Reihe von Methoden) auf der ganzen Linie ersparen.
Ein anderes Beispiel
Sagen wir, anstatt ein Auto zu bauen, muss ich einen Geheimpolizeibericht über jemanden erstellen und ihn meinem bösen Oberherrn vorlegen . Mein letztes Objekt wird ein ReadyReport sein. Um es zu konstruieren, sammle ich zunächst grundlegende Informationen (Vor- und Nachname, Ehepartner ...). Dies ist meine Phase A. Je nachdem, ob es einen Ehepartner gibt oder nicht, muss ich möglicherweise mit den Phasen B1 oder B2 fortfahren und Sexualitätsdaten für eine oder zwei Personen sammeln. Dies besteht aus mehreren verschiedenen Anfragen an verschiedene Evil Minions, die das Nachtleben, Straßenkameras, Sexshop-Verkaufsbelege und was nicht kontrollieren. Und so weiter und so fort.
Wenn das Opfer keine Familie hat, werde ich nicht einmal in die GetInformationAboutFamily-Phase eintreten, aber wenn ich dies tue, ist es unerheblich, ob ich zuerst den Vater oder die Mutter oder die Geschwister (falls vorhanden) anvisiere. Das kann ich aber nicht, wenn ich keinen FamilyStatusCheck durchgeführt habe, der daher zu einer früheren Phase gehört.
Es funktioniert alles wunderbar ...
- Wenn ich eine zusätzliche Operation benötige, muss ich nur eine private Methode hinzufügen.
- Wenn die Operation mehreren Phasen gemeinsam ist, kann ich sie von einer Oberklasse erben lassen.
- Operationen sind einfach und in sich geschlossen. Kein Wert von einer Operation wird jemals von einer der anderen benötigt (Operationen, die in einer anderen Phase ausgeführt werden).
- Objekte auf der ganzen Linie müssen nicht viele Tests durchführen, da sie nicht einmal existieren könnten , wenn ihre Erstellerobjekte diese Bedingungen überhaupt nicht überprüft hätten. Dh, wenn Einsätze im Armaturenbrett platziert, das Armaturenbrett Reinigen und das Armaturenbrett zu überprüfen, muß ich nicht überprüfen, ob ein Armaturenbrett tatsächlich ist es .
- es ermöglicht ein einfaches Testen. Ich kann ein Teilobjekt leicht verspotten und eine beliebige Methode darauf ausführen, und alle Operationen sind deterministische Blackboxen.
...aber...
Das Problem trat auf, als ich eine letzte Operation in eines meiner Methodenobjekte einfügte, wodurch das Gesamtmodul einen obligatorischen Komplexitätsindex überschritt ("weniger als N private Methoden").
Ich habe die Angelegenheit bereits nach oben gebracht und vorgeschlagen, dass in diesem Fall der Reichtum an privaten Methoden kein Hinweis auf eine Katastrophe ist. Die Komplexität ist da, aber es ist da , weil der Betrieb ist komplex, und es ist eigentlich gar nicht so komplex - es ist nur ist lang .
Am Beispiel des Evil Overlord besteht mein Problem darin, dass der Evil Overlord (auch bekannt als He Who Shall Not Denied ) alle Ernährungsinformationen angefordert hat. Meine Dietary Minions haben mir mitgeteilt, dass ich Restaurants, Küchenzeilen, Straßenverkäufer, nicht lizenzierte Straßenverkäufer und Gewächshäuser abfragen muss Besitzer usw. und der böse (Unter-) Overlord - bekannt als Er, der auch nicht geleugnet werden soll - beschweren sich, dass ich in der GetDietaryInformation-Phase zu viele Abfragen durchführe.
Hinweis : Mir ist bewusst, dass dies unter verschiedenen Gesichtspunkten überhaupt kein Problem darstellt (Ignorieren möglicher Leistungsprobleme usw.). Alles, was passiert, ist, dass eine bestimmte Metrik unglücklich ist, und es gibt eine Rechtfertigung dafür.
Was ich denke, ich könnte tun
Abgesehen von der ersten sind alle diese Optionen machbar und meiner Meinung nach vertretbar.
- Ich habe überprüft, dass ich hinterhältig sein und die Hälfte meiner Methoden deklarieren kann
protected
. Aber ich würde eine Schwäche im Testverfahren ausnutzen und abgesehen davon, dass ich mich rechtfertige, wenn ich erwischt werde, mag ich das nicht. Es ist auch eine Notlösung. Was ist, wenn sich die Anzahl der erforderlichen Operationen verdoppelt? Unwahrscheinlich, aber was dann? - Ich kann diese Phase willkürlich in AnnealedObjectAlpha, AnnealedObjectBravo und AnnealedObjectCharlie aufteilen und habe ein Drittel der Operationen, die in jeder Phase ausgeführt werden. Ich habe den Eindruck, dass dies tatsächlich die Komplexität erhöht (N-1 weitere Klassen), ohne dass dies von Vorteil ist, außer dass ein Test bestanden wird. Ich kann natürlich davon ausgehen, dass ein CarWithFrontSeatsInstalled und ein CarWithAllSeatsInstalled logisch aufeinanderfolgende Stufen sind. Das Risiko, dass Alpha später eine Bravo-Methode benötigt, ist gering und noch geringer, wenn ich sie gut spiele. Aber dennoch.
- Ich kann verschiedene Operationen, die aus der Ferne ähnlich sind, in einer einzigen zusammenfassen.
performAllSeatsInstallation()
. Dies ist nur eine Notlösung und erhöht die Komplexität der einzelnen Operation. Wenn ich die Operationen A und B jemals in einer anderen Reihenfolge ausführen muss und sie in E = (A + C) und F (B + D) gepackt habe, muss ich E und F entbündeln und den Code mischen . - Ich kann eine Reihe von Lambda-Funktionen verwenden und die Prüfung insgesamt ordentlich umgehen, aber ich finde das klobig. Dies ist jedoch die bisher beste Alternative. Es würde das Nachdenken loswerden. Die beiden Probleme, die ich habe, sind, dass ich wahrscheinlich gebeten werde, alle Methodenobjekte neu zu schreiben , nicht nur die hypothetischen
CarWithEngineInstalled
, und obwohl dies eine sehr gute Arbeitsplatzsicherheit wäre, spricht es wirklich nicht allzu viel an. und dass der Code Coverage Checker Probleme mit Lambdas hat (die lösbar sind, aber immer noch ).
Damit...
- Was ist meiner Meinung nach meine beste Option?
- Gibt es einen besseren Weg, den ich nicht in Betracht gezogen habe? ( Vielleicht sollte ich besser sauber kommen und direkt fragen, was es ist? )
- Ist dieses Design hoffnungslos fehlerhaft, und ich gebe besser Niederlage und Graben zu - diese Architektur insgesamt? Nicht gut für meine Karriere, aber wäre es auf lange Sicht besser, schlecht gestalteten Code zu schreiben?
- Ist meine derzeitige Wahl tatsächlich der One True Way, und ich muss kämpfen, um bessere Qualitätsmetriken (und / oder Instrumente) zu installieren? Für diese letzte Option benötige ich Referenzen ... Ich kann nicht einfach mit der Hand auf das @PHB winken, während ich murmle. Dies sind nicht die Metriken, nach denen Sie suchen . Egal wie gerne ich dazu in der Lage wäre
Antworten:
Die lange Abfolge von Operationen scheint eine eigene Klasse zu sein, die dann zusätzliche Objekte benötigt, um ihre Aufgaben zu erfüllen. Es hört sich so an, als wären Sie dieser Lösung nahe. Dieser lange Vorgang kann in mehrere Schritte oder Phasen unterteilt werden. Jeder Schritt oder jede Phase kann eine eigene Klasse sein, die eine öffentliche Methode verfügbar macht. Sie möchten, dass jede Phase dieselbe Schnittstelle implementiert. Dann verfolgt Ihre Montagelinie eine Liste von Phasen.
Stellen Sie sich die
Car
Klasse vor, um auf Ihrem Beispiel für die Fahrzeugmontage aufzubauen :Ich werde die Implementierungen von einigen der Komponenten wegzulassen, wie
IChassis
,Seat
undDashboard
aus Gründen der Kürze.Der
Car
ist nicht für den Bau selbst verantwortlich. Das Fließband ist, also lassen Sie uns das in einer Klasse zusammenfassen:Der wichtige Unterschied besteht darin, dass der CarAssembler über eine Liste der implementierten Assembler-Phasenobjekte verfügt
IAssemblyPhase
. Sie beschäftigen sich jetzt explizit mit öffentlichen Methoden, was bedeutet, dass jede Phase für sich getestet werden kann und Ihr Codequalitäts-Tool zufriedener ist, weil Sie nicht so viel in eine einzelne Klasse stecken. Der Montageprozess ist ebenfalls kinderleicht.Assemble
Überqueren Sie einfach die Phasen und rufen Sie im Auto vorbei. Erledigt. DieIAssemblyPhase
Schnittstelle ist mit nur einer einzigen öffentlichen Methode, die ein Autoobjekt übernimmt, kinderleicht:Jetzt brauchen wir unsere konkreten Klassen, die diese Schnittstelle für jede spezifische Phase implementieren:
Jede Phase kann ziemlich einfach sein. Einige sind nur ein Liner. Einige blenden möglicherweise nur vier Sitze aus, andere müssen das Armaturenbrett konfigurieren, bevor sie dem Auto hinzugefügt werden. Die Komplexität dieses langwierigen Prozesses ist in mehrere wiederverwendbare Klassen unterteilt, die leicht zu testen sind.
Auch wenn Sie sagten, dass die Reihenfolge momentan keine Rolle spielt, könnte dies in Zukunft der Fall sein.
Schauen wir uns zum Abschluss den Prozess für die Montage eines Autos an:
Sie können CarAssembler der Unterklasse zuordnen, um eine bestimmte Art von Auto oder LKW zusammenzubauen. Jede Phase ist eine Einheit innerhalb eines größeren Ganzen, was die Wiederverwendung von Code erleichtert.
Wenn die Entscheidung, was ich geschrieben habe, umgeschrieben werden muss, ein schwarzer Fleck in meiner Karriere wäre, würde ich jetzt als Grabenbagger arbeiten, und Sie sollten keinen meiner Ratschläge befolgen. :) :)
Software Engineering ist ein ewiger Prozess des Lernens, Anwendens und anschließenden erneuten Lernens. Nur weil Sie sich entscheiden, Ihren Code neu zu schreiben, heißt das nicht, dass Sie gefeuert werden. In diesem Fall gäbe es keine Softwareentwickler.
Was Sie skizziert haben, ist nicht der One True Way. Beachten Sie jedoch, dass Codemetriken auch nicht der One True Way sind. Codemetriken sollten als Warnungen angesehen werden. Sie können in Zukunft auf Wartungsprobleme hinweisen. Sie sind Richtlinien, keine Gesetze. Wenn Ihr Code-Metrik-Tool auf etwas hinweist, würde ich zuerst den Code untersuchen, um festzustellen, ob er die SOLID- Prinzipien ordnungsgemäß implementiert . Oftmals wird das Umgestalten von Code, um ihn fester zu machen, den Code-Metrik-Tools gerecht. Manchmal nicht. Sie müssen diese Metriken von Fall zu Fall verwenden.
quelle
Ich weiß nicht, ob dies für Sie möglich ist, aber ich würde über einen datenorientierteren Ansatz nachdenken. Die Idee ist, dass Sie einen (deklarativen) Ausdruck von Regeln und Einschränkungen, Operationen, Abhängigkeiten, Status usw. erfassen. Dann sind Ihre Klassen und Ihr Code allgemeiner, orientieren sich an den Regeln, initialisieren den aktuellen Status von Instanzen und führen Operationen aus Ändern Sie den Status, indem Sie die deklarative Erfassung von Regeln und Statusänderungen verwenden, anstatt Code direkter zu verwenden. Man könnte Datendeklarationen in der Programmiersprache oder einem DSL-Tool oder einer Regel- oder Logik-Engine verwenden.
quelle
Irgendwie erinnert mich das Problem an Abhängigkeiten. Sie möchten ein Auto in einer bestimmten Konfiguration. Wie Greg Burghardt würde ich für jeden Schritt / Gegenstand /… Objekte / Klassen verwenden.
Jeder Schritt deklariert, was er zum Mix hinzufügt (was er ist
provides
) (kann mehrere Dinge sein), aber auch was er benötigt.Anschließend definieren Sie irgendwo die endgültige erforderliche Konfiguration und verfügen über einen Algorithmus, der überprüft, was Sie benötigen, und entscheidet, welche Schritte ausgeführt werden müssen / welche Teile hinzugefügt werden müssen / welche Dokumente gesammelt werden müssen.
Algorithmen zur Auflösung von Abhängigkeiten sind nicht so schwer. Der einfachste Fall ist, wenn jeder Schritt eine Sache liefert und keine zwei Dinge dasselbe liefern und die Abhängigkeiten einfach sind (kein 'oder' in den Abhängigkeiten). Für die komplexeren Fälle: Schauen Sie sich einfach ein Tool wie Debian Apt oder so an.
Schließlich reduziert sich der Bau Ihres Autos auf die möglichen Schritte und lässt den CarBuilder die erforderlichen Schritte herausfinden.
Eine wichtige Sache ist jedoch, dass Sie einen Weg finden müssen, um den CarBuilder über alle Schritte zu informieren. Versuchen Sie dies mit einer Konfigurationsdatei. Das Hinzufügen eines zusätzlichen Schritts würde dann nur ein Hinzufügen zur Konfigurationsdatei erfordern. Wenn Sie Logik benötigen, fügen Sie der Konfigurationsdatei ein DSL hinzu. Wenn alles andere fehlschlägt, müssen Sie sie nicht mehr im Code definieren.
quelle
Ein paar Dinge, die Sie erwähnen, fallen mir als "schlecht" auf
"Meine Logik verwendet derzeit Reflection, um die Implementierung zu vereinfachen."
Dafür gibt es wirklich keine Entschuldigung. Verwenden Sie wirklich einen Zeichenfolgenvergleich von Methodennamen, um zu bestimmen, was ausgeführt werden soll?! Wenn Sie die Reihenfolge ändern, müssen Sie den Namen der Methode ändern?!
"Die Operationen in einer einzelnen Phase sind bereits unabhängig"
Wenn sie wirklich unabhängig voneinander sind, deutet dies darauf hin, dass Ihre Klasse mehr als eine Verantwortung übernommen hat. Für mich scheinen sich beide Beispiele für eine Art Single zu eignen
Ansatz, mit dem Sie die Logik für jede Operation in eine eigene Klasse oder Klassen aufteilen können.
Tatsächlich stelle ich mir vor, dass sie nicht wirklich unabhängig sind. Ich stelle mir vor, dass jeder den Zustand des Objekts untersucht und modifiziert oder zumindest eine Art Validierung durchführt, bevor er beginnt.
In diesem Fall können Sie entweder:
Werfen Sie OOP aus dem Fenster und haben Sie Dienste, die mit den exponierten Daten in Ihrer Klasse arbeiten, z.
Service.AddDoorToCar (Auto Auto)
Haben Sie eine Builder-Klasse, die Ihr Objekt erstellt, indem Sie zuerst die erforderlichen Daten zusammenstellen und das fertige Objekt zurückgeben, d. H.
Car = CarBuilder.WithDoor (). WithDoor (). WithWheels ();
(Die withX-Logik kann wieder in Subbuilder aufgeteilt werden.)
Nehmen Sie den funktionalen Ansatz und übergeben Sie Funktionen / Delgates an eine Änderungsmethode
Car.Modify ((c) => c.doors ++);
quelle