Liebe Leserinnen, liebe Leser,

es war eine Weile her wieder, seitdem ich einen Beitrag geschrieben habe, aber als es ich in meiner letzten Statusmeldung erklärt habe, ist das wegen der Übersetzung meiner alten Beiträge. Die gute Nachricht ist, dass die Übersetzungen fast fertig gemacht sind, und nur ein paar Beiträge brauchen ein bisschen Arbeit.

Sowieso, der Hauptgrund dieses Beitrags ist, dass ich einige zufällige Gedanken (aus dem Gesichstpunkt einer ungeübten Person) über das Schreiben eines Programms teilen möchte. In einem meiner letzten Beiträge habe ich dieses Thema schon erwähnt, aber damals habe ich manche Fehler gemacht, die aus dem Gesichtspunkt der Programmierung zu einer kompletten Katastrophe geführt hat. Die größten Fehler waren vielleicht das Fehlen von Unit-Tests und der immer geänderte Vorsatz des Projekts. Dieses Mal habe ich diese Probleme behoben. Das bedeutet, dass Unit-Tests vor dem Produktionscode geschrieben wurden, und der Vorsatz des ganzen Projekts blieb die ganze Zeit konstant.

Trotz dieser grundsätzlichen Behebungen stellte sich das Schreiben eines Qt-basierten Programms schwierig heraus, weil ich mich mit anderen Grundlagen auch auf Trab bringen musste, die (in keiner besonderen Reihenfolge) die folgende sind:

  • Nebenläufigkeit verstehen (heutzutage kann das nicht vermieden werden)
  • Dokumentation für die Qt Entwicklungsumgebung erzeugen (um das eingebaute Hilfesystem benutzen zu können)
  • sich in die Qt-Bibliothek vertiefen (Displayhierarchie, Containerklasse, grafische Elemente, usw.)
  • über eine andere große Bibliothek (OpenCV) genug lernen, um den Blick über die Biblothekstilen zu erweitern
  • ein Projekt mit Unit Test Frameworks einrichten (Google Test und Qt Test)
  • Über die Teilung eines Projekts mit Bibliotheke lernen, anstatt ein monolitisches Programm zu erstellen (es war auch notwendig, sich in Buildsysteme zu vertiefen)
  • erste Erfahrungen über Test Driven Entwicklung bekommen
  • eine vernünftige Programmstruktur und Klassenschnittstelle ausklügeln (das war bisher die schwerste Sache)
  • eine Versionverwaltungssystem ehrlich benutzen
  • und solche anderen Sachen, die ich mich jetzt nicht erinnern kann

Obwohl über diese Themen in schon gelesenser Bücher (zumindest teilweise) diskutiert wurden, war es viel zu bewältigen, besonders wenn man bedenkt, dass Bücher nur als Richtlinien und nicht als eine Quelle von einsatzfertigen Lösungen dienen können. Zum Beispiel war es zuerst schwer zu sagen, wie groß die Granularität der Unit Tests sein soll, und das Schreiben von brüchigem Code vermieden werden kann (ich habe dabei noch viel zu lernen). Weiterhin, das ist manchmal auch schwierig festzustellen, ob eine Funktionalität doch getestet werden soll oder nicht. Hier gibt’s ein Beispiel:

C++

1
2
3
4
bool MyClass::isFormatValid(SomeClass::Format someFormat)
{
    return !(convertFormat(someFormat) == OderClass::OderFormat_Invalid);
}

Nehmen wir an, dass alle Funktionalität bezüglich der Formatkonvertierung schon getestet wurde, und der Zweck der isFormatValid() Funktion ist die Verdeutlichung des Vorsatzes von solchen Vergleichen. Diese Funktion führt der Klasse neue Funktionalität ein, also es sollte theoretisch getestet werden. Leider, der Test, der diese Funktionalität testen würde würde nur die Programmierungssprache überprüfen (die einzige Weise, auf die dieser Code scheitern könnte, ist die Auslassung der Negation des Ergebnisses). Einerseits könnte man behaupten, dass die Überprüfung der Programmierungssprache (besonders solche grundsätzlichen Teil der Sprache) verschwenderisch wäre, aber andererseits dienen ordentlich geschriebene Tests als Dokomentation, die einem darstellt, was von der Klasse erwartet und nicht erwartet werden kann. Andererseits, wenn die Tests nicht geschrieben würden, würde es wie ein nicht ordentlich dokumentierter Code klingen. Also, sollte ein Unit Test für diese Funktion geschrieben werden?

Eine der großten Überraschungen war für mich die Untrivialität des Schreibens eines kleinen Programms. Das Schreiben einer Klasse und das Schreiben einer für das Ganzes akzeptabel und Standardkonforme Klasse sind zweierlei. Die Fragen, die schon entstanden, sind die folgende:

  • Was für eine Schnittstelle eine Klasse haben soll?
  • Wie und wann soll man eine Klasse generisch schreiben, und wann (und inwieweit) kann eine Klasse von einer anderen Klasse abhängig sein?
  • Wie sollen die Klassen zwischeneinander mitteilen, wenn es nötig ist?
  • Welche Fehlerbehandlungsstrategie soll verwendet werden in einem oder anderem Fall?

Die zweite Frage ist besonders schwierig zu beantworten, weil C++ mehr als eine Stategie für die Erstellung von genersichem Code ermöglicht. Das schwerste Sache ist nicht der Wahl der Stategien, sondern die Nebeneffekte, die beide Strategien verhängen. Die Nutzung von Schnittstellen erzwingt die Nutzung von Zeiger (und dynamischer Speicherzuteilung), die Klassensignaturen komplizierter machen können (sollen Daten durch Raw-Zeiger oder refferenzzählende Zeiger weitergegeben werden?). Andererseits, wenn man Templates benutzt, könnte man eine glatte Lüge einfach schreiben: Eine Klasse, die alle andere Klassen zu akzeptieren scheint, aber in der Wirklichkeit braucht sie eine sehr spezifische Eigenschaft, die in vielen anderen Klassen nicht vorhanden ist, einer Klasse (solche Eigenschaft ist z.B die Bestimmung von dynamisch zugewiesenen Datengröße, die != sizeof(someClass) ist). Eine Klasse, die Undo-Geschichte behandelt, ist ein gutes Beispiel, sofern die Klasse sich auf die Nutzung von einer bestimmten Menge von Arbeitsspeicher einschränken kann.

Die vierte Frage ist auch nicht einfach zu beantworten, weil das nicht immer sofort klar ist, ob eine Fehlermeldung durch einen Rückgabewert oder eine Ausnahme mitgeteilt werden soll. Eine Faustregel, die ich heutzutage folge, für die Auswahl einer von zwei Lösungen ist das folgende: Falls ein Fehler (mit großer Wahrscheinlichkeit) direkt von dem Aufrufer behandelt wird, scheint ein Rückgabewert die bessere Wahl zu sein (auf diese Weise bleibt der Fehler lokal und das ist günstiger zu behandeln). Andererseits, falls das nicht klar ist, wer den Fehler behandeln wird, ist eine Ausnahme die bessere Alternative (auf diese Weise vermeiden wir die Erstellung eines komplizierteren Weitergabepfad für Fehlerinformation).

Eine Diskussion über andere genannten Themen wäre toll, aber dieser Beitrag startet zu lange zu sein, deswegen werde ich das hier beenden.

Wie immer, vielen Dank fürs Lesen.