Top-Down oder Bottom-Up beim Programmieren?
-
Gregor schrieb:
@pointercrash(): Wie gesagt: Ich habe diesen Thread eröffnet, weil ich da bei mir ein vermutlich suboptimales Vorgehen bei der Programmierung festgestellt habe. Ich habe mir auch noch keine wirklich tiefgehenden Gedanken bezüglich des Themas dieses Threads gemacht. Die Antworten, die es in diesem Thread gab, waren für mich aber sehr interessant und ich finde, als Threadersteller muss man so etwas auch irgendwo zum Ausdruck bringen. Und wenn mehrfach ein bestimmter Ansatz genannt wird, der mich etwas überrascht, dann sag ich auch, was mich bezüglich dieses Ansatzes wundert. In dem Zusammenhang verstehe ich zumindest nicht, was das mit "labern" zu tun hat.
Ganz im Ernst, in diesem Unterforum posten mehr Leute, die das letzte mal allenfalls ihren Videorecorder programmiert und sonst mit dem Thema wenig am Hut haben.
Das Thema war quasi eine Steilvorlage für Leutchen wie Prof84, hier einen geistigen Feuchtfurz reinzudrücken.Da hat's mich natürlich schon interessiert, ob Du da überhaupt mal was gemacht hast - sorry, wenn die Nachfrage schroff rüberkam.
Der Mischentwurf wird ja jetzt wohl Gewinner sein, da kann ich noch was nachreichen, wenn Dich das Thema wirklich interessiert:
FORTH ist z.B. nicht nur eine Sprache, sondern zusätzlich ein Betriebssystem plus Entwicklungsumgebung, die eigentlich nur Bottom- Up unterstützt. Wie man trotzdem Top-Down- Konzept und Bottom- Up- Design verbindet, bzw. die Limits des Top- Down- Entwurfs sind in dem Klassiker Thinking FORTH im Analysis- Teil recht gut dargelegt.Das evolutionäre Credo dort lautet:
Start simple. Get it running. Learn what you're trying to do. Add complexity gradually, as needed to fit the requirements and constraints. Don't be afraid to restart from scratch.
Der letzte Satz ist eigentlich der wichtigste - wenn Du irgendwann merkst, daß Du im Konzept was vergeigt hast, fang' lieber neu an, als es mit dem Abrichthammer zurechtzudengeln zu versuchen - das führt zu immer mehr Problemen mit einem Lösungsversuch, den Du eh' nicht mehr vertrittst.
-
byto schrieb:
Das definieren der Testfälle ist ja nicht das Problem (macht man indirekt beim manuellen Testen auch -> Debugger, Logs, Oberfläche).
Oh ja. Das definieren der Testfälle ist ein großes Problem. Zumindest ist es bei dem Beispiel mit dem Ball Pivoting ein großes Problem. Der Ball Pivoting Algorithmus hat als Eingabe grob gesehen eine Punktwolke, aus der er dann eine Oberfläche aus lauter Dreiecken konstruieren soll. Wenn man massenhaft Punkte in 3D hat, dann ist es wirklich schwer, sich zu überlegen, auf was für unterschiedliche Fälle der Algorithmus stoßen könnte. Ich meine, für so einen Fall sind locker mal 4-5 Punkte im Minimalfall relevant. Versuch mal, Dir bezüglich so vielen Punkten in 3D auszudenken, auf welche Konstellationen ein Algorithmus problematisch reagieren könnte.
Ich habe keine Testfälle definiert, sondern habe den Algorithmus stattdessen auf simulierte Testdatensätze losgelassen. In solchen Datensätzen waren dann locker mal einige 10.000 Punkte drin. Und dann knallt es irgendwo. Dann stellt man plötzlich fest, dass der Algorithmus nicht terminiert oder, dass das Ergebnis nicht in Ordnung ist. Es ist sehr schwer, dann rauszufinden, was eigentlich passiert. So schwer, dass ich teilweise Gnuplot genutzt habe, um mir eine Visualisierung der "lokalen" Ereignisse bei einem Problem auszugeben. Erst durch diese spezielle Visualisierung bin ich bei einem Problem auf die Ursache gekommen.
Naja, derartige Fehler lassen sich im Vorfeld kaum ausschließen. Egal, wie man an die Programmierung herangeht. Es wäre aber interessant, ein paar Ideen dazu zu hören, wie man Fehler bei komplexen Eingangsdaten findet, wenn man als Ausgabe praktisch nur eine Zahlenwüste zum Debugging nutzen kann. Eine Zahlenwüste, die es einem sehr schwer macht, eine Vorstellung des Problems zu entwickeln.
Aber es gibt eine ganze Menge einfacherer Fehler, die man glaube ich durch eine entsprechende Programmierweise teilweise in den Griff kriegen müsste.
Unit-Tests kann man machen, wenn man Testfälle für Teilfunktionalitäten definieren kann und einen entsprechend modularen Algorithmus hat. Im Fall des Ball Pivotings wäre das teilweise gegangen. Dort hat man zum Beispiel die Teilaufabe, den Mittelpunkt einer Kugel mit gegebenem Radius zu bestimmen, die 3 gegebene Punkte berührt. In so einem Fall sind die unterschiedlichen Fälle noch überschaubar. Aber wie gesagt: In der Mathematik hatte ich keine wirklichen Fehler. Kann da aber natürlich auch schnell vorkommen. Bei solchen Dingen ist schnell mal ein Vorzeichen falsch, so dass ein falsches Ergebnis herauskommt. Wenn man das dann erst an ganz anderer Stelle merkt, dann kriegt man Probleme, den Fehler zu finden. Ich meine, hier handelt es sich um eine Teilfunktionalität, die vielleicht 20-30 Zeilen in einem Algorithmus mit 600 Codezeilen ausmacht. Wenn man Fehler dann systematisch im Code eingrenzen kann, hat man schon viel gewonnen.
-
Ja - wie gesagt - automatisiertes Testen kann ziemlich eklig und zeitaufwändig sein. Ich kenne diesen Algorithmus nicht, aber aus Deinen Erläuterungen entnehme ich, dass die Validierung des Algorithmus auf Korrektheit alles andere als trivial ist. Umso wichtiger ist es imo, die Testfälle wirklich strukturiert zu dokumentieren und wie könnte man das besser machen als mit Unittests?
Ich weiss aus leidiger Erfahrung, wie schwierig solche Unittests manchmal sind, vor allem wenn man selbst nicht Überblicken kann, (i) welche Fälle alle überprüft werden müssen und (ii) wie man die Testdaten generieren muss, um diese Fälle zu erreichen.
Ich finde Deinen Ansatz gut, einfach einen mehr oder weniger zufälligen und geeignet komplexen Testdatensatz auf den Algorithmus loszulassen. Du musst das ja nun lediglich noch als Unittest deklarieren und geeignete Nachbedingungen formulieren. Der Vorteil ist, dass Du im Fehlerfall gezielt über Breakpoints im Unittest den Algorithmus debuggen kannst und jederzeit den Überblick über die Testdaten hast. Ausserdem kannst Du sicherstellen, dass wenn Du Bug A behebst, nicht versehentlich an anderer Stelle einen neuen Bug einbaust.
Es gibt übrigens auch Tools, die Dir die Coverage Deiner Tests visualisieren. Damit kannst Du sehr gut prüfen, ob Deine Unittests den gesamten Code des Algorithmus abdecken oder ob Du irgendwelche Fälle vergessen hast.
-
byto schrieb:
Ja - wie gesagt - automatisiertes Testen kann ziemlich eklig und zeitaufwändig sein. Ich kenne diesen Algorithmus nicht, aber aus Deinen Erläuterungen entnehme ich, dass die Validierung des Algorithmus auf Korrektheit alles andere als trivial ist. Umso wichtiger ist es imo, die Testfälle wirklich strukturiert zu dokumentieren und wie könnte man das besser machen als mit Unittests?
Ich weiss aus leidiger Erfahrung, wie schwierig solche Unittests manchmal sind, vor allem wenn man selbst nicht Überblicken kann, (i) welche Fälle alle überprüft werden müssen und (ii) wie man die Testdaten generieren muss, um diese Fälle zu erreichen.
Ich finde Deinen Ansatz gut, einfach einen mehr oder weniger zufälligen und geeignet komplexen Testdatensatz auf den Algorithmus loszulassen. Du musst das ja nun lediglich noch als Unittest deklarieren und geeignete Nachbedingungen formulieren. Der Vorteil ist, dass Du im Fehlerfall gezielt über Breakpoints im Unittest den Algorithmus debuggen kannst und jederzeit den Überblick über die Testdaten hast. Ausserdem kannst Du sicherstellen, dass wenn Du Bug A behebst, nicht versehentlich an anderer Stelle einen neuen Bug einbaust.
Es gibt übrigens auch Tools, die Dir die Coverage Deiner Tests visualisieren. Damit kannst Du sehr gut prüfen, ob Deine Unittests den gesamten Code des Algorithmus abdecken oder ob Du irgendwelche Fälle vergessen hast.
Das kannste erst sinnvoll machen, NACHDEM der Algo zum ersten mal läuft. Kannst den Testfall ja nicht per Hand durchrechnen, weil Du per Hand auch an jener Stelle an das else nicht gedacht hättest, und weil Du Dir die 3d-Daten dann auch nicht so supi vorstellen kannst, um den Fehler zu sehen.
Also NACDEM der Algo korrekt funktioniert, kannste deine automatisierten Tests erst definieren und die schützen Dich davor, daß er kaputtgeht, weil drunterliegende Datenstrukturen ihre Semantik ändern, weil anscheinend sichere Optimierungen doch falsch sind, weil UB drin war und der Compiler sich hier mal umentscheidet.
-
Du hast recht. Ggf. kann man auch schon während des Implementierens des Algos. anfangen, die ersten Testfälle zu definieren. Aber Gregor schrieb ja, dass gerade die Debug-Phase am Ende so lange gedauert hat. Genau in dieser Phase können Unittests gut unterstützen. Meiner Erfahrung nach ist man damit häufig sehr genauer als beim "manuellen" Testen per Debugger.
Und dass die Unittests im Nachhinein die Wahrscheinlichkeit senken, den Algo. durch Refactorings in der Datenstruktur kaputtzumachen, sehe ich als dicken Vorteil. Refactorings sind wichtig, um langfristig die Codequalität hochzuhalten und Unittests geben die Sicherheit, dabei nichts zu zerstören.