Moin,
wo kommt in der objektorientierten Programmierung zum Beispiel die
Zeichenfunktion hin. In die Graphikausgabe oder in die Objekte?
Nehmen wir als Beispiel eine 2D-Verkehrssimulation an.
Da gibt es eine GUI, mit Steuerelementen und einer Zeichenfläche für die
Simulation, und die Simulationsobjekte, zum Beispiel Fußgänger als
Kreise dargestellt, Fahrräder als Balken dargestellt und Autos als
Rechtecke dargestellt.
Macht man dann eher in die GUI eine Zeichenfunktion für alles
(Pseudocode)
1
class GUI_canvas
2
{
3
void paint()
4
{
5
for(i:Objekt)
6
{
7
case(Objekt.class)
8
{
9
Fußgänger:
10
drawCircle();
11
Fahrrad:
12
drawLine();
13
Auto:
14
drawRectangle();
15
}
16
}
17
}
18
}
oder bekommt jedes Objekt seine eigene Zeichenfunktion, die beim
Neuzeichnen für jedes Objekt aufgerufen wird?
1
class paintable
2
{
3
void paint();
4
}
5
6
class Fußgänger : paintable
7
{
8
void paint()
9
{
10
drawCircle();
11
}
12
}
13
14
class Fahrrad : paintable
15
{
16
void paint()
17
{
18
drawLine();
19
}
20
}
21
22
class Auto : paintable
23
{
24
void paint()
25
{
26
drawRectangle();
27
}
28
}
Das sind natürlich nur vereinfachte Beispiele. Die Zeichnungen könnten
natürlich deutlich komplizierter sein mit Fallunterscheidungen.
Einerseits würde ich Graphik und Logik trennen, also alles, was mit
Zeichnen zu tun hat, in die Graphik/GUI packen, andererseits wird die
dann sehr groß und vollgepackt mit Zeug, das eigentlich zum Objekt
gehört. Das spräche dann eher für die zweite Variante, damit alles
Objektspezifische beim Objekt bleibt.
ich halte die zweite Variante für besser weil damit generisch gearbeitet
werden kann. Wenn es ein neues Objekt gibt, muss der Canvas das kennen
und damit muss an zwei Stellen geändert werden. Ein generisches for
(I:Objekt) I.paint(); kann alles zeichnen ohne es zu kennen (zweite
Variante).
Aka Polymorphie. Wikipedia bringt genau das auch als Beispiel.
https://de.wikipedia.org/wiki/Polymorphie_(Programmierung)
Ich würde es auch der Logik/Funktion nach aufteilen
//----------------------
// canvas low-level
class CCanvas
void Line
void Circle
void Rectangle
//----------------------
// high level graphic
class CShape
virtual Paint(CCanvas & Canvas)
class ShapeAuto : CShape
virtual Paint(CCanvas & Canvas)
Canvas.Rectangle ...
class ShapeFahrrad : CShape
class ShapeFussgaenger : CShape
//----------------------
// simulation logic
class CSimulationObject
CPaintable Shape;
class Auto : CSimulationObject
class Fahrrad : CSimulationObject
class Fussgaenger :: CSimulationObject
// main/draw
forall (SimOb : AllSimulationObjects)
SimOb.Shape.Paint(Canvas)
Erstmal ist mir ein kleiner Fehler im ersten Beispiel aufgefallen. Es
müsste natürlich heißen:
1
for(i:Objekt)
2
{
3
case(i.class)
4
{
Ich nehme aber an, dass das auch vorher jeder verstanden hätte.
J. S. schrieb:> Ein generisches for> (I:Objekt) I.paint(); kann alles zeichnen ohne es zu kennen (zweite> Variante).
Das würde ich auch so sehen.
J. S. schrieb:> Wenn es ein neues Objekt gibt, muss der Canvas das kennen> und damit muss an zwei Stellen geändert werden.
Meine Überlegung ist, dass man das Argument auch umdrehen könnte. Wenn
man die Graphik ändern will, zum Beispiel von 2D auf 3D, muss man das in
jedem Objekt einzeln machen. Wenn alles in der GUI ist, muss man das nur
einmal machen.
Danke für die Einschätzung.
micha schrieb:> Ich würde es auch der Logik/Funktion nach aufteilen
Also in dem Beispiel drei Klassen? Die High-Level-Klasse übernimmt dann
den 'graphischen Aufbau'/das Zusammensetzen aller Objekte aus den
einfachen Zeichenobjekten?
Das drawXY kann ja auch wieder die Methode eines Canvas sein als
canvas.drawXY(). Den canvas gibt man den Objekt mit damit ganz die
Ausgabe variieren. Also um zB auf verschiedenen Displays zu zeichnen.
Die zentrale Grundidee an der Objektorientierung: Du kannst später eine
Klasse EBike hinzufügen, die ihre eigene paint() Methode hat. Musst die
bestehenden Klassen nie wieder ändern.
Die Frage ist, bleibt es bei Fußgänger, Fahrrad und Auto? Oder trudeln
da immer wieder Erweiterungswünsche ein?
Eigentlich brauchst du Erfahrung, was für Anforderungen später
hinzukommen werden. Damit du von vorn herein die erforderlicher
abstrakten Methoden in den Basisklassen anlegen kannst. Ohne
Überflüssige Methoden und Basisklassen, die nur Verwirrung stiften.
In diesem Fall solltest du einfach mal verschiedene GUI Framewoks
ausprobieren. Da stecken 40 Jahre Erfahrung drin. Das kann man nicht
mehr selbst erfinden.
F. schrieb:> wo kommt in der objektorientierten Programmierung zum Beispiel die> Zeichenfunktion hin. In die Graphikausgabe oder in die Objekte?
Das ist ganz einfach: Beide Varianten sind richtig.
Ja, das ist kein Witz, sondern Deine Frage/Beispiel ist ein Klassiker in
der Frage des OO-Entwurfs.
Man kann wirklich beides machen. Wichtig ist so wissen, dass die beiden
Varianten unterschiedliche Konsequenzen haben:
a.) Ist die Zeichen-Funktionalität separat (Dein erstes Beispiel), dann
sind die Simulations-Objekte unabhängig von der GUI. Du kannst dann
die Simulation mit unterschiedlichen GUIs oder auch ohne verwenden.
Trotzdem kannst Du in der Zeichen-Funktion ein "catch-all" für
neue, unbekannte Klassen basteln, die z.B. einfach einen Punkt
oder etwas ähnliches malen.
b.) Ist die Zeichen-Funktion teil der Simulationobjekte, kannst Du
mitunter beim Zeichnen auf interne Daten der Objekte zugreifen.
Allerdings sind dann die Objekte fest mit der GUI verbunden.
Das ist eine relativ steife Angelegenheit hat aber den Vorteil,
das Änderungen an an der Simulation einfacher sind, weil alles
was ein Objekt betrifft in einer Klasse erledigt wird.
Aus Erfahrung würde ich Variante A wählen, auch wenn es gegen die vor
noch 25 Jahren gelehrte "Prinzipien" der OO "verstößt".
Es ist wichtiger, Abhängigkeiten zu vermeiden bzw. Abhängigkeiten an
einer Stelle zu konzentrieren. Und das geht mit Variante A besser: Du
hast eine GUI und Du hast eine Simulation. Beide wissen nichts
voneinander. Erst durch eine 3. Komponente wird GUI und Simulation
miteinander verbunden.
Dafür ist es etwas mehr Arbeit und die Objekt-Struktur (Vererbung etc.)
wird explizit. Aber auch hier zeigt die Erfahrung, dass große
Klassen-Hierarchien eher nachteilig sind. Sie sind steif und schwer zu
ändern.
Zu diesem Thema werden ganze Bücher gefüllt, das kann man hier nicht
alles kurz wiedergeben. Auch im Klassiker "Gang of Four", wird dieser
Fall diskutiert.
Experte schrieb:> Aus Erfahrung würde ich Variante A wählen, auch wenn es gegen die vor> noch 25 Jahren gelehrte "Prinzipien" der OO "verstößt".
I beg to differ! Beispiel aus der Praxis: Die Simulation selbst soll
unabhängig von plattformspezifischen APIs (Direct X, OpenGL, X-Window
oder was auch immer) sein - nur die GUI darf das. Trotzdem möchte ich
die Polymorphie nutzen, um nicht für jedes neue Objekt im GUI neue
Zeichenmethoden zu implementieren.
Lösung A: Ich abstrahiere, z.B. mit
extern DrawCircle();
extern DrawLine();
...
Die eigentlichen Zeichenroutinen müssen dann erst beim Linken von GUI
und Simulation irgendwie dazukommen.
Lösung B: Ich arbeite mit dem Visitor-Pattern, d.h. jede Klasse hat eine
acceptVisitor-Methode, und da holt man sich dann die für das Zeichnen
nötigen Informationen.
Meiner Meinung nach gehört die Funktion, die die Modelldaten auf die GUI
übersetzt, weder ins Model noch in die GUI, sondern in den Controller.
Such Mal nach "Model View Controller" für mehr Details.
Wie wäre es mit einer Eigenen Klasse für die Aufgabe? Schließlich will
man im realen Leben vermutlich nicht nur einen Kreis mit einem Pixel,
sondern einen Kreis mir n-Pixeln im Abstand r-Pixel com Mittelpunkt m an
der Position x;y-Pixel. (Pixel lässt sich durch eine beliebige Einheit
ersetzen).
Ja mei, wir können noch geschätzt einhundert verschiedene Entwürfe für
das Problem in den Ring werfen...
Björg Strupp oder so schrieb:> Ich abstrahiere, z.B. mit>> extern DrawCircle();> extern DrawLine();
Tja, Klassiker: Ist nämlich keine (bzw. sehr schwache) Abstraktion,
sondern einfach nur eine Verschleierung.
Was machst Du wenn Du z.B. eine Monochrom-GUI hast? Was machst Du, wenn
die GUI gar keine Grafik-Primitive kennt, weil sie z.B. in einer Konsole
läuft? Was machst Du, wenn die GUI ausschließlich mit Sprites arbeitet?
Ach, es gibt so viele Varianten auf dieser Welt...
Vielen Dank für die Antworten. Da habe ich Anhaltspunkte. Ich dachte,
das Problem wäre vielleicht schon gelöst worden.
Dann werde ich mir mal ansehen, wie ich es am Besten umsetze.
Nur aus Interesse, weiß jemand, wie das bei modernen 3D-Computerspielen
gemacht wird?
F. schrieb:> wo kommt in der objektorientierten Programmierung zum Beispiel die> Zeichenfunktion hin. In die Graphikausgabe oder in die Objekte?
Das klassische OO-Problem: Es kollidieren Architekturen.
Die übliche Lösung sind: Interfaces. Die Alternative wäre
Mehrfach-Vererbung, aber die letztlich daraus resultierende Komplexität
ist kaum beherrschbar.
Also: mache deine Klassenhierachie, so wie du sie brauchst und füge den
Klassen, die die Fähigkeit haben müssen, sich zeichnen zu können, ein
entsprechendes Interface (und natürlich dessen Implementierung) hinzu.
Experte schrieb:> Was machst Du, wenn> die GUI gar keine Grafik-Primitive kennt, weil sie z.B. in einer Konsole> läuft?
Ich habe genau den Fall bei mir. Die Simulation kann auch ohne GUI auf
einem Cluster laufen, in einer Konsolenanwendung. Da habe ich dann
void DrawCircle()
{
// Dude, I'm a console app!
}
> Aus Erfahrung würde ich Variante A wählen...
Ja, genau deswegen mache ich diesen Job! In allen anderen Bereichen
schreiben irgendwelche Bürokraten und sonstigen Sesselpuper vor, wie wir
es machen müssen.
Programmierung ist das einzig übrig gebliebene mittelalterliche
Handwerk, in dem die alten Meister aus Erfahrung heraus selbst
entscheiden, wie es gemacht werden muss.
Hay, F.(Gast) die Antwort auf deine Frage: Es gibt keinen Konsens. Es
gibt keine Vorschriften. Es gibt auch keine DIN Normen. Du musst deine
eigenen Erfahrungen sammeln.
F. schrieb:> wo kommt in der objektorientierten Programmierung zum Beispiel die> Zeichenfunktion hin. In die Graphikausgabe oder in die Objekte?
Stroustrup (der C++ Erfinder) hat sich damit intensiv im Buch
"Programming Principles and Practice" beschäftigt.
Seine Lösung: Alle Objekte leiten sich von einer Interfaceklasse
"Shape" ab.
Die virtuelle Interfacefunktion "draw_lines()" wird von den Objekten
überschrieben. Das "Fenster" hat eine Liste von Zeigern auf "Shape"
Objekte, und ruft die virtuelle "draw_lines()" Funktion auf, um sich neu
zu zeichnen.
Das "Fenster" ist wieder eine eigene Klasse, die eine Liste
von Objekten verwaltet. Die Anwendung kann dann Objekte in die Liste
dazufügen oder rausnehmen.
Gruss,
Udo
udok schrieb:> Seine Lösung: Alle Objekte leiten sich von einer Interfaceklasse> "Shape" ab.> Die virtuelle Interfacefunktion "draw_lines()" wird von den Objekten> überschrieben. ...
Das wäre ja meine zweite Variante, wenn ich das richtig verstehe (außer,
dass ich nicht Shape und drawLines habe, sondern paintable und paint()
).
Auch noch danke für die weiteren Antworten.
F. schrieb:> Das wäre ja meine zweite Variante, wenn ich das richtig verstehe (außer,> dass ich nicht Shape und drawLines habe, sondern paintable und paint()
Im Prinzip ja, nur das paintable::paint() eine virtuelle Funktion ist.
Das GUI sieht nur eine Liste/Vektor von paintable Objekten, und
verwendet die paintable::paint() zum Zeichnen. Das GUI weiss aber nicht
ob es sich um
einen Fussgänger oder ein Auto handelt.
Das GUI ist damit von der konkreten Implementierung der
Simulations-Objekte
entkoppelt. Es können einfach neue Objekte dazugefügt werden, ohne dass
der GUI Code geändert werden muss.
udok schrieb:> F. schrieb:>> Das wäre ja meine zweite Variante, wenn ich das richtig verstehe (außer,>> dass ich nicht Shape und drawLines habe, sondern paintable und paint()>> Im Prinzip ja, nur das paintable::paint() eine virtuelle Funktion ist.
So war das von mir auch gemeint, das ist nur dem Pseudocode zum Opfer
gefallen. In C++ hätte ich paintable::paint() virtuell gemacht, in Java
wäre paintable ein Interface geworden.