Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API is too unflexible/non-OOPish #1

Open
markusj opened this issue May 14, 2012 · 6 comments
Open

API is too unflexible/non-OOPish #1

markusj opened this issue May 14, 2012 · 6 comments

Comments

@markusj
Copy link
Member

markusj commented May 14, 2012

A good API design would consist of an interface as root, allowing to build wrapper-classes around Bitmap24. (Example: For on-the-fly dimension-flipping)

@Koios
Copy link

Koios commented May 15, 2012

More input please.
dimension-flipping?

Bitmap24 btw is not a conventional canvas, but rather a buffer. Since all drawing operations implement IBatchDrawable (it should have been IDrawable, but that would require black voodoo), a list of IBatchDrawable* describes the operations on an empty canvas. These later will be applied to a concrete buffer (picture), that is, a (Batch)Bitmap24. This is why all of these classes shall use RelativeCoordinates.

You're right if you mean some of the member functions should have been virtual. I forgot that (in fact, I intended to do such things with getWidth and getHeight for re-scaling an input area in a next step).
Whether or not we use an additional interface as root I think is of lesser importance, as all of the operations act on pixels -- and turning setPixel into a virtual method would certainly decrease the performance by a great factor (= too much OOP / too flexible). ATM, I think it will be inlined and the for-loops accessing pixels will be optimized heavily.

Operations like rotating or mirroring the picture have not been part of the requirements I made up for the drawing program. In fact, they currently work if they act inside the fixed-size "canvas" (the buffer of the bitmap). Anything else would require additional logic like a variable-sized buffer (remember: the bitmap has alignment issues in the rows).

Please consider simplicity as a main aspect here.

@markusj
Copy link
Member Author

markusj commented May 15, 2012

Whether or not we use an additional interface as root I think is of lesser importance

Of course it is. A simple transformation layer acting as a proxy between Bitmap24 and a simple implementation of the bresenham algorithm could be used to draw every possible line. (The simple bresenham just draws lines with an angle between 0° to 45°)
This would be possible if Bitmap24 had some parent interface from which the proxy could be derived. In fact, many patterns are only possible if there is some well defined interface/superclass. In contrast, those patterns (like proxy) can not be used together with the current API.

@Koios
Copy link

Koios commented May 15, 2012

Warum egtl. auf eng?

Würde das nicht wieder erfordern, das setPixel virtuell ist oder eine virtuelle Methode aufruft? Das wollte ich nämlich explizit vermeiden, und meine aktuellen Tests scheinen in die Richtung zu deuten, dass weder gcc noch MSVC dann gescheit optimieren (könnten). Hingegen die nicht-virtuelle setPixel wird komplett inlined und die Schleife dann weitergehend optimiert.

Wenn ich dich richtig verstehe, soll das Bild sich (die Koordinaten, nicht der Speicherinhalt) drehen können, damit Bresenham besser zu implementieren ist?

@markusj
Copy link
Member Author

markusj commented May 15, 2012

Warum egtl. auf eng?

Keine Ahnung. Warum hast du eine Maske auf? Und bist nicht im Chat?

Das wollte ich nämlich explizit vermeiden, und meine aktuellen Tests scheinen in die Richtung zu deuten, dass weder gcc noch MSVC dann gescheit optimieren (könnten).

Das ist so shice egal. In dem Workshop geht es nicht darum, wie man den Code so hinbiegt, dass $Compiler den gut verdauen kann. Und noch weniger bei der Einführung ins Thema OOP. Wenn der Code 25% langsamer ist: Who cares? Für unseren Anwendungszweck ist es egal.
Ggf. macht es irgendwann Sinn, über die Seiteneffekte bei der Codegenerierung von spezifischen Konstrukten zu reden, aber wohl kaum in den nächsten zwei Monaten.

Wenn ich dich richtig verstehe, soll das Bild sich (die Koordinaten, nicht der Speicherinhalt) drehen können, damit Bresenham besser zu implementieren ist?

Nicht das Bild. Ein Proxy der vor das Bild geschaltet wird. Es ist nicht die Aufgabe des Bildes, Koordinatentransformationen durchzuführen. Dafür gibt es das Proxy-Pattern. Damit könnten zum Beispiel Achsen vertauscht oder skaliert (für Bresenham mit +-1) werden.

Nachtrag: Die Aufruf-Sequenz wäre dann wie folgend gewesen: draw()/apply()/wieauchimmer() macht die Fallunterscheidung und setzt die Transformation auf. Dann wird intern bresenham() aufgerufen und haut die Pixel aufs Bild. Komplett ohne sich Gedanken über die Ausrichtung machen zu müssen.

