Sprechende Namen oder: Code-Qualität

Da es in einem meiner letzten Projekte vorkam und sich jetzt auch öfter wiederholt hat, möchte ich einmal dazu etwas schreiben. Das Thema ist „Programmieren“.

Beim Programmieren in einer der meisten gängigen Programmier- oder Scriptsprachen schreibt man Quellcode. Dieser Quellcode ruft oftmals Prozeduren oder Funktionen auf, die mit den Bibliotheken beim Compiler mitkommen. Der Programmierer schreibt aber auch eigene. Dabei geht es mir um diese Themen:

  • Verzeichnis- und Dateihierarchie
  • Benamung von Dateien
  • Benamung von Codebestandteilen
    • Funktionen
    • Prozeduren
    • Klassen/Interfaces
    • Variablen
    • Konstanten

Mir ist oft aufgefallen, dass diesen Themen oftmals nur ungenügend Aufmerksamkeit geschenkt wird, dabei ist das, meiner Meinung nach, unglaublich wichtig, wenn

  • Der Code gewartet werden soll über Jahre oder Dekaden
  • Andere den Code verstehen sollen
  • Andere den Code ändern oder erweitern sollen
  • Andere den Code portieren sollen
  • Es keine ausreichende Dokumentation gibt

Ein Beispiel. Wir haben eine Funktion, die sieht selbst so aus und hat folgenden Inhalt:

int ab_ad_nmb_re(int pab, int p_ab, bool ab) { 
    int pab_list; 
    char *pa; 
    … 
}

Da gibt es mehrere Probleme:

  1. Der Funktionsname besteht aus etlichen Abkürzungen, die ein anderer nicht kennt, nachschlagen und sich merken muss. Oder immer wieder nachschlagen muss. Dementsprechend nichtssagend ist der Name
  2. Die Variablen heißen alle fast gleich, sind nichtssagend und unglaublich verwirrend

Oftmals, von mehreren unterschiedlichen Leuten, höre ich dann folgende Argumente:

  • Ich weiß ja, was das alles heißt
  • Du kommst da schon irgendwann rein
  • Wir können doch keine aussagekräftigen Namen nehmen, da das dann zu lang ist und dann bringt das auch keinem was

Ehrlicherweise muss ich da sagen: Nein. Ich nehme (s. GitHub) halbwegs aussagekräftige Namen (manchmal, selten, auch mal nicht, das sollte so aber nicht sein). Sie sagen aus, was sie machen, man braucht keine oder wenig Dokumentation und auch andere sind schnell eingearbeitet und können Änderungen vornehmen.

Bezüglich langer Namen: Jeder halbwegs taugliche Editor zum Programmieren bietet Autovervollständigung. Selbst VIM bringt im Standard bereits eine kontextlose Autovervollständigung mit, die ich gerne benutze: [Strg]+[p/n]. Damit sind längere Namen kein Problem mehr, da nur der oder ein paar Anfangsbuchstaben getippt werden müssen.

Das hat viele Vorteile:

  • Der Code wird lesbarer
  • Andere können sich viel schneller einarbeiten
  • Der Code selbst ist Dokumentation

Mein altes Projekte Warehouse nutzt solche Namen an den meisten Stellen. Das Projekt ist von 2009 und wurde mit VIM umgesetzt. Aber schaut euch auch Frameworks an, die Methodennamen mit vernünftiger Benamung haben wie Qt, wxWidgets, Java-SDK, uvm.

Ich kann nur empfehlen, lieber den Code etwas aussagekräftiger zu gestalten, denn meiner Meinung nach zahlt sich das schnell aus. Speicher ist günstig, da machen die paar Bytes den Kohl auch nicht fett.

Bald neue Videos

Viele haben mir geschrieben, dass ich doch weitere Videos bzw. Tutorials machen soll. Das freut mich sehr.

Zur Zeit nimmt mich die neue Arbeitstelle und meine Familie gut ein, ich habe aber schon die nächsten Videos geplant (Themen: wxWidgets, Qt und auch FreeBSD) und freue mich, sie bald machen und veröffentlichen zu können. Es dauert allerdings noch ein paar Tage. Aber: Es wird mit den Tutorials weiter gehen.

