Was ist die beste Vorgehensweise beim Schreiben von Klassen, die möglicherweise etwas über die Benutzeroberfläche wissen müssen? Würde eine Klasse, die selbst zeichnen kann, nicht einige bewährte Methoden brechen, da dies von der Benutzeroberfläche (Konsole, GUI usw.) abhängt?
In vielen Programmierbüchern bin ich auf das Beispiel "Shape" gestoßen, das die Vererbung zeigt. Die Basisklassenform verfügt über eine draw () -Methode, mit der jede Form, z. B. ein Kreis und ein Quadrat, überschrieben wird. Dies ermöglicht Polymorphismus. Aber ist die draw () -Methode nicht sehr stark von der Benutzeroberfläche abhängig? Wenn wir diese Klasse beispielsweise für Win Forms schreiben, können wir sie nicht für eine Konsolen- oder Webanwendung wiederverwenden. Ist das richtig?
Der Grund für die Frage ist, dass ich immer wieder festgefahren bin und mich nicht sicher bin, wie ich Klassen verallgemeinern kann, damit sie am nützlichsten sind. Dies wirkt sich tatsächlich gegen mich aus und ich frage mich, ob ich es "zu sehr versuche".
Shape
Klasse schreiben , schreiben Sie wahrscheinlich den Grafikstapel selbst und nicht einen Client in den Grafikstapel.Antworten:
Das hängt von der Klasse und dem Anwendungsfall ab. Ein visuelles Element, das weiß, wie man sich selbst zeichnet, ist nicht unbedingt ein Verstoß gegen das Prinzip der Einzelverantwortung.
Wieder nicht unbedingt. Wenn Sie eine Schnittstelle erstellen können (drawPoint, drawLine, set Color usw.), können Sie praktisch jeden Kontext zum Zeichnen von Objekten an die Form übergeben, z. B. im Konstruktor der Form. Dies würde es Formen ermöglichen, sich selbst auf einer Konsole oder einer gegebenen Zeichenfläche zu zeichnen.
Nun, das stimmt. Wenn Sie ein UserControl (im Allgemeinen keine Klasse) für Windows Forms schreiben, können Sie es nicht mit einer Konsole verwenden. Das ist aber kein Problem. Warum sollte ein UserControl für Windows Forms für jede Art von Präsentation geeignet sein? Das UserControl sollte eines tun und es gut machen. Es ist per Definition an eine bestimmte Darstellungsform gebunden. Am Ende braucht der Benutzer etwas Konkretes und keine Abstraktion. Dies mag für Frameworks nur teilweise zutreffen, für Endbenutzeranwendungen jedoch.
Die Logik dahinter sollte jedoch entkoppelt sein, damit Sie sie wieder mit anderen Präsentationstechnologien verwenden können. Führen Sie bei Bedarf Schnittstellen ein, um die Orthogonalität für Ihre Anwendung aufrechtzuerhalten. Die allgemeine Regel lautet: Die konkreten Dinge sollten mit anderen konkreten Dingen austauschbar sein.
Sie wissen, extreme Programmierer lieben ihre YAGNI- Einstellung. Versuchen Sie nicht, alles allgemein zu schreiben, und geben Sie sich nicht zu viel Mühe, um alles zu einem allgemeinen Zweck zu machen. Dies wird als Überentwicklung bezeichnet und führt schließlich zu völlig verschlungenem Code. Geben Sie jeder Komponente genau eine Aufgabe und vergewissern Sie sich, dass sie gut funktioniert. Fügen Sie bei Bedarf Abstraktionen ein, bei denen Sie Änderungen erwarten (z. B. Schnittstelle für den Zeichnungskontext, wie oben angegeben).
Im Allgemeinen sollten Sie beim Schreiben von Geschäftsanwendungen immer versuchen, Dinge zu entkoppeln. MVC und MVVM eignen sich hervorragend, um die Logik von der Präsentation zu entkoppeln, sodass Sie sie für eine Webpräsentation oder eine Konsolenanwendung wiederverwenden können. Denken Sie daran, dass am Ende einige Dinge konkret sein müssen. Ihre Benutzer können nicht mit einer Abstraktion arbeiten, sie brauchen etwas Konkretes. Abstraktionen sind nur eine Hilfe für Sie als Programmierer, um den Code erweiterbar und wartbar zu halten. Sie müssen sich überlegen, wo Sie Ihren Code brauchen, um flexibel zu sein. Schließlich müssen alle Abstraktionen etwas Konkretes hervorbringen.
Bearbeiten: Wenn Sie mehr über Architektur- und Designtechniken erfahren möchten, die Best Practices bieten, empfehlen wir Ihnen, die Antwort von @Catchops und SOLID- Praktiken auf Wikipedia zu lesen .
Außerdem empfehle ich für den Anfang immer das folgende Buch: Head First Design Patterns . Es wird Ihnen helfen, Abstraktionstechniken / OOP-Entwurfspraktiken besser zu verstehen als das GoF-Buch (was ausgezeichnet ist, es ist einfach nicht für Anfänger geeignet).
quelle
Sie haben definitiv recht. Und nein, du versuchst es einfach nur gut :)
Lesen Sie mehr über das Prinzip der einmaligen Verantwortung
Ihre innere Arbeitsweise in der Klasse und die Art und Weise, wie diese Informationen dem Benutzer präsentiert werden sollen, sind zwei Verantwortlichkeiten.
Haben Sie keine Angst, den Unterricht zu entkoppeln. Selten ist das Problem zu viel Abstraktion und Entkopplung :)
Zwei sehr relevante Muster sind Model-View-Controller für Webanwendungen und Model View ViewModel für Silverlight / WPF.
quelle
Ich benutze MVVM häufig und meiner Meinung nach sollte eine Business-Objektklasse nie etwas über die Benutzeroberfläche wissen müssen. Sicher, sie müssen möglicherweise das
SelectedItem
, oderIsChecked
oderIsVisible
usw. kennen, aber diese Werte müssen nicht an eine bestimmte Benutzeroberfläche gebunden sein und können generische Eigenschaften der Klasse sein.Wenn Sie etwas an der Schnittstelle im Code tun müssen, z. B. den Fokus einstellen, eine Animation ausführen, Hotkeys verwalten usw., sollte der Code Teil des Code-Behind der Benutzeroberfläche sein und nicht Ihre Geschäftslogikklassen.
Also würde ich sagen, hören Sie nicht auf, zu versuchen, Ihre Benutzeroberfläche und Ihre Klassen zu trennen. Je entkoppelter sie sind, desto einfacher können sie gewartet und getestet werden.
quelle
Es gibt eine Reihe von bewährten Designmustern, die im Laufe der Jahre entwickelt wurden, um genau das zu erreichen, wovon Sie sprechen. Andere Antworten auf Ihre Frage beziehen sich auf das Prinzip der einheitlichen Verantwortlichkeit - das absolut gültig ist - und auf das, was Ihre Frage anzutreiben scheint. Dieser Grundsatz besagt einfach, dass eine Klasse eine Sache GUT tun muss. Mit anderen Worten, die Kohäsion erhöhen und die Kopplung verringern - das ist es, was gutes objektorientiertes Design ausmacht - macht eine Klasse eine Sache gut und hat nicht viele Abhängigkeiten von anderen.
Nun ... Sie haben Recht, wenn Sie einen Kreis auf einem iPhone zeichnen möchten, ist dies ein Unterschied zum Zeichnen eines Kreises auf einem PC, auf dem Windows ausgeführt wird. Sie MÜSSEN (in diesem Fall) eine konkrete Klasse haben, die einen Brunnen auf dem iPhone zeichnet, und eine andere, die einen Brunnen auf einem PC zeichnet. Dies ist der Grundbestandteil der Vererbung, den all diese Formenbeispiele auflösen. Sie können es einfach nicht mit Vererbung allein tun.
Hier kommen Schnittstellen ins Spiel - wie es in dem Buch der Viererbande heißt (MUSS GELESEN WERDEN) -. Ziehen Sie die Implementierung immer der Vererbung vor. Verwenden Sie mit anderen Worten Schnittstellen, um eine Architektur zusammenzustellen, die verschiedene Funktionen auf vielfältige Weise ausführen kann, ohne sich auf fest codierte Abhängigkeiten zu verlassen.
Ich habe Hinweise auf die SOLID- Prinzipien gesehen. Das sind großartig. Das "S" ist das Prinzip der Einzelverantwortung. ABER das 'D' steht für Dependency Inversion. Hier kann die Inversion des Kontrollmusters (Dependency Injection) verwendet werden. Es ist sehr leistungsfähig und kann verwendet werden, um die Frage zu beantworten, wie ein System aufgebaut werden kann, das einen Kreis sowohl für ein iPhone als auch für einen PC zeichnen kann.
Mit diesen Konstrukten kann eine Architektur erstellt werden, die allgemeine Geschäftsregeln und Datenzugriff enthält, jedoch verschiedene Implementierungen von Benutzeroberflächen enthält. Es ist jedoch sehr hilfreich, tatsächlich in einem Team zu sein, das es implementiert hat, und es in Aktion zu sehen, um es wirklich zu verstehen.
Dies ist nur eine schnelle allgemeine Antwort auf eine Frage, die eine genauere Antwort verdient. Ich ermutige Sie, sich eingehender mit diesen Mustern zu befassen. Etwas konkretere Implementierungen dieser Muster sind unter den bekannten Namen MVC und MVVM zu finden.
Viel Glück!
quelle
In diesem Fall können Sie weiterhin MVC / MVVM verwenden und verschiedene UI-Implementierungen über die gemeinsame Schnittstelle einspeisen:
Auf diese Weise können Sie Ihre Controller- und Model-Logik wiederverwenden und gleichzeitig neue GUI-Typen hinzufügen.
quelle
Dafür gibt es verschiedene Muster: MVP, MVC, MVVM usw.
Ein schöner Artikel von Martin Fowler (großer Name) zum Lesen ist GUI Architectures: http://www.martinfowler.com/eaaDev/uiArchs.html
MVP wurde noch nicht erwähnt, aber es ist definitiv zu erwähnen: Schauen Sie es sich an.
Es ist das von den Entwicklern von Google Web Toolkit vorgeschlagene Muster, es ist wirklich ordentlich.
Hier finden Sie echten Code, echte Beispiele und Gründe, warum dieser Ansatz nützlich ist:
http://code.google.com/webtoolkit/articles/mvp-architecture.html
http://code.google.com/webtoolkit/articles/mvp-architecture-2.html
Einer der Vorteile dieses oder eines ähnlichen Ansatzes, der hier nicht genug betont wurde, ist die Testbarkeit! In vielen Fällen würde ich sagen, das ist der Hauptvorteil!
quelle
Dies ist einer der Orte , an denen OOP nicht einen guten Job bei Abstraktion zu tun. Der OOP-Polymorphismus verwendet den dynamischen Versand über eine einzelne Variable ('this'). Wenn der Polymorphismus auf Shape basiert, können Sie ihn nicht polymorph auf dem Renderer (Konsole, GUI usw.) verteilen.
Stellen Sie sich ein Programmiersystem vor, das zwei oder mehr Variablen verarbeiten kann:
Angenommen, das System bietet Ihnen die Möglichkeit, poly_draw für verschiedene Kombinationen von Shape-Typen und Renderer-Typen auszudrücken. Es wäre dann einfach, eine Klassifizierung von Formen und Renderern zu finden, oder? Die Typprüfung würde Ihnen irgendwie helfen, herauszufinden, ob es Form- und Rendererkombinationen gibt, deren Implementierung Sie möglicherweise verpasst haben.
Die meisten OOP-Sprachen unterstützen nichts wie das oben Genannte (einige tun es, aber sie sind kein Mainstream). Ich würde vorschlagen, dass Sie sich das Besuchermuster ansehen, um dieses Problem zu umgehen.
quelle
Oben klingt für mich richtig. Nach meinem Verständnis bedeutet dies eine relativ enge Kopplung zwischen Controller und View in Bezug auf das MVC-Entwurfsmuster. Dies bedeutet auch, dass man zum Umschalten zwischen Desktop-Konsole und Web-App sowohl Controller als auch View als Paar entsprechend umschalten muss - das einzige Modell bleibt unverändert.
Nun, meine derzeitige Auffassung ist, dass diese View-Controller-Kupplung, von der wir sprechen, in Ordnung ist und vor allem im modernen Design eher im Trend liegt .
Vor ein oder zwei Jahren fühlte ich mich auch unsicher. Ich habe meine Meinung geändert, nachdem ich im Sun-Forum über Muster und OO-Design diskutiert hatte.
Wenn Sie interessiert sind, probieren Sie dieses Forum aus - es wurde jetzt auf Oracle migriert ( Link ). Wenn Sie dort ankommen , versuchen Sie, Saish anzupingen - damals erwiesen sich seine Erklärungen zu diesen heiklen Dingen als äußerst hilfreich für mich. Ich kann allerdings nicht sagen, ob er noch dabei ist - ich selbst war schon eine ganze Weile nicht mehr dort
quelle
Aus pragmatischer Sicht muss ein Code in Ihrem System wissen, wie man so etwas zeichnet,
Rectangle
wenn dies für den Benutzer erforderlich ist. Und das wird sich irgendwann auf wirklich einfache Dinge wie das Rastern von Pixeln oder das Anzeigen von Inhalten in einer Konsole auswirken.Aus Sicht der Kopplung stellt sich für mich die Frage, wer / was von dieser Art von Informationen abhängen soll und in welchem Detailgrad (wie abstrakt zB)?
Abstrakte Zeichen- / Renderfunktionen
Denn wenn der übergeordnete Zeichnungscode nur von etwas sehr Abstraktem abhängt, funktioniert diese Abstraktion möglicherweise (durch Substitution konkreter Implementierungen) auf allen Plattformen, auf die Sie abzielen möchten. Als konstruiertes Beispiel
IDrawer
könnte eine sehr abstrakte Schnittstelle in der Lage sein, sowohl in Konsolen- als auch in GUI-APIs implementiert zu werden, um Dinge wie Plotformen auszuführen (die Konsolenimplementierung könnte die Konsole wie ein "Bild" von 80 x N mit ASCII-Grafik behandeln). Natürlich ist das ein erfundenes Beispiel, da Sie eine Konsolenausgabe normalerweise nicht wie einen Bild- / Bildpuffer behandeln möchten. In der Regel erfordern die meisten Benutzeranforderungen mehr textbasierte Interaktionen in Konsolen.Eine andere Überlegung ist, wie einfach es ist, eine stabile Abstraktion zu entwerfen. Weil es einfach sein könnte, wenn Sie nur moderne GUI-APIs als Ziel verwenden, um die grundlegenden Funktionen zum Zeichnen von Formen wie Zeichnen von Linien, Rechtecken, Pfaden, Text und ähnlichen Dingen zu abstrahieren (nur einfache 2D-Rasterung einer begrenzten Menge von Grundelementen). , mit einer abstrakten Schnittstelle, die mit geringem Aufwand über verschiedene Subtypen einfach für alle implementiert werden kann. Wenn Sie eine solche Abstraktion effektiv entwerfen und auf allen Zielplattformen implementieren können, dann würde ich sagen, dass es für eine Form- oder GUI-Steuerung oder was auch immer ein weitaus geringeres Übel ist, wenn Sie wissen, wie Sie sich selbst mit einer solchen zeichnen Abstraktion.
Angenommen, Sie versuchen, die blutigen Details zu beseitigen, die zwischen einer Playstation Portable, einem iPhone, einer XBox One und einem leistungsstarken Gaming-PC variieren, während Sie die modernsten Echtzeit-3D-Rendering- / Shading-Techniken für jede einzelne verwenden müssen . In diesem Fall ist es fast sicher, dass der Versuch, eine einzige abstrakte Benutzeroberfläche zu entwickeln, um die Rendering-Details zu abstrahieren, wenn die zugrunde liegenden Hardwarefunktionen und APIs so stark variieren, zu einem enormen Zeitaufwand beim Entwerfen und erneuten Entwerfen führt Entdeckungen und ebenso eine Lösung mit dem kleinsten gemeinsamen Nenner, bei der die volle Einzigartigkeit und Leistungsfähigkeit der zugrunde liegenden Hardware nicht ausgenutzt wird.
Abhängigkeiten fließen in Richtung stabiler, "einfacher" Designs
Auf meinem Gebiet bin ich in diesem letzten Szenario. Wir zielen auf viele verschiedene Hardware mit radikal unterschiedlichen zugrunde liegenden Fähigkeiten und APIs ab, und der Versuch, eine Rendering- / Zeichnungsabstraktion zu entwickeln, um sie alle zu beherrschen, ist grenzenlos hoffnungslos (wir könnten weltberühmt werden, wenn wir das nur so effektiv tun, als wäre es ein Spiel Wechsler in der Industrie). Also das letzte , was ich in meinem Fall möge wie der analogical ist
Shape
oderModel
oderParticle Emitter
das weiß , wie man sich ziehen, auch wenn es , dass in der höchsten Ebene zeichnen ausdrückt und abstrakteste Weise möglich ...... weil diese Abstraktionen zu schwer zu entwerfen sind und wenn es schwierig ist, ein Design zu erhalten und alles davon abhängt, ist dies ein Rezept für die kostspieligsten zentralen Designänderungen, die alles davon abhängig kräuseln und zerbrechen. Das Letzte, was Sie möchten, ist, dass die Abhängigkeiten in Ihren Systemen zu stark in Richtung abstrakter Designs fließen, um korrekt zu werden (zu stark, um ohne aufdringliche Änderungen stabilisiert zu werden).
Schwierig hängt von leicht ab, nicht leicht hängt von schwer ab
Stattdessen lassen wir die Abhängigkeiten in Richtung einfach zu gestaltender Dinge fließen. Es ist viel einfacher, ein abstraktes "Modell" zu entwerfen, das sich nur auf das Speichern von Dingen wie Polygonen und Materialien konzentriert, und dieses Design korrekt zu gestalten, als einen abstrakten "Renderer" zu entwerfen, der effektiv (durch austauschbare konkrete Subtypen) für Service-Zeichnungen implementiert werden kann fordert einheitlich Hardware an, die so unterschiedlich ist wie eine PSP von einem PC.
Wir kehren also die Abhängigkeiten von den Dingen um, die schwer zu entwerfen sind. Anstatt abstrakte Modelle dazu zu bringen, sich auf ein abstraktes Renderer-Design zu beziehen, von dem sie alle abhängig sind (und ihre Implementierungen zu unterbrechen, wenn sich dieses Design ändert), haben wir stattdessen einen abstrakten Renderer, der weiß, wie jedes abstrakte Objekt in unserer Szene gezeichnet wird ( Modelle, Partikelemitter usw.), und so können wir dann einen OpenGL-Renderer-Subtyp für PCs wie
RendererGl
, einen anderen für PSPs wieRendererPsp
, einen anderen für Mobiltelefone usw. implementieren . In diesem Fall fließen die Abhängigkeiten in Richtung stabiler Designs. Vom Renderer zu verschiedenen Arten von Objekten (Modellen, Partikeln, Texturen usw.) in unserer Szene, nicht umgekehrt.Wenn Sie versuchen, etwas auf einer zentralen Ebene Ihrer Codebasis zu abstrahieren, und es einfach zu schwierig ist, es zu entwerfen, anstatt hartnäckig gegen die Wände zu schlagen und ständig aufdringliche Änderungen daran vorzunehmen, die das Aktualisieren von 8.000 Quelldateien erfordern, weil es sich um solche handelt Mein erster Vorschlag ist, die Abhängigkeiten zu invertieren. Sehen Sie, ob Sie den Code so schreiben können, dass das, was so schwer zu entwerfen ist, von allem abhängt, was einfacher zu entwerfen ist, und nicht von den Dingen, die einfacher zu entwerfen sind, je nachdem, was so schwer zu entwerfen ist. Beachten Sie, dass es sich um Designs handelt (speziell um Schnittstellendesigns) und nicht um Implementierungen: Manchmal sind die Dinge einfach zu entwerfen und schwer zu implementieren. und manchmal sind Dinge schwer zu entwerfen, aber einfach zu implementieren. Abhängigkeiten fließen in Richtung Designs, daher sollte der Fokus nur darauf liegen, wie schwierig es ist, hier etwas zu entwerfen, um die Richtung zu bestimmen, in die Abhängigkeiten fließen.
Grundsatz der einheitlichen Verantwortung
Für mich ist SRP hier normalerweise nicht so interessant (obwohl es vom Kontext abhängt). Ich meine, es gibt eine Gratwanderung beim Entwerfen von Dingen, die eindeutig und wartbar sind, aber Ihre
Shape
Objekte müssen möglicherweise detailliertere Informationen anzeigen, wenn sie beispielsweise nicht wissen, wie sie sich selbst zeichnen sollen, und es gibt möglicherweise nicht viele sinnvolle Dinge dafür Mit einer Form in einem bestimmten Verwendungskontext zu tun, als sie zu konstruieren und zu zeichnen. Es gibt Kompromisse mit so gut wie allem, und es hängt nicht mit SRP zusammen, das die Dinge darauf aufmerksam machen kann, wie man sich selbst in der Lage macht, in bestimmten Kontexten nach meiner Erfahrung ein solcher Alptraum für die Instandhaltung zu werden.Es hat viel mehr mit der Kopplung und der Richtung zu tun, in die Abhängigkeiten in Ihrem System fließen. Wenn Sie versuchen, eine abstrakte Rendering-Oberfläche, von der alles abhängt (weil sie sich selbst damit zeichnet), auf eine neue Ziel-API / -Hardware zu portieren, und feststellen, dass Sie ihr Design erheblich ändern müssen, damit sie dort effektiv funktioniert Dies ist eine sehr kostspielige Änderung, die das Ersetzen von Implementierungen von allem in Ihrem System erfordert, das weiß, wie man sich selbst zeichnet. Und das ist das praktischste Wartungsproblem, das mir begegnet, wenn ich weiß, wie man sich selbst zeichnet, wenn dies zu einer Schiffsladung von Abhängigkeiten führt, die in Richtung von Abstraktionen fließen, die zu schwierig sind, im Voraus richtig zu entwerfen.
Entwickler-Stolz
Ich erwähne diesen einen Punkt, weil er meiner Erfahrung nach oft das größte Hindernis ist, die Abhängigkeitsrichtung auf Dinge zu lenken, die einfacher zu gestalten sind. Für Entwickler ist es sehr einfach, hier ein wenig ehrgeizig zu werden und zu sagen: "Ich werde die plattformübergreifende Rendering-Abstraktion so gestalten, dass sie alle beherrschen. Ich werde lösen, was andere Entwickler monatelang für die Portierung aufwenden, und ich werde bekommen." es stimmt und es wird auf jeder einzelnen Plattform, die wir unterstützen, wie von Zauberhand funktionieren und auf jeder die neuesten Rendering-Techniken anwenden; ich habe es mir bereits in meinem Kopf vorgestellt. "In diesem Fall widersetzen sie sich der praktischen Lösung, das zu vermeiden, und kehren einfach die Richtung der Abhängigkeiten um und übersetzen die möglicherweise enorm kostspieligen und wiederkehrenden zentralen Entwurfsänderungen in einfach billige und lokal wiederkehrende Änderungen an der Implementierung. Es muss eine Art "weiße Fahne" -Instinkt in den Entwicklern geben, um aufzugeben, wenn es zu schwierig ist, etwas so abstrakt zu gestalten, und ihre gesamte Strategie zu überdenken, da sie sonst großen Kummer und Schmerzen ausgesetzt sind. Ich würde vorschlagen, solche Ambitionen und den Kampfgeist auf modernste Implementierungen zu übertragen, die einfacher zu entwerfen sind, als solche weltberühmenden Ambitionen auf die Ebene des Interface-Designs zu bringen.
quelle
OpenGLBillboard
, würdest du ein machenOpenGLRenderer
, das weiß, wie man irgendeine Art zeichnetIBillBoard
? Aber würde es das tun, indem es Logik an delegiertIBillBoard
, oder würde es riesige Schalter oderIBillBoard
Typen mit Bedingungen haben? Das ist, was ich schwer zu begreifen finde, denn das scheint überhaupt nicht wartbar zu sein!RendererPsp
die die Abstraktionen Ihrer Spieleszene auf höchster Ebene kennt, kann sie all die Magie und Rückschritte ausführen, die erforderlich sind, um solche Dinge auf eine Weise zu rendern, die auf der PSP überzeugend aussieht ...BillBoard
es wirklich schwierig ist, Objekte auf niedriger Ebene, wie z. B. ein, von diesen abhängig zu machen? Während aIRenderer
bereits ein hohes Niveau aufweist und mit viel weniger Aufwand von diesen Bedenken abhängen könnte?