Wahrscheinlich habe ich wie viele andere Probleme mit Designproblemen, bei denen es beispielsweise ein Designmuster / einen Designansatz gibt, das intuitiv zu dem Problem zu passen scheint und die gewünschten Vorteile bietet. Sehr oft gibt es einige Einschränkungen, die es schwierig machen, das Muster / den Ansatz ohne irgendeine Art von Arbeit zu implementieren, die dann den Nutzen des Musters / Ansatzes negiert. Ich kann sehr leicht durch viele Muster / Ansätze iterieren, da vorhersehbar fast alle in realen Situationen, in denen es einfach keine einfache Lösung gibt, einige sehr bedeutende Einschränkungen aufweisen.
Beispiel:
Ich werde Ihnen ein hypothetisches Beispiel geben, das lose auf einem realen basiert, dem ich kürzlich begegnet bin. Angenommen, ich möchte Komposition über Vererbung verwenden, da Vererbungshierarchien in der Vergangenheit die Skalierbarkeit des Codes beeinträchtigt haben. Ich könnte den Code umgestalten, aber dann feststellen, dass es einige Kontexte gibt, in denen die Oberklasse / Basisklasse einfach die Funktionalität der Unterklasse aufrufen muss, obwohl versucht wird, dies zu vermeiden.
Der nächstbeste Ansatz scheint darin zu bestehen, ein Muster aus einem halben Delegierten / Beobachter und einem halben Kompositionsmuster zu implementieren, damit die Oberklasse das Verhalten delegieren kann oder damit die Unterklasse Ereignisse der Oberklasse beobachten kann. Dann ist die Klasse weniger skalierbar und wartbar, da unklar ist, wie sie erweitert werden soll. Außerdem ist es schwierig, vorhandene Listener / Delegaten zu erweitern. Auch Informationen sind nicht gut versteckt, da man die Implementierung kennen muss, um zu sehen, wie die Oberklasse erweitert werden kann (es sei denn, Sie verwenden Kommentare sehr häufig).
Danach könnte ich mich dafür entscheiden, einfach nur Beobachter oder Delegierte vollständig einzusetzen, um den Nachteilen zu entkommen, die mit einer starken Verwechslung der Ansätze verbunden sind. Dies bringt jedoch seine eigenen Probleme mit sich. Zum Beispiel könnte ich feststellen, dass ich für eine zunehmende Anzahl von Verhaltensweisen Beobachter oder Delegierte benötige, bis ich für praktisch jedes Verhalten Beobachter / Delegierte benötige. Eine Option könnte darin bestehen, nur einen großen Listener / Delegaten für das gesamte Verhalten zu haben, aber dann hat die implementierende Klasse viele leere Methoden usw.
Dann könnte ich einen anderen Ansatz versuchen, aber es gibt genauso viele Probleme damit. Dann der nächste und der nach etc.
Dieser iterative Prozess wird sehr schwierig, wenn jeder Ansatz so viele Probleme zu haben scheint wie jeder andere und zu einer Art Lähmung der Entwurfsentscheidung führt . Es ist auch schwierig zu akzeptieren, dass der Code gleichermaßen problematisch wird, unabhängig davon, welches Entwurfsmuster oder welcher Ansatz verwendet wird. Wenn ich in dieser Situation lande, bedeutet das, dass das Problem selbst überdacht werden muss? Was machen andere, wenn sie auf diese Situation stoßen?
Bearbeiten: Es scheint eine Reihe von Interpretationen der Frage zu geben, die ich klären möchte:
- Ich habe OOP vollständig aus der Frage herausgenommen, weil sich herausstellt, dass es nicht spezifisch für OOP ist, und es zu leicht ist, einige der Kommentare, die ich im Vorbeigehen über OOP gemacht habe, falsch zu interpretieren.
- Einige haben behauptet, ich sollte einen iterativen Ansatz wählen und verschiedene Muster ausprobieren, oder ich sollte ein Muster verwerfen, wenn es nicht mehr funktioniert. Dies ist der Prozess, auf den ich mich überhaupt beziehen wollte. Ich dachte, das wäre aus dem Beispiel klar, aber ich hätte es klarer machen können, also habe ich die Frage bearbeitet, um dies zu tun.
Antworten:
Wenn ich zu einer so schwierigen Entscheidung komme, stelle ich mir normalerweise drei Fragen:
Was sind die Vor- und Nachteile aller verfügbaren Lösungen?
Gibt es eine Lösung, über die ich noch nicht nachgedacht habe?
Am wichtigsten:
Was genau sind meine Anforderungen? Nicht die Anforderungen auf dem Papier, die wirklich grundlegenden?
Kann ich das Problem irgendwie neu formulieren / seine Anforderungen optimieren, so dass eine einfache, unkomplizierte Lösung möglich ist?
Kann ich meine Daten auf andere Weise darstellen, so dass eine so einfache und unkomplizierte Lösung möglich ist?
Dies sind die Fragen zu den Grundlagen des wahrgenommenen Problems. Es kann sich herausstellen, dass Sie tatsächlich versucht haben, das falsche Problem zu lösen. Ihr Problem kann spezifische Anforderungen haben, die eine viel einfachere Lösung ermöglichen als der allgemeine Fall. Stellen Sie Ihre Formulierung Ihres Problems in Frage!
Ich finde es sehr wichtig, über alle drei Fragen nachzudenken, bevor ich fortfahre. Machen Sie einen Spaziergang, gehen Sie in Ihrem Büro auf und ab, tun Sie alles, um über die Antworten auf diese Fragen nachzudenken. Die Beantwortung dieser Fragen sollte jedoch nicht unangemessen lange dauern. Die richtigen Zeiten können zwischen 15 Minuten und etwa einer Woche liegen. Sie hängen von der Schlechtigkeit der bereits gefundenen Lösungen und ihren Auswirkungen auf das Ganze ab.
Der Wert dieses Ansatzes besteht darin, dass Sie manchmal überraschend gute Lösungen finden. Elegante Lösungen. Lösungen, die die Zeit wert sind, die Sie in die Beantwortung dieser drei Fragen investiert haben. Und Sie werden diese Lösung nicht finden, wenn Sie einfach sofort die nächste Iteration eingeben.
Natürlich scheint es manchmal keine gute Lösung zu geben. In diesem Fall bleiben Sie bei Ihren Antworten auf Frage eins hängen, und gut ist einfach das am wenigsten schlechte. In diesem Fall sollten Sie sich die Zeit nehmen, um diese Fragen zu beantworten, da Sie möglicherweise Iterationen vermeiden, die fehlschlagen müssen. Stellen Sie einfach sicher, dass Sie innerhalb einer angemessenen Zeitspanne wieder mit dem Codieren beginnen.
quelle
Das Wichtigste zuerst - Muster sind nützliche Abstraktionen, nicht das Ende des gesamten Designs, geschweige denn des OO-Designs.
Zweitens - modernes OO ist klug genug zu wissen, dass nicht alles ein Objekt ist. Manchmal führt die Verwendung einfacher alter Funktionen oder sogar einiger Skripte im imperativen Stil zu einer besseren Lösung für bestimmte Probleme.
Nun zum Fleisch der Dinge:
Warum? Wenn Sie eine Reihe ähnlicher Optionen haben, sollte Ihre Entscheidung einfacher sein! Sie werden nicht viel verlieren, wenn Sie die "falsche" Option wählen. Und wirklich, Code ist nicht repariert. Probieren Sie etwas aus, um zu sehen, ob es gut ist. Iterieren .
Harte Nüsse. Harte Probleme - tatsächliche mathematisch schwierige Probleme sind einfach schwer. Wahrscheinlich schwer. Es gibt buchstäblich keine gute Lösung für sie. Und es stellt sich heraus, dass die einfachen Probleme nicht wirklich wertvoll sind.
Aber sei vorsichtig. Zu oft habe ich Leute gesehen, die durch keine guten Optionen frustriert waren, weil sie das Problem nicht auf eine bestimmte Art und Weise betrachten oder weil sie ihre Verantwortung auf eine Weise reduziert haben, die für das jeweilige Problem unnatürlich ist. "Keine gute Option" kann ein Geruch sein, dass etwas grundlegend falsch mit Ihrem Ansatz ist.
Perfekt ist der Feind des Guten. Lass etwas funktionieren und überarbeite es dann.
Wie bereits erwähnt, minimiert die iterative Entwicklung dieses Problem im Allgemeinen. Sobald Sie etwas zum Laufen gebracht haben, sind Sie mit dem Problem besser vertraut und können es besser lösen. Sie haben tatsächlich den Code zu sehen und zu bewerten, nicht irgendein abstraktes Design , das nicht scheint recht.
quelle
Die Situation, die Sie beschreiben, sieht aus wie ein Bottom-up-Ansatz. Sie nehmen ein Blatt, versuchen es zu reparieren und stellen fest, dass es mit einem Zweig verbunden ist, der selbst auch mit einem anderen Zweig usw. verbunden ist.
Es ist, als würde man versuchen, ein Auto zu bauen, das mit einem Reifen beginnt.
Was Sie brauchen, ist einen Schritt zurück zu treten und das Gesamtbild zu betrachten. Wie sitzt dieses Blatt im Gesamtdesign? Ist das noch aktuell und richtig?
Wie würde das Modul aussehen, wenn Sie es von Grund auf neu entwerfen und implementieren würden? Wie weit von diesem "Ideal" entfernt ist Ihre aktuelle Implementierung.
Auf diese Weise haben Sie ein größeres Bild davon, worauf Sie hinarbeiten müssen. (Oder wenn Sie entscheiden, dass es zu viel Arbeit ist, was sind die Probleme).
quelle
Ihr Beispiel beschreibt eine Situation, die normalerweise bei größeren Teilen von Legacy-Code auftritt und wenn Sie versuchen, ein "zu großes" Refactoring durchzuführen.
Der beste Rat, den ich Ihnen für diese Situation geben kann, ist:
Versuchen Sie nicht, Ihr Hauptziel mit einem "Urknall" zu erreichen .
Erfahren Sie, wie Sie Ihren Code in kleineren Schritten verbessern können!
Das ist natürlich leichter zu schreiben als zu tun. Wie kann man das in der Realität erreichen? Nun, Sie brauchen Übung und Erfahrung, es hängt sehr stark vom Fall ab, und es gibt keine feste Regel, die "dies oder das tun" sagt und für jeden Fall passt. Aber lassen Sie mich Ihr hypothetisches Beispiel verwenden. "Komposition über Vererbung" ist kein Alles-oder-Nichts-Entwurfsziel, sondern ein perfekter Kandidat, der in mehreren kleinen Schritten erreicht werden kann.
Nehmen wir an, Sie haben festgestellt, dass "Komposition über Vererbung" das richtige Werkzeug für diesen Fall ist. Es muss einige Hinweise dafür geben, dass dies ein vernünftiges Ziel ist, sonst hätten Sie dies nicht gewählt. Nehmen wir also an, dass die Oberklasse viele Funktionen enthält, die nur von den Unterklassen "aufgerufen" werden. Diese Funktionalität ist also ein Kandidat dafür, nicht in dieser Oberklasse zu bleiben.
Wenn Sie feststellen, dass Sie die Oberklasse nicht sofort aus den Unterklassen entfernen können, können Sie zunächst die Oberklasse in kleinere Komponenten umgestalten, die die oben genannten Funktionen enthalten. Beginnen Sie mit den am niedrigsten hängenden Früchten und extrahieren Sie zuerst einige einfachere Komponenten, wodurch Ihre Oberklasse bereits weniger komplex wird. Je kleiner die Oberklasse wird, desto einfacher werden zusätzliche Refactorings. Verwenden Sie diese Komponenten sowohl aus den Unterklassen als auch aus der Oberklasse.
Wenn Sie Glück haben, wird der verbleibende Code in der Oberklasse während dieses Vorgangs so einfach, dass Sie die Oberklasse ohne weitere Probleme aus den Unterklassen entfernen können. Oder Sie werden feststellen, dass das Beibehalten der Oberklasse kein Problem mehr darstellt, da Sie bereits genug Code in Komponenten extrahiert haben, die Sie ohne Vererbung wiederverwenden möchten.
Wenn Sie sich nicht sicher sind, wo Sie anfangen sollen, weil Sie nicht wissen, ob sich ein Refactoring als einfach herausstellen wird, ist es manchmal am besten, ein Scratch-Refactoring durchzuführen .
Natürlich könnte Ihre reale Situation komplizierter sein. Lernen Sie also, sammeln Sie Erfahrungen und seien Sie geduldig. Es dauert Jahre, bis dies richtig ist. Es gibt zwei Bücher, die ich hier empfehlen kann. Vielleicht finden Sie sie hilfreich:
Refactoring von Fowler: Beschreibt einen vollständigen Katalog sehr kleiner Refactorings.
Effektiv mit Legacy-Code von Feathers arbeiten: Gibt ausgezeichnete Ratschläge, wie Sie mit großen Teilen schlecht gestalteten Codes umgehen und ihn in kleineren Schritten besser testen können
quelle
Manchmal sind die beiden besten Designprinzipien KISS * und YAGNI **. Sie müssen nicht jedes bekannte Designmuster in ein Programm packen, das nur "Hallo Welt!" Drucken muss.
Nach Fragenaktualisierung bearbeiten (und teilweise spiegeln, was Pieter B sagt):
Manchmal treffen Sie frühzeitig eine architektonische Entscheidung, die Sie zu einem bestimmten Design führt, das zu allerlei Hässlichkeit führt, wenn Sie versuchen, es umzusetzen. Leider besteht die "richtige" Lösung zu diesem Zeitpunkt darin, einen Schritt zurückzutreten und herauszufinden, wie Sie zu dieser Position gekommen sind. Wenn Sie die Antwort noch nicht sehen können, treten Sie so lange zurück, bis Sie dies tun.
Aber wenn die Arbeit dazu unverhältnismäßig wäre, bedarf es einer pragmatischen Entscheidung, nur die am wenigsten hässliche Problemumgehung für das Problem zu finden.
quelle
Wenn ich in dieser Situation bin, höre ich zuerst auf. Ich wechsle zu einem anderen Problem und arbeite eine Weile daran. Vielleicht eine Stunde, vielleicht ein Tag, vielleicht mehr. Das ist nicht immer eine Option, aber mein Unterbewusstsein wird an Dingen arbeiten, während mein bewusstes Gehirn etwas Produktiveres tut. Schließlich komme ich mit frischen Augen darauf zurück und versuche es erneut.
Eine andere Sache, die ich tue, ist, jemanden zu fragen, der schlauer ist als ich. Dies kann in Form von Fragen zu Stack Exchange, Lesen eines Artikels im Internet zu diesem Thema oder Fragen an einen Kollegen erfolgen, der über mehr Erfahrung in diesem Bereich verfügt. Oft stellt sich heraus, dass die Methode, die ich für den richtigen halte, für das, was ich versuche, völlig falsch ist. Ich habe einen Aspekt des Problems falsch charakterisiert und es passt nicht zu dem Muster, von dem ich denke, dass es es tut. Wenn das passiert, kann es eine große Hilfe sein, wenn jemand anderes sagt: "Weißt du, das sieht eher nach ... aus".
Im Zusammenhang mit dem oben Gesagten steht das Entwerfen oder Debuggen durch Geständnis. Sie gehen zu einem Kollegen und sagen: "Ich werde Ihnen das Problem erklären, das ich habe, und dann werde ich Ihnen die Lösungen erklären, die ich habe. Sie weisen auf Probleme in jedem Ansatz hin und schlagen andere Ansätze vor . " Oft, bevor die andere Person überhaupt spricht, wie ich erkläre, merke ich, dass ein Weg, der anderen gleich zu sein schien, tatsächlich viel besser oder schlechter ist, als ich ursprünglich gedacht hatte. Das resultierende Gespräch kann entweder diese Wahrnehmung verstärken oder auf neue Dinge hinweisen, an die ich nicht gedacht habe.
Also TL; DR: Machen Sie eine Pause, erzwingen Sie sie nicht, bitten Sie um Hilfe.
quelle