Wenn ihr Vorschläge für bestimmte Themen habt, gerne her damit.

compow als Privatprojekt in Form einer Referenz wieder online

Eines meiner Projekte, dass ich vor wenigen Jahren gemacht hatte, lag noch auf meiner Festplatte und war bereits lange nicht mehr aktiv. Ich dachte mir allerdings, dass ich das noch einmal gerne als Referenz von mir online stellen wollte: compow.

Bei compow handelt es sich um eine Website, auf der sich Firmen vorstellen können und Stellenanzeigen schalten können. Ursprünglich war das mal kostenpflichtig, was ich aber herausgenommen habe, da ich nicht selbständig bin und damit kein Geld verdiene, es ist lediglich eine meiner Referenzen.

Das Interessante an der Website ist der Tech-Stack, denn anstelle einer der üblichen Webprogrammiersprachen wie Ruby, PHP, Python, Go, ASP.NET (keine Sprache, aber ihr wisst, was ich meine), basiert diese Website auf folgenden Technologien:

Wie gesagt, die Seite dient einfach nur als Referenz, womit ich mich in den letzten Jahren beschäftigte. Die Website ist nicht weiterentwickelt und wird es mitunter auch nicht.

Mini-Kurztipp: LLVM für Windows

Ein wenig peinlich, aber vielleicht geht es dem ein oder anderem Benutzer unixoider Betriebssysteme genauso: auf FreeBSD nutze ich clang(++), auf macOS auch. Auf Linux kann ich das auch. Ich habe jetzt erst bemerkt, dass es auch auf Windows geht. Und noch mehr: die Libs, die am Ende herausfallen, sind mit denen von Visual Studio kompatibel.

LLVM (clang++) auf Windows

Wer es ausprobieren möchte, findet die Downloads hier.

Ein paar Gedanken: Ist statische Code-Analyse und Code-Coverage ein Garant für guten Code und gute Software?

In den letzten Wochen setzte ich mich mit verschiedenen Werkzeugen zur Verbesserung meines Codes und dem Code anderer auseinander. Teilweise laß und hörte ich, dass durch statische Code-Analyse sowie Code-Coverage, einfach gesagt, der eigene Code und die eigene Software besser wird. Dabei hatte ich ein flaues Gefühl im Magen.

Ich sah mir also folgende Dinge an: SonarQube für die statische Analyse von Quellcode, Google-Test mit gcov beziehungsweise llvm-cov für Code-Coverage, aber auch zusätzlich clang-format und clang-tidy, um einheitliche Codeformatierung hinzubekommen und bereits Warnings zur Bearbeitungszeit zu Problemen in meinem Code. Dies versuchte ich mittels VIM und ALE abzudecken, wobei es da zu größeren, bisher ungelösten Problemen kam.

SonarQube

SonarQube gibt es in verschiedenen Lizensierungsmodellen. Neben der Community-Edition, die nur wenige Programmier- und Auszeichnungssprachen beherrscht, gibt es beispielsweise auch die Developer-Edition, die einige mehr kann. Meine Erfahrungen habe ich mit C++-Code versucht, zu machen. Dementsprechend kommt man um die Developer-Edition nicht herum. Diese ist kostenpflichtig und wird nach zu analysierenden Zeilen von Code gezahlt. Das kann schnell teuer werden, zum jetzigen Zeitpunkt, Oktober 2023, kostet das System 150 USD für 100.000 Zeilen Quellcode pro Jahr. SonarQube unterteilt sich in unterschiedliche Programme, sofern man es für C++ (und C und Objective-C) einsetzt: Die Website, auf der die Ergebnisse illustriert werden, den build-wrapper, der aber auch durch compiledb ersetzt werden kann und den sonar-scaner. Die SonarQube-Werkzeuge sind in Java geschrieben und bringen ihr eigenes JRE mit, können aber auch mit einem anderen genutzt werden. Hier ist Vorsicht geboten, sind die Scripte, die die SonarQube-Komponenten aufrufen, in der Shebang doch fest auf /bin/bash gestellt, was auf verschiedenen Betriebssystemen zu Problemen führen kann.