Kern des Problems ist: Du hast Annahmen darüber gemacht, wie andere die API benutzen. Tatsächlich sollte die API so universell einsetzbar sein wie möglich und dementsprechend möglichst wenige Annahmen über die Verwendung treffen.

@Koios
Copy link

Koios commented May 15, 2012

Nicht das Bild. Ein Proxy der vor das Bild geschaltet wird.
So hatte ich das gemeint mit den Koordinaten.
Dieses spezielle Beispiel ließe sich aber auch mit Proxys vor dem Algorithmus verwirklichen.

Ich sehe aber den Punkt denke ich, und über den Aspekt "tradeoff guter Stil vs. gute Performance" habe ich auch nachgedacht. Hier lässt sich definitiv noch diskutieren.

Eigentlich geht es imho aber um die Definition der Anforderungen als um das Interface selbst. Wenn in den Anforderungen nicht enthalten ist, dass das Bild z.B. Drehungen vollziehen kann, dann ist bspw. ein fester Puffer möglich.
Dem entgegen sehe ich zwei Argumente:

  1. Anforderungen ändern sich im "normalen" Softwarebetrieb häufig, die Verwendung eines root-Interfaces würde die Anpassung vereinfachen.
  2. Drehungen (oder andere konkrete Beispiele) zeigen nicht das tatsächliche Problem auf, nämlich das Festschreiben des Zugriffs auf den Puffer.

Andere Sichtweisen:

  • Bitmap24 ist nur die Implementierung eines Bitmaps mit fester Größe, das Problem wäre also eher, dass die virtuelle Funktion in IBatchDrawable auf Bitmap24 und nicht auf dem besagten root-Interface operiert. (Beachte: In C++ gibt es Mehrfachvererbung auch gemischt von Interfaces und Klassen.)
  • Die Vorgehensweise schien mir im Hinblick auf bereits verwendete Bibliotheken tendenziell konventionell. Die Bibliotheken sind dabei freilich C/C++ Bibliotheken und keine Java/C#.
  • Flexibilität hat ihre Grenzen, und sie könnten an dieser Stelle je nach Anforderungen gezogen werden.

Es kann gut sein (auch wenn ich für eine präzisere Antwort mehr Bibliotheken kennen müsste), dass die Verwendung eines root-Interfaces in rein objektorientierten Sprachen wie Java und C# geläufiger ist, hingegen in C++ weniger (abgesehen von Undingen wie qt). Dass in C++ häufig besser mit einem root-Interface gearbeitet werden sollte, möchte ich dabei nicht in Abrede stellen. Es scheint mir nur bereits auf sprachlicher Ebene eher der Fall zu sein, dass C++ weniger eine streng Paradigmen-orientierte Sprache ist als eine Sprache, die sich sowohl an Paradigmen als auch an der Implementierung orientiert.

Bei der ganzen Geschichte hatte ich zwei Aussagen im Hinterkopf: Die eine stammt von Alexander Stepanov (Erfinder des generischen Programmierens und der STL) die andere von Günter Quast (Prof. am EKP, arbeitet an der Datenanalyse von LHC-Experimenten mit). Beide sagen ungefähr, dass totale Objektorientierung an der Wirklichkeit vorbeigeht. Das ist mit dem Hintergrund zu sehen, dass bei größeren Frameworks mit großer Hierarchie viel Laufzeit für die Objektorientierungs-Mechanismen draufgeht (laut Quast).

Das kann natürlich nur bedingt ein Argument sein, bei einer Vorstellung von Objektorientierung nur halbherzig objektorientiert zu arbeiten. Viel eher hatte ich dabei die Einfachheit im Hinterkopf, dass also nicht zu viel Flexibilität den Einblick in die Funktionsweise oder die Einfachheit der Verwendung verhindert. Daher habe ich an die höchste Stelle der Hierarchie eine möglichst einfache, grundlegende Klasse gestellt, die gerade soviel Implementierung bietet, dass sie nach außen hin sicher ist (zunächst war das Ding nur da zum Abspeichern, aber da möglicherweise BatchBitmap24 von den TNs hätte implementiert werden sollen, mussten die restlichen Teile wie getter/setter noch dran).

