Wie vermeiden Sie endloses Durchlaufen gleichermaßen suboptimaler Designs?

10

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.
Jonathan
quelle
3
"Abonnieren Sie nicht die OO-Philosophie". Es ist ein Werkzeug, keine wichtige Lebensentscheidung. Verwenden Sie es, wenn es hilft, und verwenden Sie es nicht, wenn es nicht hilft.
user253751
Wenn Sie nicht in bestimmten Mustern denken können, versuchen Sie vielleicht, ein mäßig komplexes Projekt in C zu schreiben. Es wird interessant sein zu erfahren, wie viel Sie ohne diese Muster tun können.
user253751
2
Oh C hat Muster. Sie sind nur sehr unterschiedliche Muster. :)
candied_orange
Ich habe die meisten dieser Fehlinterpretationen in der Bearbeitung geklärt
Jonathan
Es ist normal, dass diese Probleme gelegentlich auftreten. Ein häufiges Auftreten sollte nicht der Fall sein. Höchstwahrscheinlich verwenden Sie das falsche Programmierparadigma für das Problem oder Ihre Entwürfe sind eine Mischung aus Paradigmen an den falschen Stellen.
Dunk

Antworten:

8

Wenn ich zu einer so schwierigen Entscheidung komme, stelle ich mir normalerweise drei Fragen:

  1. Was sind die Vor- und Nachteile aller verfügbaren Lösungen?

  2. Gibt es eine Lösung, über die ich noch nicht nachgedacht habe?

  3. 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.

cmaster - Monica wieder einsetzen
quelle
Ich könnte dies als Antwort markieren, dies ist für mich am sinnvollsten und es scheint ein Konsens darüber zu bestehen, das Problem selbst einfach neu zu bewerten.
Jonathan
Ich mag auch, wie Sie es in Schritte unterteilt haben, so dass es eine Art loses Verfahren zur Beurteilung der Situation gibt.
Jonathan
OP, ich sehe, dass Sie in der Mechanik der Codelösung stecken, also können Sie "diese Antwort genauso gut markieren". Der beste Code, den wir geschrieben haben, war nach einer sehr sorgfältigen Analyse der Anforderungen, abgeleiteten Anforderungen, Klassendiagramme, Klasseninteraktionen usw. Gott sei Dank haben wir uns allen Zwischenfällen durch die billigen Sitze widersetzt (lesen Sie Management & Mitarbeiter): "Sie Ich verbringe zu viel Zeit mit Design "," beeil dich und hol dir das Codieren! "," das sind zu viele Klassen! "usw. Sobald wir dazu gekommen sind, war das Codieren eine Freude - mehr als Spaß. Objektiv war das der beste Code im gesamten Projekt.
Radarbob
13

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:

Dies wird sehr schwierig, wenn jeder Ansatz so viele Probleme zu haben scheint wie jeder andere.

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 .

Es ist auch schwierig zu akzeptieren, dass der Code sehr problematisch wird, unabhängig davon, welches Entwurfsmuster oder welcher Ansatz verwendet wird.

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.

Dies führt zu einem Motivationsverlust, da "sauberer" Code manchmal keine Option mehr ist.

Perfekt ist der Feind des Guten. Lass etwas funktionieren und überarbeite es dann.

Gibt es Designansätze, die dazu beitragen können, dieses Problem zu minimieren? Gibt es eine akzeptierte Praxis für das, was man in einer solchen Situation tun sollte?

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.

Telastyn
quelle
Ich dachte nur, ich sollte sagen, dass ich einige Fehlinterpretationen in der Bearbeitung behoben habe. Ich mache im Allgemeinen den "Get it Working" - und Refactor-Ansatz. Dies führt jedoch meiner Erfahrung nach häufig zu einer hohen technischen Verschuldung. Wenn ich zum Beispiel nur eine Sache zum Laufen bringe, aber ich selbst oder andere darauf aufbauen, können einige dieser Dinge unvermeidbare Abhängigkeiten aufweisen, die dann auch eine Menge Refactoring erfordern. Natürlich kann man das nicht immer im Voraus wissen. Aber wenn Sie bereits wissen, dass der Ansatz fehlerhaft ist, sollten Sie ihn zum Laufen bringen und dann iterativ umgestalten, bevor Sie auf dem Code aufbauen?
Jonathan
@ Jonathan - alle Designs sind eine Reihe von Kompromissen. Ja, Sie sollten sofort iterieren, wenn das Design fehlerhaft ist und Sie eine klare Möglichkeit haben, es zu verbessern. Wenn es keine klare Verbesserung gibt, hör auf zu ficken.
Telastyn
"Sie haben ihre Verantwortung auf eine Weise gekürzt, die für das vorliegende Problem unnatürlich ist. Keine gute Option kann der Geruch sein, dass etwas grundlegend falsch mit Ihrem Ansatz ist." Ich würde darauf wetten. Einige Entwickler stoßen nie auf die im OP beschriebenen Probleme, andere scheinen dies ziemlich häufig zu tun. Oft ist die Lösung nicht leicht zu erkennen, wenn sie vom ursprünglichen Entwickler gestellt wird, da er das Design bereits in der Art und Weise voreingenommen hat, wie er das Problem umrahmt. Manchmal ist der einzige Weg, um die ganz offensichtliche Lösung zu finden, den Weg zurück zu den Anforderungen für das Design.
Dunk
8

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).

Pieter B.
quelle
4

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

Doc Brown
quelle
1
Das ist auch sehr hilfreich. Kleines schrittweises oder Scratch-Refactoring ist eine gute Idee, da es dann zusammen mit anderen Arbeiten schrittweise abgeschlossen werden kann, ohne es zu sehr zu behindern. Ich wünschte, ich könnte mehrere Antworten genehmigen!
Jonathan
1

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.

 * Keep It Simple, Stupid
 ** You Ain't Gonna Need It

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.

Simon B.
quelle
Ich habe einige davon in der Bearbeitung geklärt, aber im Allgemeinen beziehe ich mich genau auf die Probleme, bei denen es keine einfache Lösung gibt.
Jonathan
Ah schön, ja, es scheint ein Konsens darüber zu bestehen, zurückzutreten und das Problem neu zu bewerten. Das ist eine gute Idee.
Jonathan
1

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.

user1118321
quelle