Nicht jedes jedes System wird mit allen Tools unterstützt. Beispielsweise wird macOS arm64 nicht vom build-wrapper unterstützt, so dass man schlussendlich auf compiledb ausweichen muss. Das impliziert: verschiedene Architekturen, verschiedene Tools. Darum soll es hier aber nicht gehen.

Der Vorgang ist folgender: nachdem SonarQube installiert und initial konfiguriert wurde, erstellt man sein erstes eigenes Projekt und erhält einen Token, den man dann beim sonar-scanner entweder auf der Kommandozeile übergibt oder in die SonarQube-Konfiguration einträgt. Dann lässt man sein Make-System einmal mit dem build-wrapper oder compiledb durchlaufen, damit SonarQube weitere Informationen erhält. Zum Schluß wird der sonar-scanner gestartet, der dann die Code-Analyse durchführt, das Coverage verarbeitet, das aber durch andere Tools vorher erstellt werden muss, und die Ergebnisse ins Internet schiebt, so dass sie auf der Website erscheinen.

So, wie ich das verstanden habe, besteht die Analyse aus verschiedenen Teilbereichen. Es wird teils nach echten Bugs geschaut, teils nach sogennanten Code-Smells. SonarQube erkannte tatsächlich sowas:

auto *x = new y();
if (x) {
    //
} else {
    x->method();
}

Aber auch sowas:

const int x = 10;
return x == 10 ? a : b;

SonarQube stufte diese Fehler als Bugs ein und markierte sie als kritisch, was auch gut war. Womit ich nicht zufrieden war, war die Kategorisierung der anderen Regeln. Dies lässt sich in der Konfiguration zwar abändern, aber ich gebe diese zwei Beispiele trotzdem. Bei beiden geht es um sogenannte Code-Smells. Dabei handelt es sich um Code-Konventionen, die keine Bugs sind, sondern dem in SonarQube eingestellten Regelwerk unterliegen und gefixt werden sollten. Im ersten Fall gilt eine Einrückungstiefe innerhalb von Kontrollstrukturen tiefer als drei als kritisch:

if (true) {
    if (true) {
        if (true) {
            if (true) {
            }
        }
    }
}

Im Gegensatz dazu wurde folgender Code “nur” als Major eingestuft, obwohl er meiner Meinung nach kritisch sein sollte:

int var = 10;
if (true)
    int var = 5;

Man sieht klar, dass das Regelwerk für jeden Programmierer oder jedes Projekt oder jedes Team innerhalb verschiedener Konstellationen angepasst werden muss.

Aber kommen wir zur wichtigen Frage: wird mein Code und/oder meine Software durch den Einsatz von statischer Code-Analyse, wie mit SonarQube, automatisch gut?

Meine Antwort darauf ist nein. Das Positive war, dass SonarQube im Projekt zwei potentielle Bugs gefunden hat, das meiste Andere war eher kosmetischer Natur. Dabei kennt SonarQube keinen Kontext. Es prüft nicht, ob Abläufe richtig sind, ob externe Bibliotheken mit hineinspielen, ob Toolkits bestimmte Voraussetzungen haben, an die man sich halten muss und es prüft natürlich auch nicht die Logik oder Algorithmen. Weiterhin verpflichtet SonarQube nicht. Mittels // NOSONAR direkt im Code schaltet man alle Prüfungen für die Zeile ab. Je nach Situation macht das durchaus Sinn, kann aber natürlich auch vom “faulen” Programmierer missbraucht werden.

Ein anderes Problem ist, dass je nach Regelwerk der Code komplexer und auch schwieriger zu lesen wird und der Programmierer mitunter beginnt, an diesem Regelwerk vorbeizuarbeiten. Je nach Firma und Verständnis ist die Übersichtseite (z.B. im Falle von SonarQube) natürlich gut für jeden einsehbar und auch Entscheidungsträger können hier anhand von roten, gelben und grünen Symbolen auf die vermeintliche Qualität der Software schielen und Mitarbeiter dementsprechend beurteilen. Somit könnte der Programmierer natürlich versuchen, gewisse, nervende Regeln zu umgehen. Ein Beispiel: In SonarQube dürfen Klassen aus nicht mehr als 35 Methoden und 20 Membern bestehen. Spätestens bei GUIs kann das schnell zum Problem werden. Diese Klassen dann aufzusplitten, erhöht den Aufwand, die Komplexität und bietet aber mitunter keinen vernünftigen Nutzwert. In diesem Fall könnte der Programmierer beginnen – bei C++ – mit inneren Klassen und/oder Structs zu arbeiten, um dieser Problematik zu entgehen, da er sicher nicht die gesamte GUI-Datei in der Konfiguration ausklammern möchte. Dabei könnte dann folgendes herauskommen:

class MyGUI : public GUI {

public:
    struct UI {
        Button b0;
        Button b1;
    };
    UI ui;

};

Das stimmt SonarQube an der Stelle glücklich. Wie man aber sieht, wird der Code selbst umfangreicher.

Die Qualität kann zunehmen, muss sie aber nicht. Für mich ist an der Stelle statische Codeanalyse ein Werkzeug für mich, aber absolut kein Garant für guten Code, noch für gute Software.

Code-Coverage

Code-Coverage ist vom Wording meiner Meinung nach nicht ganz korrekt. Test-Code-Coverage oder ähnlich sollte es eher immer heißen.

Damit ist gemeint, dass mit Testszenarien (Unit-Tests, Intergrations-Tests, …) eine gewisse Menge Code getestet wird, also abgedeckt ist. Nimmt man hier als Beispiel gcov, ist es recht einfach. Man kompiliert sein Programm mit –coverage und führt es aus. Beim Kompilieren und Ausführen werden verschiedene Parameter gesammelt. Wichtig hier ist, welche Zeile im Code aufgerufen wird. Wird sie mindestens einmal aufgerufen, gilt sie als abgedeckt.

Hier sieht man, dass mitunter Funktionen oder Methoden selbst nicht explizit aufgerufen werden, sondern implizit und dennoch gelten sie als gecovert. Auch sagt Code-Coverage nichts darüber aus, wie sinnvoll ein Test ist, ob es nicht noch weitere Tests geben müsste, ob wirklich alles getestet ist und damit, ob die Qualität der eigenen Software verbessert wurde.

Noch ein Wort zu Formattern und Lintern

Beide Teile, also Formatter und Linter, sind spätestens im Team natürlich durchaus praktisch. Der Linter zeigt an, wo es noch Probleme bei Konventionen im Code geben könnte. Hier funktioniert clang-tidy recht gut. Der Formatter formatiert den Code so, dass er für alle gleich ist. Sprich, er achtet auf Einrückungen und ähnliches.

Fazit

Also, was denke ich? Wenn man diese Programme nutzt, als das, was sie sind, können sie dem Programmierer durchaus nützlich sein. Selbst, wenn man aus verschiedenen Gründen nicht alle Regeln einhalten kann oder möchte, regen sie doch zum Nachdenken an. Aber sie sichern selbst nicht die Qualität des Codes. Das muss der Entwickler selbst tun, wobei es sich hier um einen iterativen Prozess handeln kann.

Diese Tools dürfen aber niemals zum Einsatz gegen den Programmierer genutzt werden, sondern bei Problemen sollte dieser immer erst befragt werden.

wxWidgets auf Windows per Kommandozeile mit Visual Studio kompilieren

Wer schnell und einfach wxWidgets auf Windows auf der Kommandozeile kompilieren will, öffnet ein Visual Studio Terminal, also ein Terminal, in dem die Umgebungsvariablen für VS gesetzt sind, navigiert zu den Sourcen von wxWidgets und dort ins Verzeichnis build\msw und kann via nmake den Kompilationsprozess in Gang setzen:

nmake /f makefile.vc BUILD=release SHARED=0 TARGET_CPU=X64 RUNTIME_LIBS=static

Wir sehen am Beispiel, dass wir eine Release-Version von wxWidgets bauen, also keine Debug-Informationen drin sind (ansonsten release einfach durch debug tauschen). SHARED=0 bedeutet, dass wir keine dynamischen Libraries bauen möchten (wenn doch, einfach weglassen). Die restlichen Parameter sollten ebenso selbsterklärend sein.

Ein interessanter Fehler beim Testen von Software

In der Firma, in der ich arbeite, ist mir ein Malheur passiert, und zwar beim Testen der von mir geschriebenen Software. Ich will (vielleicht darf) ich keine Details verraten, aber es geht um ein Programm, welches ein anderes Programm startet, überwacht und beendet. Nichts Großes, nichts Kompliziertes. Eine Sache muss man aber wissen: das Programm, an dem ich arbeite und das Programm, dass von meinem gestartet wird, soll auf macOS arm64 und macOS amd64 laufen und ist keine Fat-Binary, was bedeutet, dass es für die jeweilige Plattform immer ein Bundle gibt.

Jetzt ging es zum Glück nur um eine Testversion, die ich meinen Kollegen zum Ausprobieren geben wollte. Ich packte mein Programm also, zusammen mit dem Fremdprogramm. Einmal für macOS amd64, einmal für macOS arm64 (und noch weitere Plattformen). Faul, wie ich bin, hatte ich dafür ein Script geschrieben. Leider habe ich nicht weit genug automatisiert, denn hier passierte das Problem: Ich packte mein Programm für amd64 und das Fremdprogramm für arm64. Bevor ich meine Software zum Testen herausgebe, probiere ich diese immer einmal aus, zumindest im kleinen Rahmen. Eine CI-CD-Pipeline gab es in dem frühen Entwicklungsstadium leider noch nicht (Das ist ein Fehler! Kümmert euch direkt darum!). Jetzt hätte das Problem beim Testen auffallen müssen, aber hier beging ich den entscheidenden großen Fehler in der gesamten, sehr suboptimalen Kette: Ich probierte die arm64 und die amd64 meine Software und der Fremdsoftware nur auf meiner arm64-macOS-VM aus.

Wer aufgepasst hat, kann sich sicher denken, was jetzt passiert ist: Da Rosetta 2 lief, startete sie natürlich mein amd64-Programm und mein amd64-Programm startete das arm64-Fremdprogramm, ohne, dass ich was von einem Fehler mitbekam.

Ich lieferte natürlich die amd64-Version aus und es kam, wie es kommen musste: bei den Kollegen mit amd64 macOS funktionierte zwar mein Programm, das Fremdprogramm aber natürlich nicht, da es kein Rosetta-Äquivalent auf macOS amd64 gibt.

Dabei hatte ich den Fehler die ganze Zeit auf dem Schirm, denn mein Paketierungsscript lieferte Hinweise, wenn die Architektur der Fremdsoftware mit der Architektur meines Programms nicht zusammenpasst. Aber ein Hinweis lässt sich im Stress leicht übersehen. Hier hätte ich doch besser einen Abbruch eingebaut.

Was lernen wir daraus? Am Besten vollständig automatisierte Tests implementieren, aber wenn man für verschiedene Architekturen händisch testet, das dann auch direkt auf der jeweiligen Architektur durchführen und nicht in einer Emulation.

Java: JAR-Klassen-Versionsproblematik

Ja, nach langer langer Pause musste ich mich wieder einmal mit Java auseinander setzen. Dabei stieß ich auf ein Problem: Ich wollte JDBC-Treiber in einer bereits vorhandenen Version eines Programms einbinden. Soweit kein Problem, doch nicht alle Treiber funktionierten. Ich bekam keine (sinnvolle) Fehlermeldung, sondern Dinge funktionierten einfach nicht.

Ich fand dann heraus, dass mein JDK mitunter zu alt war, um mit den Klassendateien im JAR zu laufen. hier hätte ich mir eine vernünftige Fehlermeldung gewünscht, es kam aber keine.

Da ich für das Deploying (und nur für das Deploying) diese Problematik testen und Fehler werfen wollte, überlegte ich, wie ich vorzugehen habe. Zuerst dachte ich naiv, dass JAR-Dateien versioniert sein könnten. Natürlich völliger Quatsch, sind das ja einfach nur Archive, in denen alles Mögliche drin sein kann. So ist es auch: die darin enthaltenen Klassen-Dateien (.class) können für unterschiedliche JRE-Versionen kompiliert worden sein.

Allerdings kann man recht einfach herausfinden, für welche Version die Klassendateien kompiliert wurden: Im fünften und sechsten Byte der class-Datei steht die Minor-Version (also Byte-Offset 4 und 5), im siebten und achten Byte steht die Major-Version: s. Wikipedia.

Ich schrieb also ein Programm in C, welches die libzip nutzt, um an die Dateien im JAR zu kommen, las dann Byte 7 und 8 aus (Offset 6 und 7) (auf Endianess achten!) und fand somit die höchste und niedrigste Version des JDKs heraus. Eigentlich recht einfach.

Vielleicht hilft dem ein oder anderen das.

Hier noch die Versionsnummern bis 19 (kopiert von hier):

Java SE 19 = 63 (0x3F hex)
Java SE 18 = 62 (0x3E hex)
Java SE 17 = 61 (0x3D hex)
Java SE 16 = 60 (0x3C hex)
Java SE 15 = 59 (0x3B hex)
Java SE 14 = 58 (0x3A hex)
Java SE 13 = 57 (0x39 hex)
Java SE 12 = 56 (0x38 hex)
Java SE 11 = 55 (0x37 hex)
Java SE 10 = 54 (0x36 hex)
Java SE 9 = 53 (0x35 hex)
Java SE 8 = 52 (0x34 hex)
Java SE 7 = 51 (0x33 hex)
Java SE 6.0 = 50 (0x32 hex),
Java SE 5.0 = 49 (0x31 hex)
JDK 1.4 = 48 (0x30 hex)
JDK 1.3 = 47 (0x2F hex)
JDK 1.2 = 46 (0x2E hex)
JDK 1.1 = 45 (0x2D hex)

KooKooK (Update 4): Die Authentifizierung im Einzelnen

Vielleicht interessiert es den ein oder anderen, wie ich die Authentifizierung durchführe. Mich würde unbedingt Eure Meinungen interessieren.

Ziel war es, dass man nicht Benutzername und Kennwort über die Leitung zum Server zur Verifizierung überträgt, da ich das für unsicher halte. Ich habe es erst einmal so gelöst:

  1. Client verbindet sich zum Server
  2. TLS-Handshake
  3. Server sendet Welcome-Message an Client. Die Welcome-Message besteht aus der Versionsnummer des Servers sowie einer UUID, getrennt mit einem Doppelpunkt (1.0.1:8234-234-234-234)
  4. Der Client trennt die empfangene Versionsnummer von der UUID anhand des Doppelpunkts
  5. Der Benutzer hat am Client den Benutzernamen und das Kennwort eingegeben
    1. Das Kennwort wird mit MD5 gehasht
    2. Benutzername, UUID und gehashtes Kennwort wird als Bytearray zusammengefasst und mit Keccak_512 gehasht
    3. Daraus entsteht dann das Bytearray, was zum Server geschickt werden soll
  6. Das gehashte Bytearray mit vorangestelltem Benutzername@ wird zum Server geschickt (ausgedachtes Beispiel: thorsten@ljdf08asdflu0dsa98foklj234ASDF)
  7. Der Server hat jetzt den unverschlüsselten Benutzernamen, der eindeutig in der Datenbank ist und zieht das Kennwort, fügt auch alles zusammen und hasht dann das Bytearray
  8. Der Server vergleicht, ob das empfangene Bytearray gleich dem selbst generiertem Bytearray ist. Ja = Der Benutzer ist eingeloggt, Nein = Der Benutzer ist nicht eingeloggt

Anmerkungen: In der Implementierung werden weitere Fehler behandelt und zurückgeliefert. Beispielsweise, ob der Benutzer überhaupt vorhanden ist, ob der Hash gebildet wurde, usw. Was noch fehlt ist, ob der Benutzer auch (in der Datenbank) auf aktiv steht.

Ihr könnt das in folgenden Dateien nachvollziehen:

Mitunter sollte man weitergehen und zum Beispiel auch den Benutzernamen nicht im Klartext senden?