Momentan vertrete ich folgende Meinung: Das "Interface" (Bitmap24) ist flexibel genug für alles, was mit dem Zeichenprogramm getan werden soll (möglicherweise bis auf den Fakt, dass getWidth usw. nicht virtuell ist). Es ist definitiv kein Lehrbeispiel für vollendete Objektorientierung oder maximale Flexibilität, sollte aber mit bei den einfachsten und überschaubarsten Varianten sein (für das, was wir damit tun möchten).
Die Vorgehensweise mit Verschachteln sollte in einem WS thematisiert werden, dann aber unter dem Aspekt, dass die TNs darauf basierend Hierarchien schaffen.

Die in der Wirklichkeit verwendete Variante hat denke ich Sven (? Oli?) schon genannt: Eine Canvas-Klassenhierarchie, bei der die Canvas-Implementierung die Primitives anbietet. Das Performance-Problem tritt dann in den Hintergrund, wenn die "internen" (virtuellen) Methoden selbst gewisse Komplexität erreichen. Das ist aber hier bei setPixel usw. nicht der Fall.

@markusj
Copy link
Member Author

markusj commented May 15, 2012

Eigentlich geht es imho aber um die Definition der Anforderungen als um das Interface selbst. Wenn in den Anforderungen nicht enthalten ist, dass das Bild z.B. Drehungen vollziehen kann, dann ist bspw. ein fester Puffer möglich.

Möglich. Aber nicht nötig. Beim Interface-Design liegt der Tradeoff darin, es so flexibel und unbeschränkt wie möglich zu entwerfen, ohne dass dabei die Komplexität überhand nimmt.
In diesem Fall wurde aber noch nicht einmal ein Interface definiert. Damit geht die Flexibilität gegen Null, weil das einzige Werkzeug das mir noch zur Verfügung steht, das Ableiten eigener Klassen ist. Und ein vollständiges Bitmap-Objekt zu erzeugen, nur um wie ein trojanisches Pfed durch seine API zu agieren, geht am Ziel vorbei. (Ich hätte kein Problem damit gehabt, zwei Methoden auf virtual umzustellen, wollte mich aber nicht so dermaßen an der Architektur zu vergehen)
Die Konsequenz ist: Schnittstellendesign ist eine der höchsten Disziplinen im Softwareentwurf. Mit einer vernünftigen Schnittstelle und eine sauber geplanten Hierarchie steht und fällt die Nutzerfreundlichkeit. (Btw.: Eine API ist gut, wenn man sie einfach verwenden kann, nicht wenn sie einfach zu implementieren ist)

Bei der ganzen Geschichte hatte ich zwei Aussagen im Hinterkopf: Die eine stammt von Alexander Stepanov (Erfinder des generischen Programmierens und der STL) die andere von Günter Quast (Prof. am EKP, arbeitet an der Datenanalyse von LHC-Experimenten mit). Beide sagen ungefähr, dass totale Objektorientierung an der Wirklichkeit vorbeigeht. Das ist mit dem Hintergrund zu sehen, dass bei größeren Frameworks mit großer Hierarchie viel Laufzeit für die Objektorientierungs-Mechanismen draufgeht (laut Quast).

Das sind Sonderfälle. Ich fordere auch nicht die totale Objektorientierung (wer wollte noch eine Funktion zum Zeichnen von Linien als Objekt?) sondern mit Augenmaß. Und Fakt ist, dass Code mit schlechter Architektur und schlechter Umsetzung den größten Teil des Wartungsaufwandes bei Softwareprojekten ausmacht. (Und damit, um die Welt jenseits des Elfenbeinturms zu berücksichtigen: Auch der Kosten)

Das kann natürlich nur bedingt ein Argument sein, bei einer Vorstellung von Objektorientierung nur halbherzig objektorientiert zu arbeiten.

Es ist in diesem Kontext überhaupt kein brauchbares Argument. Gerade da sollte man eigentlich vorbildlich Arbeiten.

Viel eher hatte ich dabei die Einfachheit im Hinterkopf, dass also nicht zu viel Flexibilität den Einblick in die Funktionsweise oder die Einfachheit der Verwendung verhindert. Daher habe ich an die höchste Stelle der Hierarchie eine möglichst einfache, grundlegende Klasse gestellt, die gerade soviel Implementierung bietet, dass sie nach außen hin sicher ist.

Schlechte Nachricht: Das ist dir nur zum Teil gelungen. Ich weiß aus dem Channel, dass mindestens eine Person im Praxisteil daran verzweifelt ist, nachzuvollziehen was du von ihr willst und wie die API dazu genutzt werden soll.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants