Warum eure Software mehr Personal frisst als Weihnachtskekse 🍪💻

Oder: Was Omas, Stanzmaschinen und LEGO euch über Software-Entwicklung beibringen können

Es gibt Firmen, die seit Jahrzehnten die gleichen Produkte, Maschinen oder Anlagen herstellen – und dafür Software benötigen. Seit Jahrzehnten beschäftigen sie Software-Entwickler. Und jedes Jahr brauchen sie mehr davon: mehr Entwickler, mehr Architekten, mehr Projektleiter. Die Kosten steigen, die Teams wachsen.

Und doch fragt sich niemand:
👉 „Warum ist das so?“
👉 „Warum geben wir jedes Jahr mehr für Software aus?“

Vielleicht helfen ein paar Vergleiche …


Die Wirtschaftlichkeit hausgemachter Weihnachtskekse 🎄🍪

Stell dir Oma beim Keksebacken vor:

  • Holt sie jedes Jahr neue Keksformen? Nein.
  • Braucht sie immer mehr Ausstecher, um dieselben Vanillekipferl zu machen? Natürlich nicht.

Die Formen halten jahrelang – und die Rezepte sowieso.


Die Wirtschaftlichkeit der Stanzmaschinen 🏭🔩

Es gibt Firmen, die Blech stanzen. Sie kaufen sich einmal Stanzmaschinen, die jahrzehntelang laufen.
Nur die Formen (die Werkzeuge) werden ab und zu getauscht – je nach Auftrag oder Abnutzung.

Keiner käme auf die Idee, die ganze Maschine jedes Jahr wegzuschmeißen.


LEGO® und die Kunst der Wiederverwendung 🧱✨

LEGO® bringt ständig neue Sets heraus – Raumschiffe, Schlösser, Dinosaurier, super geile Kampfroboter mit Laser, Phaser und Chat GPT 😉. Aber die meisten Steine sind seit Jahrzehnten gleich: Noppen oben, Löcher unten.

Die Maschinen, die die Steine spritzen, laufen jahrzehntelang. Sie werden nur ersetzt, wenn Automatisierung, Energieeffizienz oder Industrie-4.0-Spielereien echte Vorteile bringen.


Fazit 🤔

Liebe Firmen,
wenn ihr seit Jahrzehnten Software entwickelt und trotzdem jedes Jahr mehr Entwickler braucht, dann stellt euch bitte mal die Frage: Warum?

Vielleicht liegt’s daran, dass eure Software gar nicht so soft ist.
Vielleicht ist sie eher wie Hardware: starr, schwerfällig und unflexibel.

Und wenn das so ist … dann wundert euch bitte nicht, dass die Personalkosten explodieren. 😉

Die berühmte „Was-wäre-wenn…?“-Frage – Der Schlüssel zur soliden Software-Architektur

Wie ich bereits in einem früheren Artike (oder auch hier) erwähnt habe, beginnen die wichtigsten Fragen in der Software-Architektur mit „Was wäre, wenn…?“. Wird eine dieser Fragen übergangen oder vorschnell beantwortet, ohne gründlich darüber nachzudenken, darf man sich später nicht wundern, wenn bereits kleine Änderungen – etwa ein neues Feature oder eine Kundenanpassung – mühsame und kostspielige Anpassungen an unzähligen Stellen im Code nach sich ziehen.

Plötzlich müssen Methoden geändert, Signaturen angepasst und Rückgabewerte modifiziert werden. Ganze Datentypen, inklusive ihrer Eigenschaften, Methoden und Events, müssen umgestaltet werden. Dadurch verlieren bestehende Unit-Tests ihre Gültigkeit, müssen umgeschrieben oder gar entfernt werden. Noch schlimmer: Neue Unit-Tests passen oft nicht mehr nahtlos in das ursprünglich durchdachte Konzept.

Das TDD-Paradigma, kann hier paradoxerweise zu Problemen führen. Denn wenn die Architektur bereits auf wackligen Beinen steht, entstehen „Risse“ nicht nur im Code, sondern auch in der Benutzerführung – spürbar durch inkonsistente Steuerung oder unerwartetes Verhalten der Oberfläche bei Desktop-Anwendungen und Mobile-Apps.


Flexibilität: Was sich immer ändern wird

Manche Dinge ändern sich zwangsläufig – und sollten daher von vornherein leicht konfigurierbar oder erweiterbar sein:

  • Theme, Look & Feel – weil sich Design-Standards weiterentwickeln.
  • Sprachunterstützung – denn neue Märkte bedeuten neue Anforderungen.
  • Datenexport – weil Kunden irgendwann ein anderes Format benötigen.
  • Diagramme und Reports – da sich Analyse- und Visualisierungsanforderungen ändern.
  • Kommunikation mit Schnittstellen und Protokollen – weil neue Technologien und Standards entstehen.

Unwahrscheinliche Änderungen? Vielleicht doch nicht!

Dann gibt es Dinge, bei denen man glaubt, sie würden nie oder nur in den seltensten Fällen geändert werden – und doch geschieht es irgendwann:

  • Der Kunde will Produktionsdaten in einer exotischen Datenbank oder einem speziellen Dateiformat speichern.
  • Eine „unbedeutende“ Zahl soll plötzlich auf dem Bildschirm angezeigt oder eine neue Konfigurationsmöglichkeit für die Nachtschicht integriert werden.
  • Eine günstigere SPS oder Industrie-Kamera eines unbekannten Herstellers muss eingebunden werden – leider nicht ganz standardkonform.

Ob, wann und wie solche Änderungen kommen, ist nur eine Frage der Zeit.


Architektur ist wie Schach – erst denken, dann ziehen!

Ein guter Software-Architekt verhält sich wie ein professioneller Schachspieler: Er nimmt sich Zeit für das Vorausdenken, prüft verschiedene Szenarien und fragt sich „Was wäre, wenn…?“, bevor er zur Tastatur greift.

Denn grobe Architekturfehler verhalten sich wie unbedachte Züge im Schach: Ein frühzeitiger Fehlgriff kann zum Schachmatt führen – oder die gesamte Strategie unnötig kompliziert und mühselig machen. Einmal falsch platziert, steht die Figur im Weg und macht jede weitere Planung schwieriger.

Besser also, man nimmt sich die Zeit für eine durchdachte Architektur – bevor man sich später durch einen Flickenteppich aus Workarounds kämpft.

Klaus und seine magische Kiste: Warum Monolithen nicht die Antwort sind

Neulich erzählte mir mein Beifahrer von einem faszinierenden Geschäftspartner in der Mechatronik und Automatisierung. Dieser war seit Jahren in der Branche tätig und frustriert über die ständigen Anpassungen in komplexen Systemen. Jede kleine Änderung führte dazu, dass Kollegen monatelang das gesamte System umstellen mussten. Ein neuer SPS-Typ, der keinen GOTO-Befehl unterstützte? Problem. Ein spezieller Sensor, der nur RS-232, aber kein CAN konnte? Noch ein Problem. Also beschloss unser Held, das Problem ein für alle Mal zu lösen, indem er eine programmierbare „magische Kiste“ baute – ein Gerät mit allen möglichen Schnittstellen, das angeblich jedes Kompatibilitätsproblem beheben sollte. Ohne Code. Klingt toll, oder? Nun ja…

Als ich diese Geschichte hörte, musste ich schmunzeln. Ich unterbrach meinen Beifahrer und erklärte ihm, warum ich skeptisch war – anhand der berüchtigten All-in-One-Stereoanlagen aus den 1980er Jahren. Diese Geräte waren damals der letzte Schrei und versprachen alles in einem: CD-Player, Kassettendeck, Verstärker, Radio – und oft sogar einen Schallplattenspieler. Doch sie waren Monolithen, und das brachte erhebliche Nachteile mit sich:

  1. Monolithen und Modularität: Wenn das eingebaute Netzteil oder der Verstärker kaputtging, funktionierte gar nichts mehr. Weder der CD-Player noch die Kassettendecks – alles war tot.
  2. Bezahlte Überflüssigkeit: Selbst wenn man nur CDs abspielen wollte, zahlte man für Funktionen, die man nicht brauchte – wie ein zweites Kassettendeck oder den eingebauten Plattenspieler.
  3. Platzfresser: Diese Geräte waren riesig und belegten mehr Raum als nötig.
  4. Eingeschränkte Kompatibilität: Lautsprecher konnten manchmal ersetzt werden, aber nur, wenn sie über standardisierte Cinch-Buchsen angeschlossen waren. Wenn nicht, Pech gehabt.
  5. Qualitätskompromisse: Audiophile legten sich oft zwei Systeme zu, weil eines bessere Kassettenwiedergabe bot, während das andere bei CDs brillierte.

Und das alles in einer Ära ohne EU-Gewährleistung! Manche Hersteller boten stolz ein halbes Jahr Garantie, andere gerade mal zwei Monate.

Die Wende zur Modularität

Ende der 80er und Anfang der 90er lernten die Hersteller aus ihren Fehlern. Sie zerlegten ihre Monolithen in einzelne Module. Verbraucher konnten nun:

  1. Nur die Module kaufen, die sie tatsächlich benötigten.
  2. Komponenten verschiedener Hersteller kombinieren: ein Kassettendeck von Marke X, ein CD-Player von Marke Y und ein Verstärker von Marke Z – ideal, um die 1000-Gigawatt-Boxen fürs nächste Erdbeben mit Saft zu versorgen.

Die Lektion für die Software

Die Geschichte der Stereoanlagen zeigt, warum Modularität wichtig ist – eine Lektion, die auch in der Softwareentwicklung gilt. Die Frustration des Automatisierungs-Mechatronikers, der nach jeder kleinen Änderung das gesamte System anpassen musste, deutet auf ein grundlegendes Problem hin: Ad-hoc-Programmierung ohne durchdachte Software-System-Architektur.

Ein erfahrener Softwareentwickler (bzw. SW-Architekt) hätte sich sofort gefragt:

  1. Was, wenn Hersteller U nicht mehr existiert?
  2. Was, wenn Hersteller V das Produkt nicht mehr anbietet?
  3. Was, wenn Hersteller W die Lizenzbedingungen ändert?
  4. Was, wenn Hersteller X seine Treiber oder Schnittstellen ändert?
  5. Was, wenn Hersteller Y nur noch neue Modelle ohne vollständige Abwärtskompatibilität verkauft?
  6. Was, wenn Standard Z obsolet wird?

Die Liste ließe sich endlos fortsetzen. Ohne diese „Was-wäre-wenn„-Fragen läuft man Gefahr, eine digitale eierlegende Wollmilchsau zu bauen – eine magische Kiste, die zwar beeindruckend klingt, in der Praxis aber niemand will.

Niemand möchte von einem einzigen Hersteller abhängig sein, oder von einem einzigen Softwaresystem oder Produktmodell usw.

Niemand möchte mehr für Schnittstellen und wunderbare Features und Möglichkeiten bezahlen, die zwar mehr Raum und Speicher belegen und komplexer sind, diese aber nicht benötigt.

Der Unterschied zwischen Coder und Architekt

Hier kommt die entscheidende Lektion: Code zu schreiben ist nicht dasselbe wie Softwarearchitektur. Wer die Konzepte von Modularität, Komplexität und Abhängigkeiten nicht versteht, investiert vielleicht Jahre in einen Monolithen, der letztlich unbrauchbar ist.

Also, wenn Ihnen das nächste Mal jemand eine magische All-in-One-Lösung verspricht, denken Sie an die Stereoanlagen der 80er – und seien Sie skeptisch. Modularität mag weniger glamourös klingen, aber sie ist der Schlüssel zu langlebigen, flexiblen und wartbaren Systemen. Und das gilt für Hardware genauso wie für Software.

Gewidmet an P.M. (der Beifahrer)
Im nächsten Artikel erkläre ich dir, wie manche mit Buzz-Words wie „Low Code“ und „No Code“ viel Geld für das Blaue vom Himmel und sonstige Versprechungen (Blabla) verlangen und reich werden, während ihre Kunden doppelt und dreifach draufzahlen.

Wenn dein Code „Blinde Kuh“ spielt: Eine Reise durch das Labyrinth des digitalen Spaghetti-Chaos!

Ich betrat zum ersten Mal ein mir unbekanntes Haus und war auf der verzweifelten Suche nach einem Kugelschreiber und einem Blatt Papier.

Das Arbeitszimmer!“, dachte ich, „Ein Schriftsteller wohnt hier, dort wird sicherlich ein Schreibutensil zu finden sein.“

Die Türschilder leiteten mich durch die Räume, und ich machte mich auf den Weg zum beschrifteten „Arbeitszimmer“. Dort erwartete ich den klassischen Anblick eines Schreibtisches, Buchregale und ein paar Notizbücher. Doch was mich hinter der Tür erwartete, war der duftende Innenraum eines gut bestückten Kühlschranks, umgeben von Kochutensilien. Verblüfft verließ ich das vermeintliche Arbeitszimmer und folgte meiner Intuition zur Tür mit dem Schild „Küche“. Doch anstelle eines Herds fand ich ein farbenfrohes Kinderzimmer vor.

Nach einigen weiteren, eher komischen Zimmern und einer kleinen, skurrilen Reise durch das Haus, stolperte ich schließlich im „Gästezimmer“ über den lange gesuchten Schreibtisch. Mit einem erleichterten „Endlich!“ auf den Lippen schreckte ich hoch. Es war nur ein Albtraum gewesen, aber wenigstens mit einem glücklichen Ausgang.

Und die Moral der Geschichte lautet: Die Bezeichnung einer Klasse in der Programmierung sollte präzise widerspiegeln, was sich darin befindet. Ein „Converter“ ist dafür da, zu konvertieren und nicht zu formatieren. Ein „Formatter“ sollte keine Serialisierungsmethoden anbieten, genauso wie ein „DatabaseManager“ sich nicht um die Serialisierung von Daten in XML oder JSON kümmern sollte.

Die Analogien in dieser Kurzgeschichte, in der Reihenfolge ihres Auftretens:

  • Unbekanntes Haus: fremder Code
  • Schriftsteller: Datei- oder Klassen-Name
  • Kugelschreiber und Blatt Papier: spezifische, aber festgelegte Methode(n) mit klar definierten Funktionen
  • Türschilder: Namensräume, Klassen-, Methoden- und Property-Namen
  • Albtraum: Albtraum für den Code-Autor, seine Kollegen, die Nachwelt, die Firma und die Kunden
  • Nachwelt: alle, die nach dem Ausscheiden des Autors aus der Firma in irgendeiner Weise mit dem Code in Berührung kommen.

Wenn der Code-Schreiber keine Ahnung von Compiler hat, sich aber auf ihn und seine Optimierungsalgorithmen verlässt …

Es gibt Code-Schreiberlinge, die glauben tatsächlich, dass „der Compiler eh alles optimiert„.
Andere glauben sogar, dass der Compiler eine gewisse Intelligenz besitzt.
Dann gibt es noch die Code-Schreiberlinge, die glauben, dass es gut ist, wenn sie jede Methode mit einem Try-Catch-Block zu versehen. Nachdem Motto „Bringt nichts, schadet nichts“ und/oder „Doppelt hält besser„.


Schauen wir uns das ganze Mal genauer an.
Wieder mal ein Stück Code aus freie Wildbahn (Industrie):

// Listing 1 (ugly code)

void Main()
{
	Console.WriteLine(GetSomething(0));
}

public double GetSomething(int value)
{
    try
    {
        return 0;
    }
    catch(Exception ex)
    {
        return 0;
    }
}

Man stelle sich nun vor (das wird mir niemand glauben) es gäbe ein SW-System mit hundert und mehr solche Methoden wie „GetSomething(int)„.

Der folgende Code tut exakt das Gleiche, nur viel CPU und Speicher freundlicher, überschaubarer, verständlicher und einfacher zu lesen:

// Listing 2 (better code)

public const double DBL_CONST = 0.0;

void Main()
{
	Console.WriteLine(0);
}

Und nun schauen wir uns mal an, was der Compiler:

  • Optimiert, und es dann
  • in „Intermediate Language“ (IL) Befehle für CLR übersetzt, und anschließen für den echten CPU (Hardware)
  • ins Assembler übersetzt

Fangen wir mal mit Listing 1 (ugly code) an:

Von links nach rechts: C# optimiert, IL, ASM (intel x64)

Man stelle sich vor der Code oben wird in einer Schleife immer wieder aufgerufen …

Und nun schauen wir uns Listing 2 (better code) an:

Von links nach rechts: C# optimiert, IL, ASM (intel x64)

Wie man sieht, wird der CPU bei Listing 1 (ugly code) unnötig beschäftigt. Man könnte meinen, es handle sich um eine „Beschäftigungstherapie“ für den CPU: „Das es eahm jo ned langweilig wird!„.

Wegen solche Code-Schreiberlinge, benötigen wir immer stärkere CPUs, noch mehr Cores pro Sockel, und immer mehr RAM. Von der Energieverschwendung und dem CO₂ Bilanz ganz zu schweigen.

Clean Code: Trennen von Daten aus Algorithmen

Es ist immer eine gute Idee, Daten von Algorithmen zu trennen, um mehr Abstraktion zu schaffen. Dadurch erhält man viele Vorteile, die wir nach ein kurzes, praktisches Beispiel aus dem Alltag sehen werden.

Zuerst schauen wir uns mal, exemplarisch, eine Methode zum Initialisieren einer Datentabelle, namens „settings“ an:

public void InitSettingsValues()
{
    _DataSet.Tables["settings"].Rows[0]["column0"] = false;
    _DataSet.Tables["settings"].Rows[0]["column1"] = true;
    _DataSet.Tables["settings"].Rows[0]["column2"] = null;
    _DataSet.Tables["settings"].Rows[0]["column3"] = 47.11;
    _DataSet.Tables["settings"].Rows[0]["column4"] = "Hello World!";
    _DataSet.Tables["settings"].Rows[0]["column5"] = new DateTime(2001, 9, 11);
    _DataSet.Tables["settings"].Rows[0]["column6"] = 6;
    _DataSet.Tables["settings"].Rows[0]["column7"] = 7;
    _DataSet.Tables["settings"].Rows[0]["column8"] = 8;
    _DataSet.Tables["settings"].Rows[0]["column9"] = 9;
    _DataSet.Tables["settings"].Rows[0]["column10"] = 10;
    //... usw.
    _DataSet.Tables["settings"].Rows[0]["column99"] = 99;
    _DataSet.Tables["settings"].Rows[0]["column100"] = 100;
}

Dieser Code ist alles andere als optimal:

  1. Ist weder leicht lesbar, noch überschaubar
  2. Enthält viele Wiederholungen (DRY: Don’t Repeat Yourself)
  3. Ist nicht abstrakt, sondern explizit („settings“ und 0 als Zeilennummer)
  4. Man kann es nicht wiederverwenden (für die Tabelle „nichtSettings“ muss eigene Methode geschrieben werden)
  5. Man kann es nicht leicht pflegen, erweitern, oder mehrere Varianten davon, nebeneinander erstellen/verwenden/aufrufen (siehe den besseren Code unten):
    • Was tun wir, wenn bei nächstem Release, per Konfiguration, entweder die eine oder andere Methode aufgerufen werden soll? (z.B. für spezielle Kunden, oder Rollback der Version aufgrund gravierende Sicherheitsmangel udg.)
    • Was tun wir, wenn es N (N >= 2) Daten-Varianten gibt (der eine hat X Spalten mehr/weniger)?
  6. Man kann nicht wirklich von „Algorithmus“ sprechen
  7. Der Code für CPU und RAM wird unnötig aufgeblasen, und lässt sich schlecht optimieren (siehe IL und Assembler Listings für Intel x64 zum Vergleichen am Ende des Beitrags)


Und nun trennen wir die Daten von Algorithmus:

// Daten.
private (string ColumnName, object Value)[] _SettingsInitValues = { ("column0", false), ("column1", true), ("column2", null), ("column3", 47.11), ("column4", "Hello World!"), ("column5", new DateTime(2001, 9, 11)), ("column6", 6), ("column7", 7), ("column8", 8), ("column9", 9), ("column10", 10),
    //... usw.
    ("column99", 99), ("column100", 100)
};

// Algorithmus.
public void SetTableValues(DataSet pDataSet, string pTableName, int pRowIndex, IEnumerable<(string ColumnName, object Value)> pColumnNameValueTuples)
{
    foreach (var tuple in pColumnNameValueTuples)
    {
        pDataSet.Tables[pTableName].Rows[pRowIndex][tuple.ColumnName] = tuple.Value;
    }
}

Die Methode besteht nun aus die Signatur plus 2 Zeilen, und bietet folgende Qualitätsmerkmale:

  1. Leichter lesbar: 2 Zeilen
  2. Überschaubar: 2 Zeilen
  3. Verständlicher/Selbsterklärend: 2 Zeilen
  4. Abstrakter: kann auch für andere Tabellen des „_DataSet“ oder sogar andere DataSets verwendet werden
  5. Leichter zu pflegen
  6. Leichter zu Debuggen
  7. Leichter zu erweitern
  8. Grundsätzlich: leichter zu ändern
  9. Man kann weitere Daten-Varianten nebeneinander einsetzen

Mit diesem Code, brauchen wir uns keine Sorgen machen, denn:

  • Es ist wiederverwendbar (für andere DataSets, Tabellen, Zeilen-Index)
  • Falls es für Kunde X oder Version Y andere Daten-Varianten genommen werden müssen: kein Problem
  • Falls es gleichzeitig unterschiedliche Daten-Varianten für unterschiedliche SW-Versionen genommen werden müssen: kein Problem!
  • Das Gleiche für die Nutzung unterschiedliche Daten-Varianten per Konfiguration: kein Problem!
  • Sollte aus welchem Grund auch immer, ein Rollback der SW-Version unternommen werden muss: kein Problem!
  • Sollte Microsoft in der nächste .NET Framework, Core, NET 9 usw. irgendwas an DataSet, deren Tabelle, Zeile oder Spalten ändern: kein Problem! Wir müssen nur eine einzige Zeile ändern

Wie man hier sieht, kann man für 10 verschiedene Versionen/Kunden, die Settings-Tabelle per Konfiguration mit 10 unterschiedliche Daten-Werte initialisieren, OHNE unsere 2 Zeilen Code ändern zu müssen:

private (string ColumnName, object Value)[] _Vers_1_0 = ("column0", 0), ("column1", "dw4mrkzy") /*... usw. */};
private (string ColumnName, object Value)[] _Vers_1_1 = ("column0", 2), ("column1", "LR0_uClc") /*... usw. */};
private (string ColumnName, object Value)[] _Vers_1_2 = ("column0", 0), ("column1", "SR1Vj5ts") /*... usw. */};
private (string ColumnName, object Value)[] _Vers_1_3 = ("column0", 2), ("column1", "hiKlLG55") /*... usw. */};
private (string ColumnName, object Value)[] _Vers_1_4 = ("column0", 8), ("column1", "s2Z4MkKZ") /*... usw. */};
private (string ColumnName, object Value)[] _Vers_1_5 = ("column0", 0), ("column1", "qXZM0x4h") /*... usw. */};
private (string ColumnName, object Value)[] _Vers_1_6 = ("column0", 0), ("column1", "UQryY_SV") /*... usw. */};
private (string ColumnName, object Value)[] _Vers_1_7 = ("column0", 0), ("column1", "CB80QEcy") /*... usw. */};
private (string ColumnName, object Value)[] _Vers_1_8 = ("column0", 8), ("column1", "S59sdhFc") /*... usw. */};
private (string ColumnName, object Value)[] _Vers_1_9 = ("column0", 7), ("column1", "FSgCWXii") /*... usw. */};
private (string ColumnName, object Value)[] _Vers_1_10 = ("column0", 3), ("column1", "hdNvQ8Wt") /*... usw. */};

Die Unterschiede in IL:

Zuerst der IL Code von „SetTableValues(…)“ (optimaler Code):

Die letzte Zeilennummer (in Hex) ist 0x0057 = 87 dez.

IL_0000  nop   
IL_0001  nop   
IL_0002  ldarg.3   
IL_0003  callvirt  IEnumerable <ValueTuple<String,Object>>.GetEnumerator () 
IL_0008  stloc.0   
IL_0009  br.s  IL_0042 
IL_000B  ldloc.0   
IL_000C  callvirt  IEnumerator <ValueTuple<String,Object>>.get_Current () 
IL_0011  stloc.1     // tuple 
IL_0012  nop   
IL_0013  ldarg.0   
IL_0014  ldfld  UserQuery._DataSet 
IL_0019  callvirt  DataSet.get_Tables () 
IL_001E  ldarg.1   
IL_001F  callvirt  DataTableCollection.get_Item (String) 
IL_0024  callvirt  DataTable.get_Rows () 
IL_0029  ldarg.2   
IL_002A  callvirt  DataRowCollection.get_Item (Int32) 
IL_002F  ldloc.1     // tuple 
IL_0030  ldfld  ValueTuple <String, Object>.Item1 
IL_0035  ldloc.1     // tuple 
IL_0036  ldfld  ValueTuple <String, Object>.Item2 
IL_003B  callvirt  DataRow.set_Item (String, Object) 
IL_0040  nop   
IL_0041  nop   
IL_0042  ldloc.0   
IL_0043  callvirt  IEnumerator.MoveNext () 
IL_0048  brtrue.s  IL_000B 
IL_004A  leave.s  IL_0057 
IL_004C  ldloc.0   
IL_004D  brfalse.s  IL_0056 
IL_004F  ldloc.0   
IL_0050  callvirt  IDisposable.Dispose () 
IL_0055  nop   
IL_0056  endfinally   
IL_0057  ret  

Der IL Code von „InitSettingsValues()“ (nicht optimaler Code):

Die letzte Zeilennummer (in Hex) lautet: 0x0291 = 657 dez.

IL_0000  nop   
IL_0001  ldarg.0   
IL_0002  ldfld  UserQuery._DataSet 
IL_0007  callvirt  DataSet.get_Tables () 
IL_000C  ldstr  "settings" 
IL_0011  callvirt  DataTableCollection.get_Item (String) 
IL_0016  callvirt  DataTable.get_Rows () 
IL_001B  ldc.i4.0   
IL_001C  callvirt  DataRowCollection.get_Item (Int32) 
IL_0021  ldstr  "column0" 
IL_0026  ldc.i4.0   
IL_0027  box  Boolean 
IL_002C  callvirt  DataRow.set_Item (String, Object) 
IL_0031  nop   
IL_0032  ldarg.0   
IL_0033  ldfld  UserQuery._DataSet 
IL_0038  callvirt  DataSet.get_Tables () 
IL_003D  ldstr  "settings" 
IL_0042  callvirt  DataTableCollection.get_Item (String) 
IL_0047  callvirt  DataTable.get_Rows () 
IL_004C  ldc.i4.0   
IL_004D  callvirt  DataRowCollection.get_Item (Int32) 
IL_0052  ldstr  "column1" 
IL_0057  ldc.i4.1   
IL_0058  box  Boolean 
IL_005D  callvirt  DataRow.set_Item (String, Object) 
IL_0062  nop   
IL_0063  ldarg.0   
IL_0064  ldfld  UserQuery._DataSet 
IL_0069  callvirt  DataSet.get_Tables () 
IL_006E  ldstr  "settings" 
IL_0073  callvirt  DataTableCollection.get_Item (String) 
IL_0078  callvirt  DataTable.get_Rows () 
IL_007D  ldc.i4.0   
IL_007E  callvirt  DataRowCollection.get_Item (Int32) 
IL_0083  ldstr  "column2" 
IL_0088  ldnull   
IL_0089  callvirt  DataRow.set_Item (String, Object) 
IL_008E  nop   
IL_008F  ldarg.0   
IL_0090  ldfld  UserQuery._DataSet 
IL_0095  callvirt  DataSet.get_Tables () 
IL_009A  ldstr  "settings" 
IL_009F  callvirt  DataTableCollection.get_Item (String) 
IL_00A4  callvirt  DataTable.get_Rows () 
IL_00A9  ldc.i4.0   
IL_00AA  callvirt  DataRowCollection.get_Item (Int32) 
IL_00AF  ldstr  "column3" 
IL_00B4  ldc.r8  AE 47 E1 7A 14 8E 47 40  // 47,11 
IL_00BD  box  Double 
IL_00C2  callvirt  DataRow.set_Item (String, Object) 
IL_00C7  nop   
IL_00C8  ldarg.0   
IL_00C9  ldfld  UserQuery._DataSet 
IL_00CE  callvirt  DataSet.get_Tables () 
IL_00D3  ldstr  "settings" 
IL_00D8  callvirt  DataTableCollection.get_Item (String) 
IL_00DD  callvirt  DataTable.get_Rows () 
IL_00E2  ldc.i4.0   
IL_00E3  callvirt  DataRowCollection.get_Item (Int32) 
IL_00E8  ldstr  "column4" 
IL_00ED  ldstr  "Hello World!" 
IL_00F2  callvirt  DataRow.set_Item (String, Object) 
IL_00F7  nop   
IL_00F8  ldarg.0   
IL_00F9  ldfld  UserQuery._DataSet 
IL_00FE  callvirt  DataSet.get_Tables () 
IL_0103  ldstr  "settings" 
IL_0108  callvirt  DataTableCollection.get_Item (String) 
IL_010D  callvirt  DataTable.get_Rows () 
IL_0112  ldc.i4.0   
IL_0113  callvirt  DataRowCollection.get_Item (Int32) 
IL_0118  ldstr  "column5" 
IL_011D  ldc.i4  D1 07 00 00  // 2001 
IL_0122  ldc.i4.s  09  // 9 
IL_0124  ldc.i4.s  0B  // 11 
IL_0126  newobj  DateTime..ctor 
IL_012B  box  DateTime 
IL_0130  callvirt  DataRow.set_Item (String, Object) 
IL_0135  nop   
IL_0136  ldarg.0   
IL_0137  ldfld  UserQuery._DataSet 
IL_013C  callvirt  DataSet.get_Tables () 
IL_0141  ldstr  "settings" 
IL_0146  callvirt  DataTableCollection.get_Item (String) 
IL_014B  callvirt  DataTable.get_Rows () 
IL_0150  ldc.i4.0   
IL_0151  callvirt  DataRowCollection.get_Item (Int32) 
IL_0156  ldstr  "column6" 
IL_015B  ldc.i4.6   
IL_015C  box  Int32 
IL_0161  callvirt  DataRow.set_Item (String, Object) 
IL_0166  nop   
IL_0167  ldarg.0   
IL_0168  ldfld  UserQuery._DataSet 
IL_016D  callvirt  DataSet.get_Tables () 
IL_0172  ldstr  "settings" 
IL_0177  callvirt  DataTableCollection.get_Item (String) 
IL_017C  callvirt  DataTable.get_Rows () 
IL_0181  ldc.i4.0   
IL_0182  callvirt  DataRowCollection.get_Item (Int32) 
IL_0187  ldstr  "column7" 
IL_018C  ldc.i4.7   
IL_018D  box  Int32 
IL_0192  callvirt  DataRow.set_Item (String, Object) 
IL_0197  nop   
IL_0198  ldarg.0   
IL_0199  ldfld  UserQuery._DataSet 
IL_019E  callvirt  DataSet.get_Tables () 
IL_01A3  ldstr  "settings" 
IL_01A8  callvirt  DataTableCollection.get_Item (String) 
IL_01AD  callvirt  DataTable.get_Rows () 
IL_01B2  ldc.i4.0   
IL_01B3  callvirt  DataRowCollection.get_Item (Int32) 
IL_01B8  ldstr  "column8" 
IL_01BD  ldc.i4.8   
IL_01BE  box  Int32 
IL_01C3  callvirt  DataRow.set_Item (String, Object) 
IL_01C8  nop   
IL_01C9  ldarg.0   
IL_01CA  ldfld  UserQuery._DataSet 
IL_01CF  callvirt  DataSet.get_Tables () 
IL_01D4  ldstr  "settings" 
IL_01D9  callvirt  DataTableCollection.get_Item (String) 
IL_01DE  callvirt  DataTable.get_Rows () 
IL_01E3  ldc.i4.0   
IL_01E4  callvirt  DataRowCollection.get_Item (Int32) 
IL_01E9  ldstr  "column9" 
IL_01EE  ldc.i4.s  09  // 9 
IL_01F0  box  Int32 
IL_01F5  callvirt  DataRow.set_Item (String, Object) 
IL_01FA  nop   
IL_01FB  ldarg.0   
IL_01FC  ldfld  UserQuery._DataSet 
IL_0201  callvirt  DataSet.get_Tables () 
IL_0206  ldstr  "settings" 
IL_020B  callvirt  DataTableCollection.get_Item (String) 
IL_0210  callvirt  DataTable.get_Rows () 
IL_0215  ldc.i4.0   
IL_0216  callvirt  DataRowCollection.get_Item (Int32) 
IL_021B  ldstr  "column10" 
IL_0220  ldc.i4.s  0A  // 10 
IL_0222  box  Int32 
IL_0227  callvirt  DataRow.set_Item (String, Object) 
IL_022C  nop   
IL_022D  ldarg.0   
IL_022E  ldfld  UserQuery._DataSet 
IL_0233  callvirt  DataSet.get_Tables () 
IL_0238  ldstr  "settings" 
IL_023D  callvirt  DataTableCollection.get_Item (String) 
IL_0242  callvirt  DataTable.get_Rows () 
IL_0247  ldc.i4.0   
IL_0248  callvirt  DataRowCollection.get_Item (Int32) 
IL_024D  ldstr  "column99" 
IL_0252  ldc.i4.s  63  // 99 
IL_0254  box  Int32 
IL_0259  callvirt  DataRow.set_Item (String, Object) 
IL_025E  nop   
IL_025F  ldarg.0   
IL_0260  ldfld  UserQuery._DataSet 
IL_0265  callvirt  DataSet.get_Tables () 
IL_026A  ldstr  "settings" 
IL_026F  callvirt  DataTableCollection.get_Item (String) 
IL_0274  callvirt  DataTable.get_Rows () 
IL_0279  ldc.i4.0   
IL_027A  callvirt  DataRowCollection.get_Item (Int32) 
IL_027F  ldstr  "column100" 
IL_0284  ldc.i4.s  64  // 100 
IL_0286  box  Int32 
IL_028B  callvirt  DataRow.set_Item (String, Object) 
IL_0290  nop   
IL_0291  ret  

Die Intel x64 Assembler Unterschiede:

Zuerst der Assembler Code von „SetTableValues(…)“ (optimaler Code):

Die letzte Zeilennummer (in Hex) ist 0x017d = 575 dez.

L0000 push rbp 
L0001 sub rsp, 0x80 
L0008 lea rbp, [rsp+0x80] 
L0010 xor eax, eax 
L0012 mov [rbp-0x58], rax 
L0016 xorps xmm4, xmm4 
L0019 movaps [rbp-0x50], xmm4 
L001d movaps [rbp-0x40], xmm4 
L0021 movaps [rbp-0x30], xmm4 
L0025 movaps [rbp-0x20], xmm4 
L0029 movaps [rbp-0x10], xmm4 
L002d mov [rbp-0x60], rsp 
L0031 mov [rbp+0x10], rcx 
L0035 mov [rbp+0x18], rdx 
L0039 mov [rbp+0x20], r8d 
L003d mov [rbp+0x28], r9 
L0041 mov rax, 0x7ffd31adc320 
L004b cmp dword ptr [rax], 0 
L004e je short L0055 
L0050 call 0x00007ffd906485f0 
L0055 nop  
L0056 nop  
L0057 mov rcx, [rbp+0x28] 
L005b mov r11, 0x7ffd31ad7020 
L0065 mov rax, 0x7ffd31ad7020 
L006f call qword ptr [rax] 
L0071 mov [rbp-0x20], rax 
L0075 mov rcx, [rbp-0x20] 
L0079 mov [rbp-8], rcx 
L007d nop  
L007e jmp L0106 
L0083 lea rdx, [rbp-0x38] 
L0087 mov rcx, [rbp-8] 
L008b mov r11, 0x7ffd31ad7030 
L0095 mov rax, 0x7ffd31ad7030 
L009f call qword ptr [rax] 
L00a1 movups xmm0, [rbp-0x38] 
L00a5 movups [rbp-0x18], xmm0 
L00a9 nop  
L00aa mov rcx, [rbp+0x10] 
L00ae mov rcx, [rcx+0x10] 
L00b2 cmp [rcx], ecx 
L00b4 call System.Data.DataSet.get_Tables() 
L00b9 mov [rbp-0x40], rax 
L00bd mov rcx, [rbp-0x40] 
L00c1 mov rdx, [rbp+0x18] 
L00c5 cmp [rcx], ecx 
L00c7 call System.Data.DataTableCollection.get_Item(System.String) 
L00cc mov [rbp-0x48], rax 
L00d0 mov rcx, [rbp-0x48] 
L00d4 cmp [rcx], ecx 
L00d6 call System.Data.DataTable.get_Rows() 
L00db mov [rbp-0x50], rax 
L00df mov rcx, [rbp-0x50] 
L00e3 mov edx, [rbp+0x20] 
L00e6 cmp [rcx], ecx 
L00e8 call System.Data.DataRowCollection.get_Item(Int32) 
L00ed mov [rbp-0x58], rax 
L00f1 mov rcx, [rbp-0x58] 
L00f5 mov rdx, [rbp-0x18] 
L00f9 mov r8, [rbp-0x10] 
L00fd cmp [rcx], ecx 
L00ff call System.Data.DataRow.set_Item(System.String, System.Object) 
L0104 nop  
L0105 nop  
L0106 mov rcx, [rbp-8] 
L010a mov r11, 0x7ffd31ad7028 
L0114 mov rax, 0x7ffd31ad7028 
L011e call qword ptr [rax] 
L0120 mov [rbp-0x24], eax 
L0123 cmp dword ptr [rbp-0x24], 0 
L0127 jne L0083 
L012d nop  
L012e jmp short L0130 
L0130 mov rcx, rsp 
L0133 call L0140 
L0138 nop  
L0139 nop  
L013a lea rsp, [rbp] 
L013e pop rbp 
L013f ret  
L0140 push rbp 
L0141 sub rsp, 0x30 
L0145 mov rbp, [rcx+0x20] 
L0149 mov [rsp+0x20], rbp 
L014e lea rbp, [rbp+0x80] 
L0155 cmp qword ptr [rbp-8], 0 
L015a je short L0177 
L015c mov rcx, [rbp-8] 
L0160 mov r11, 0x7ffd31ad7038 
L016a mov rax, 0x7ffd31ad7038 
L0174 call qword ptr [rax] 
L0176 nop  
L0177 nop  
L0178 add rsp, 0x30 
L017c pop rbp 
L017d ret 

Der Assembler Code von „InitSettingsValues()“ (nicht optimaler Code):

Die letzte Zeilennummer (in Hex) lautet: 0x0843 = 2115 dez.

L0000 push rbp 
L0001 sub rsp, 0x1e0 
L0008 lea rbp, [rsp+0x1e0] 
L0010 xorps xmm4, xmm4 
L0013 movaps [rbp-0x1c0], xmm4 
L001a mov rax, 0xfffffffffffffe50 
L0024 movaps [rax+rbp], xmm4 
L0028 movaps [rbp+rax+0x10], xmm4 
L002d movaps [rbp+rax+0x20], xmm4 
L0032 add rax, 0x30 
L0036 jne short L0024 
L0038 mov [rbp+0x10], rcx 
L003c mov rax, 0x7ffd3193c320 
L0046 cmp dword ptr [rax], 0 
L0049 je short L0050 
L004b call 0x00007ffd906485f0 
L0050 nop  
L0051 mov rcx, [rbp+0x10] 
L0055 mov rcx, [rcx+0x10] 
L0059 cmp [rcx], ecx 
L005b call System.Data.DataSet.get_Tables() 
L0060 mov [rbp-8], rax 
L0064 mov rdx, 0x1b5d3cd23a8 
L006e mov rdx, [rdx] 
L0071 mov rcx, [rbp-8] 
L0075 cmp [rcx], ecx 
L0077 call System.Data.DataTableCollection.get_Item(System.String) 
L007c mov [rbp-0x10], rax 
L0080 mov rcx, [rbp-0x10] 
L0084 cmp [rcx], ecx 
L0086 call System.Data.DataTable.get_Rows() 
L008b mov [rbp-0x18], rax 
L008f mov rcx, [rbp-0x18] 
L0093 xor edx, edx 
L0095 cmp [rcx], ecx 
L0097 call System.Data.DataRowCollection.get_Item(Int32) 
L009c mov [rbp-0x20], rax 
L00a0 mov rcx, 0x7ffd30967238 
L00aa call 0x00007ffd9050af00 
L00af mov [rbp-0x28], rax 
L00b3 mov r8, [rbp-0x28] 
L00b7 mov byte ptr [r8+8], 0 
L00bc mov r8, [rbp-0x28] 
L00c0 mov rdx, 0x1b5d3ceb6b8 
L00ca mov rdx, [rdx] 
L00cd mov rcx, [rbp-0x20] 
L00d1 cmp [rcx], ecx 
L00d3 call System.Data.DataRow.set_Item(System.String, System.Object) 
L00d8 nop  
L00d9 mov rcx, [rbp+0x10] 
L00dd mov rcx, [rcx+0x10] 
L00e1 cmp [rcx], ecx 
L00e3 call System.Data.DataSet.get_Tables() 
L00e8 mov [rbp-0x30], rax 
L00ec mov rdx, 0x1b5d3cd23a8 
L00f6 mov rdx, [rdx] 
L00f9 mov rcx, [rbp-0x30] 
L00fd cmp [rcx], ecx 
L00ff call System.Data.DataTableCollection.get_Item(System.String) 
L0104 mov [rbp-0x38], rax 
L0108 mov rcx, [rbp-0x38] 
L010c cmp [rcx], ecx 
L010e call System.Data.DataTable.get_Rows() 
L0113 mov [rbp-0x40], rax 
L0117 mov rcx, [rbp-0x40] 
L011b xor edx, edx 
L011d cmp [rcx], ecx 
L011f call System.Data.DataRowCollection.get_Item(Int32) 
L0124 mov [rbp-0x48], rax 
L0128 mov rcx, 0x7ffd30967238 
L0132 call 0x00007ffd9050af00 
L0137 mov [rbp-0x28], rax 
L013b mov r8, [rbp-0x28] 
L013f mov byte ptr [r8+8], 1 
L0144 mov r8, [rbp-0x28] 
L0148 mov rdx, 0x1b5d3ceb6c0 
L0152 mov rdx, [rdx] 
L0155 mov rcx, [rbp-0x48] 
L0159 cmp [rcx], ecx 
L015b call System.Data.DataRow.set_Item(System.String, System.Object) 
L0160 nop  
L0161 mov rcx, [rbp+0x10] 
L0165 mov rcx, [rcx+0x10] 
L0169 cmp [rcx], ecx 
L016b call System.Data.DataSet.get_Tables() 
L0170 mov [rbp-0x50], rax 
L0174 mov rdx, 0x1b5d3cd23a8 
L017e mov rdx, [rdx] 
L0181 mov rcx, [rbp-0x50] 
L0185 cmp [rcx], ecx 
L0187 call System.Data.DataTableCollection.get_Item(System.String) 
L018c mov [rbp-0x58], rax 
L0190 mov rcx, [rbp-0x58] 
L0194 cmp [rcx], ecx 
L0196 call System.Data.DataTable.get_Rows() 
L019b mov [rbp-0x60], rax 
L019f mov rcx, [rbp-0x60] 
L01a3 xor edx, edx 
L01a5 cmp [rcx], ecx 
L01a7 call System.Data.DataRowCollection.get_Item(Int32) 
L01ac mov [rbp-0x68], rax 
L01b0 mov rdx, 0x1b5d3ceb6c8 
L01ba mov rdx, [rdx] 
L01bd mov rcx, [rbp-0x68] 
L01c1 xor r8d, r8d 
L01c4 cmp [rcx], ecx 
L01c6 call System.Data.DataRow.set_Item(System.String, System.Object) 
L01cb nop  
L01cc mov rcx, [rbp+0x10] 
L01d0 mov rcx, [rcx+0x10] 
L01d4 cmp [rcx], ecx 
L01d6 call System.Data.DataSet.get_Tables() 
L01db mov [rbp-0x70], rax 
L01df mov rdx, 0x1b5d3cd23a8 
L01e9 mov rdx, [rdx] 
L01ec mov rcx, [rbp-0x70] 
L01f0 cmp [rcx], ecx 
L01f2 call System.Data.DataTableCollection.get_Item(System.String) 
L01f7 mov [rbp-0x78], rax 
L01fb mov rcx, [rbp-0x78] 
L01ff cmp [rcx], ecx 
L0201 call System.Data.DataTable.get_Rows() 
L0206 mov [rbp-0x80], rax 
L020a mov rcx, [rbp-0x80] 
L020e xor edx, edx 
L0210 cmp [rcx], ecx 
L0212 call System.Data.DataRowCollection.get_Item(Int32) 
L0217 mov [rbp-0x88], rax 
L021e mov rcx, 0x7ffd3096e688 
L0228 call 0x00007ffd9050af00 
L022d mov [rbp-0x28], rax 
L0231 mov r8, [rbp-0x28] 
L0235 movsd xmm0, [UserQuery.InitSettingsValues()] 
L023d movsd [r8+8], xmm0 
L0243 mov r8, [rbp-0x28] 
L0247 mov rdx, 0x1b5d3ceb6d0 
L0251 mov rdx, [rdx] 
L0254 mov rcx, [rbp-0x88] 
L025b cmp [rcx], ecx 
L025d call System.Data.DataRow.set_Item(System.String, System.Object) 
L0262 nop  
L0263 mov rcx, [rbp+0x10] 
L0267 mov rcx, [rcx+0x10] 
L026b cmp [rcx], ecx 
L026d call System.Data.DataSet.get_Tables() 
L0272 mov [rbp-0x90], rax 
L0279 mov rdx, 0x1b5d3cd23a8 
L0283 mov rdx, [rdx] 
L0286 mov rcx, [rbp-0x90] 
L028d cmp [rcx], ecx 
L028f call System.Data.DataTableCollection.get_Item(System.String) 
L0294 mov [rbp-0x98], rax 
L029b mov rcx, [rbp-0x98] 
L02a2 cmp [rcx], ecx 
L02a4 call System.Data.DataTable.get_Rows() 
L02a9 mov [rbp-0xa0], rax 
L02b0 mov rcx, [rbp-0xa0] 
L02b7 xor edx, edx 
L02b9 cmp [rcx], ecx 
L02bb call System.Data.DataRowCollection.get_Item(Int32) 
L02c0 mov [rbp-0xa8], rax 
L02c7 mov r8, 0x1b5d3ceb6e0 
L02d1 mov r8, [r8] 
L02d4 mov rdx, 0x1b5d3ceb6d8 
L02de mov rdx, [rdx] 
L02e1 mov rcx, [rbp-0xa8] 
L02e8 cmp [rcx], ecx 
L02ea call System.Data.DataRow.set_Item(System.String, System.Object) 
L02ef nop  
L02f0 mov rcx, [rbp+0x10] 
L02f4 mov rcx, [rcx+0x10] 
L02f8 cmp [rcx], ecx 
L02fa call System.Data.DataSet.get_Tables() 
L02ff mov [rbp-0xb0], rax 
L0306 mov rdx, 0x1b5d3cd23a8 
L0310 mov rdx, [rdx] 
L0313 mov rcx, [rbp-0xb0] 
L031a cmp [rcx], ecx 
L031c call System.Data.DataTableCollection.get_Item(System.String) 
L0321 mov [rbp-0xb8], rax 
L0328 mov rcx, [rbp-0xb8] 
L032f cmp [rcx], ecx 
L0331 call System.Data.DataTable.get_Rows() 
L0336 mov [rbp-0xc0], rax 
L033d mov rcx, [rbp-0xc0] 
L0344 xor edx, edx 
L0346 cmp [rcx], ecx 
L0348 call System.Data.DataRowCollection.get_Item(Int32) 
L034d mov [rbp-0xc8], rax 
L0354 xor ecx, ecx 
L0356 mov [rbp-0xd0], rcx 
L035d lea rcx, [rbp-0xd0] 
L0364 mov edx, 0x7d1 
L0369 mov r8d, 9 
L036f mov r9d, 0xb 
L0375 call System.DateTime..ctor(Int32, Int32, Int32) 
L037a mov rdx, 0x1b5d3ceb6e8 
L0384 mov rdx, [rdx] 
L0387 mov [rbp-0x1c0], rdx 
L038e lea rdx, [rbp-0xd0] 
L0395 mov rcx, 0x7ffd30bef900 
L039f call 0x00007ffd9050af40 
L03a4 mov [rbp-0x1b8], rax 
L03ab mov r8, [rbp-0x1b8] 
L03b2 mov rdx, [rbp-0x1c0] 
L03b9 mov rcx, [rbp-0xc8] 
L03c0 cmp [rcx], ecx 
L03c2 call System.Data.DataRow.set_Item(System.String, System.Object) 
L03c7 nop  
L03c8 mov rcx, [rbp+0x10] 
L03cc mov rcx, [rcx+0x10] 
L03d0 cmp [rcx], ecx 
L03d2 call System.Data.DataSet.get_Tables() 
L03d7 mov [rbp-0xd8], rax 
L03de mov rdx, 0x1b5d3cd23a8 
L03e8 mov rdx, [rdx] 
L03eb mov rcx, [rbp-0xd8] 
L03f2 cmp [rcx], ecx 
L03f4 call System.Data.DataTableCollection.get_Item(System.String) 
L03f9 mov [rbp-0xe0], rax 
L0400 mov rcx, [rbp-0xe0] 
L0407 cmp [rcx], ecx 
L0409 call System.Data.DataTable.get_Rows() 
L040e mov [rbp-0xe8], rax 
L0415 mov rcx, [rbp-0xe8] 
L041c xor edx, edx 
L041e cmp [rcx], ecx 
L0420 call System.Data.DataRowCollection.get_Item(Int32) 
L0425 mov [rbp-0xf0], rax 
L042c mov rcx, 0x7ffd3096b258 
L0436 call 0x00007ffd9050af00 
L043b mov [rbp-0x28], rax 
L043f mov r8, [rbp-0x28] 
L0443 mov dword ptr [r8+8], 6 
L044b mov r8, [rbp-0x28] 
L044f mov rdx, 0x1b5d3ceb6f0 
L0459 mov rdx, [rdx] 
L045c mov rcx, [rbp-0xf0] 
L0463 cmp [rcx], ecx 
L0465 call System.Data.DataRow.set_Item(System.String, System.Object) 
L046a nop  
L046b mov rcx, [rbp+0x10] 
L046f mov rcx, [rcx+0x10] 
L0473 cmp [rcx], ecx 
L0475 call System.Data.DataSet.get_Tables() 
L047a mov [rbp-0xf8], rax 
L0481 mov rdx, 0x1b5d3cd23a8 
L048b mov rdx, [rdx] 
L048e mov rcx, [rbp-0xf8] 
L0495 cmp [rcx], ecx 
L0497 call System.Data.DataTableCollection.get_Item(System.String) 
L049c mov [rbp-0x100], rax 
L04a3 mov rcx, [rbp-0x100] 
L04aa cmp [rcx], ecx 
L04ac call System.Data.DataTable.get_Rows() 
L04b1 mov [rbp-0x108], rax 
L04b8 mov rcx, [rbp-0x108] 
L04bf xor edx, edx 
L04c1 cmp [rcx], ecx 
L04c3 call System.Data.DataRowCollection.get_Item(Int32) 
L04c8 mov [rbp-0x110], rax 
L04cf mov rcx, 0x7ffd3096b258 
L04d9 call 0x00007ffd9050af00 
L04de mov [rbp-0x28], rax 
L04e2 mov r8, [rbp-0x28] 
L04e6 mov dword ptr [r8+8], 7 
L04ee mov r8, [rbp-0x28] 
L04f2 mov rdx, 0x1b5d3ceb6f8 
L04fc mov rdx, [rdx] 
L04ff mov rcx, [rbp-0x110] 
L0506 cmp [rcx], ecx 
L0508 call System.Data.DataRow.set_Item(System.String, System.Object) 
L050d nop  
L050e mov rcx, [rbp+0x10] 
L0512 mov rcx, [rcx+0x10] 
L0516 cmp [rcx], ecx 
L0518 call System.Data.DataSet.get_Tables() 
L051d mov [rbp-0x118], rax 
L0524 mov rdx, 0x1b5d3cd23a8 
L052e mov rdx, [rdx] 
L0531 mov rcx, [rbp-0x118] 
L0538 cmp [rcx], ecx 
L053a call System.Data.DataTableCollection.get_Item(System.String) 
L053f mov [rbp-0x120], rax 
L0546 mov rcx, [rbp-0x120] 
L054d cmp [rcx], ecx 
L054f call System.Data.DataTable.get_Rows() 
L0554 mov [rbp-0x128], rax 
L055b mov rcx, [rbp-0x128] 
L0562 xor edx, edx 
L0564 cmp [rcx], ecx 
L0566 call System.Data.DataRowCollection.get_Item(Int32) 
L056b mov [rbp-0x130], rax 
L0572 mov rcx, 0x7ffd3096b258 
L057c call 0x00007ffd9050af00 
L0581 mov [rbp-0x28], rax 
L0585 mov r8, [rbp-0x28] 
L0589 mov dword ptr [r8+8], 8 
L0591 mov r8, [rbp-0x28] 
L0595 mov rdx, 0x1b5d3ceb700 
L059f mov rdx, [rdx] 
L05a2 mov rcx, [rbp-0x130] 
L05a9 cmp [rcx], ecx 
L05ab call System.Data.DataRow.set_Item(System.String, System.Object) 
L05b0 nop  
L05b1 mov rcx, [rbp+0x10] 
L05b5 mov rcx, [rcx+0x10] 
L05b9 cmp [rcx], ecx 
L05bb call System.Data.DataSet.get_Tables() 
L05c0 mov [rbp-0x138], rax 
L05c7 mov rdx, 0x1b5d3cd23a8 
L05d1 mov rdx, [rdx] 
L05d4 mov rcx, [rbp-0x138] 
L05db cmp [rcx], ecx 
L05dd call System.Data.DataTableCollection.get_Item(System.String) 
L05e2 mov [rbp-0x140], rax 
L05e9 mov rcx, [rbp-0x140] 
L05f0 cmp [rcx], ecx 
L05f2 call System.Data.DataTable.get_Rows() 
L05f7 mov [rbp-0x148], rax 
L05fe mov rcx, [rbp-0x148] 
L0605 xor edx, edx 
L0607 cmp [rcx], ecx 
L0609 call System.Data.DataRowCollection.get_Item(Int32) 
L060e mov [rbp-0x150], rax 
L0615 mov rcx, 0x7ffd3096b258 
L061f call 0x00007ffd9050af00 
L0624 mov [rbp-0x28], rax 
L0628 mov r8, [rbp-0x28] 
L062c mov dword ptr [r8+8], 9 
L0634 mov r8, [rbp-0x28] 
L0638 mov rdx, 0x1b5d3ceb708 
L0642 mov rdx, [rdx] 
L0645 mov rcx, [rbp-0x150] 
L064c cmp [rcx], ecx 
L064e call System.Data.DataRow.set_Item(System.String, System.Object) 
L0653 nop  
L0654 mov rcx, [rbp+0x10] 
L0658 mov rcx, [rcx+0x10] 
L065c cmp [rcx], ecx 
L065e call System.Data.DataSet.get_Tables() 
L0663 mov [rbp-0x158], rax 
L066a mov rdx, 0x1b5d3cd23a8 
L0674 mov rdx, [rdx] 
L0677 mov rcx, [rbp-0x158] 
L067e cmp [rcx], ecx 
L0680 call System.Data.DataTableCollection.get_Item(System.String) 
L0685 mov [rbp-0x160], rax 
L068c mov rcx, [rbp-0x160] 
L0693 cmp [rcx], ecx 
L0695 call System.Data.DataTable.get_Rows() 
L069a mov [rbp-0x168], rax 
L06a1 mov rcx, [rbp-0x168] 
L06a8 xor edx, edx 
L06aa cmp [rcx], ecx 
L06ac call System.Data.DataRowCollection.get_Item(Int32) 
L06b1 mov [rbp-0x170], rax 
L06b8 mov rcx, 0x7ffd3096b258 
L06c2 call 0x00007ffd9050af00 
L06c7 mov [rbp-0x28], rax 
L06cb mov r8, [rbp-0x28] 
L06cf mov dword ptr [r8+8], 0xa 
L06d7 mov r8, [rbp-0x28] 
L06db mov rdx, 0x1b5d3ceb710 
L06e5 mov rdx, [rdx] 
L06e8 mov rcx, [rbp-0x170] 
L06ef cmp [rcx], ecx 
L06f1 call System.Data.DataRow.set_Item(System.String, System.Object) 
L06f6 nop  
L06f7 mov rcx, [rbp+0x10] 
L06fb mov rcx, [rcx+0x10] 
L06ff cmp [rcx], ecx 
L0701 call System.Data.DataSet.get_Tables() 
L0706 mov [rbp-0x178], rax 
L070d mov rdx, 0x1b5d3cd23a8 
L0717 mov rdx, [rdx] 
L071a mov rcx, [rbp-0x178] 
L0721 cmp [rcx], ecx 
L0723 call System.Data.DataTableCollection.get_Item(System.String) 
L0728 mov [rbp-0x180], rax 
L072f mov rcx, [rbp-0x180] 
L0736 cmp [rcx], ecx 
L0738 call System.Data.DataTable.get_Rows() 
L073d mov [rbp-0x188], rax 
L0744 mov rcx, [rbp-0x188] 
L074b xor edx, edx 
L074d cmp [rcx], ecx 
L074f call System.Data.DataRowCollection.get_Item(Int32) 
L0754 mov [rbp-0x190], rax 
L075b mov rcx, 0x7ffd3096b258 
L0765 call 0x00007ffd9050af00 
L076a mov [rbp-0x28], rax 
L076e mov r8, [rbp-0x28] 
L0772 mov dword ptr [r8+8], 0x63 
L077a mov r8, [rbp-0x28] 
L077e mov rdx, 0x1b5d3ceb718 
L0788 mov rdx, [rdx] 
L078b mov rcx, [rbp-0x190] 
L0792 cmp [rcx], ecx 
L0794 call System.Data.DataRow.set_Item(System.String, System.Object) 
L0799 nop  
L079a mov rcx, [rbp+0x10] 
L079e mov rcx, [rcx+0x10] 
L07a2 cmp [rcx], ecx 
L07a4 call System.Data.DataSet.get_Tables() 
L07a9 mov [rbp-0x198], rax 
L07b0 mov rdx, 0x1b5d3cd23a8 
L07ba mov rdx, [rdx] 
L07bd mov rcx, [rbp-0x198] 
L07c4 cmp [rcx], ecx 
L07c6 call System.Data.DataTableCollection.get_Item(System.String) 
L07cb mov [rbp-0x1a0], rax 
L07d2 mov rcx, [rbp-0x1a0] 
L07d9 cmp [rcx], ecx 
L07db call System.Data.DataTable.get_Rows() 
L07e0 mov [rbp-0x1a8], rax 
L07e7 mov rcx, [rbp-0x1a8] 
L07ee xor edx, edx 
L07f0 cmp [rcx], ecx 
L07f2 call System.Data.DataRowCollection.get_Item(Int32) 
L07f7 mov [rbp-0x1b0], rax 
L07fe mov rcx, 0x7ffd3096b258 
L0808 call 0x00007ffd9050af00 
L080d mov [rbp-0x28], rax 
L0811 mov r8, [rbp-0x28] 
L0815 mov dword ptr [r8+8], 0x64 
L081d mov r8, [rbp-0x28] 
L0821 mov rdx, 0x1b5d3ceb720 
L082b mov rdx, [rdx] 
L082e mov rcx, [rbp-0x1b0] 
L0835 cmp [rcx], ecx 
L0837 call System.Data.DataRow.set_Item(System.String, System.Object) 
L083c nop  
L083d nop  
L083e lea rsp, [rbp] 
L0842 pop rbp 
L0843 ret 

Dazu mehr ist auf Wikipedia in Separation of Concerns zu lesen.

Schiache Pfuscher-Code, im Vergleich zu sauberer Professioneller-Code #1

Für J.E.
Wünsche dir alles Gute und viel Erfolg bei deinem Studium.


Diesen Code gab es tatsächlich, und ist nicht aus der Luft gegriffen.
Schaue dir diesen Code mal gründlich in Ruhe an. Was fällt dir auf?

/// <summary>
/// Gibt einen wert zurück
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public object GetValue(string name) 
{
    try
    {
        if (_DataSet == null) return null;

        if (_DataSet.Tables.Contains("Settings"))
        {
            return Convert.ChangeType(
                _DataSet.Tables["Settings"].Rows[0][name],
                _DataSet.Tables["Settings"].Columns[name].DataType);
        }
    }
    catch(Exception ex)
    {
        return null;
    }

    return "";
}

  1. Kommentar:
    • Ist nicht auf Englisch
    • Sagt nichts Neues aus, somit sagt nichts aus
  2. Rückgabe-Typ:
    • Unklarer/Vager Rückgabe-Typ (ein Objekt kann alles Mögliche sein: Animal, bool, Gott, Welt, …). Der Aufrufer muss Unboxing betreiben (falls der Wert nicht NULL ist, wie wir sehen werden)
  3. Der Name der Methode:
    • ist weder klar noch eindeutig
    • lässt vermuten, dass keine Ausnahmefehler abgefangen/behandelt werden
    • Der Aufrufer wird selbst, nicht notwendigerweise, Try-Catch-Block herumbauen
  4. NULL-Rückgabe:
    • Was macht der Aufrufer, wenn er für Einstellungsname „enabled“, true oder false erwartet, jedoch einen NULL bekommt?
    • Was machen 100 aufrufende Stellen im Code? 1000?
    • Wie oft muss aufrufender Code, die NULL-Eventualität prüfen, um darauf richtig zu reagieren?
    • Was passiert mit 100 aufrufende Stellen im Code, wenn diese Methode ausgebessert wird?
  5. Mehrfaches Erzeugen und Verwenden von inhaltlich gleichem String:
    • Erzeugen von Strings ist teuer. Warum also dreimal das Gleiche?
    • Was passiert, wenn der DB-Entwickler die Tabelle in „Config“ umbenennt? Wie findet man diese Stellen überall im Code? Wie oft muss man dann der Name aktualisieren?
  6. Rückgabe-Typ: Angenommen „DataType“ = Single oder Double …
    • Der float/double Wert wird geboxt, damit der Aufrufer es wieder unboxt? Boxing/Unboxing ist ein teurer Spaß!
  7. Try-Catch-Block:
    • Wozu? Was soll schiefgehen? Welcher Ausnahmefehler soll da geworfen werden? Eine DB-Tabelle mit Einstellungen taucht (in ein ordentliches Schema und SW-System) nicht einfach auf, und verschwindet einfach so!
  8. Exception-Typ und Variable:
    • Wozu? Was wird mit den Informationen in „ex“ gemacht?
  9. Rückgabe-Typ: wurde schon weiter oben thematisiert
  10. Rückgabe-Typ:
    • Aha! Ach, so ist das! Warum wird auf einmal eine leere Zeichenkette zurückgeliefert? Welcher Aufrufer rechnet damit?
    • Was macht ein Aufrufer, der, zum Beispiel auf die true/false Werte wartet, mit einer leeren Zeichenkette?

So ein Code kann man auch sauber und professionell gestalten

Principle Of Least Surprise : POLS

Sauberer Code weil:

  1. Die Namen sind klar und eindeutig, lassen weder Vermutungen noch Überraschungen zu:
    • Get Value Or Null ⇒ liefert einen Wert oder NULL zurück, schmeißt aber keine Ausnahmefehler. Deswegen müssen an zig oder hunderte Stellen, keine Try-Catch-Blocks geschrieben werden.
    • Get Value ⇒ liefert einen Wert oder schmeißt einen Ausnahmefehler
    • Get Value Or Default (generisch) ⇒ liefert einen Wert oder den Standardwert:
      • bool: false
      • byte/sbyte/short/ushort/int/unit/long/ulong: 0
      • float/double/decimal: 0.0
      • string: null
      • schmeißt keine Ausnahmefehler
    • Get Value Or Default (generisch) ⇒ liefert einen Wert oder den von Aufrufer bestimmter Standardwert, jedoch keine Ausnahmefehler geworfen
    • Get Value (generisch) ⇒ liefert einen Wert oder wirft einen Ausnahmefehler
  2. Es werden Alternativen angeboten (siehe Unterpunkte von Punkt 1!)
  3. Der Rückgabe-Typ ist bei alternative generische Funktionen von Aufrufer festgesetzt/bestimmt und eindeutig
  4. Die Rückgabe-Typen sind alle konsistent: entweder einen Wert oder Ausnahmefehler, Wert oder NULL, Wert oder Standardwert ⇒ keine Überraschungen
  5. Bei Aufruf von generische Varianten sind keine Unboxings auf der Aufrufer-Seite notwendig

Gibt es da unten eine Methode, welche ein Kommentar braucht?

public class CleanCode
{
    public const string TNAME_SETTINGS = @"Settings";
    private DataSet _DataSet;

    // 1. Variante: Objekt als Return-Value. Ist POLS-Konform.
    // Tut das Gleiche wie oben, nur viel einfacher (ohne leere Strings)
    // Die Aufrufer müssen sich um Exceptions und NULL kümmern.
    // Ansonsten, verlässlicher als der Code von oben (trotzdem NICHT GUT!)
    public object GetValue(string pSettingName, Type pType)
    {
        var value = _DataSet.Tables[TNAME_SETTINGS].Rows[0][pSettingName];
        return Convert.ChangeType(value, pType);
    }

    // 2. Variante: Objekt als Return-Value. Ist POLS-Konform.
    public object GetValueOrNull(string pSettingName)
    {
        if (_DataSet == null
            || !_DataSet.Tables.Contains(TNAME_SETTINGS)
            || _DataSet.Tables[TNAME_SETTINGS].Rows.Count == 0
            || !_DataSet.Tables[TNAME_SETTINGS].Columns.Contains(pSettingName))
            return null;

        return _DataSet.Tables[TNAME_SETTINGS].Rows[0][pSettingName];
    }

    // 3. Variante: Generischen Typ. Ist POLS-Konform.
    // Sollte etwas nicht passen oder schiefgehen, erhält der Aufrufer
    // den Standardwert des Typs.
    public T GetValueOrDefault<T>(string pSettingName)
    {
        if (_DataSet == null
            || !_DataSet.Tables.Contains(TNAME_SETTINGS)
            || _DataSet.Tables[TNAME_SETTINGS].Rows.Count == 0
            || !_DataSet.Tables[TNAME_SETTINGS].Columns.Contains(pSettingName))
            return default;

        try
        {
            var value = _DataSet.Tables[TNAME_SETTINGS].Rows[0][pSettingName];
            return (T)Convert.ChangeType(value, typeof(T));
        }
        catch
        {
            return default;
        }
    }

    // 4. Variante: Generischen Typ. Ist POLS-Konform.
    // Die Aufrufer können selbst entscheiden welchen Wert Sie erhalten möchten,
    // falls diese an den Standardwert des Typs nicht sind.
    public T GetValueOrDefault<T>(string pSettingName, T pDefault = default)
    {
        if (_DataSet == null
            || !_DataSet.Tables.Contains(TNAME_SETTINGS)
            || _DataSet.Tables[TNAME_SETTINGS].Rows.Count == 0
            || !_DataSet.Tables[TNAME_SETTINGS].Columns.Contains(pSettingName))
            return pDefault;

        try
        {
            var value = _DataSet.Tables[TNAME_SETTINGS].Rows[0][pSettingName];
            return (T)Convert.ChangeType(value, typeof(T));
        }
        catch
        {
            return pDefault;
        }
    }

    // 5. Variante: Generischer Typ. Ist POLS-Konform.
    public T GetValue<T>(string pSettingName)
    {
        var value = _DataSet.Tables[TNAME_SETTINGS].Rows[0][pSettingName];
        return (T)Convert.ChangeType(value, typeof(T));
    }
}

Code vs. Buch

Roman/Buch vs. Code/Programm lesen/schreiben:

Jeder kann Bücher lesen. Nicht jeder der Bücher lesen kann, kann auch ein (gutes/Bestseller) Buch schreiben.
Jeder kann Code schreiben. Nicht jeder der Code schreiben kann, kann auch Code lesen.

(Zitat von meinem geschätzten ehemaligen Kollege Manfred)

Damit man ein gutes Buch schreiben kann, muss man viele gute Bücher gelesen haben.
Damit man guter Code schreiben kann, muss man viele (IT) Bücher sowie Code gelesen haben.

Ein Autor bzw. eine Autorin schreibt Bücher für die Leserinnen.
Ein Software-Entwickler schreibt Code für:

  1. Sich selbst, da er/sie es vielleicht in einem Monat oder einem Jahr wieder lesen, verstehen und ändern muss
  2. für seine Kolleginnen, da sie es vielleicht lesen, verstehen und ändern müssen
  3. für die „Nachwelt“ (neue Kolleginnen in der Zukunft, wenn man nicht mehr in der Firma ist)
  4. und zum Schluss für den Compiler und CPU (oder JVM, CLR oder sonstige virtuelle Maschinen)

Wenn die Dämme brechen

In jede Firma, jede Abteilung und auf jedem Tisch und in jedem Kanban-System, gibt es einen unsichtbaren Damm. Hinter dieser Damm sammelt sich ein trüber stinkender Schlamm, bestehend aus all die Fehler, falsch oder schlecht angegangene/gelöste Aufgaben, falsche Annahmen, falsche Entscheidungen, falsch (nicht)kommunizierte oder (nicht)verstandene Dinge, und all die nicht erledigte aber wichtige Dinge.
Irgendwann bricht einer dieser Dämme und der Kaskadeneffekt beginnt. Der Aufgaben-Flut zeigt nun den Schlamm, wie ein Orkan die Existenz der Luft:

  • es müssen immer mehr Entwickler eingestellt werden
  • die Entwickler müssen immer mehr Code von bestehenden Komponenten ändern (oder hinzufügen)
  • die APIs und Framework-Komponenten ändern sich wöchentlich oder gar täglich
  • die Entwickler betreiben immer wieder Reengineering
  • die Entwickler müssen immer mehr Überstunden machen
  • die Entwickler lesen mehr Code (Implementierung-Details) als sie schreiben (der Kunde zahlt nur fürs Schreiben!)
  • die Entwickler werden täglich, ja sogar stündlich über bestimmte Bugs oder Problemen bei Kunden befragt
  • die Entwickler müssen immer mehr Telefonate, E-Mails udg. beantworten, und werden dadurch in ihrem Gedanken-Fluss unterbrochen
  • die Entwickler können sich nicht einmal für zwei Stunden, Zeit für tiefgründiges Nachdenken/Überlegen nehmen
  • wöchentlich, täglich, ja sogar stündlich, gibt es neue Versionen
  • der Compiler benötigt immer länger fürs Übersetzen (eine Sekunde ist für einen Computer eine Ewigkeit!), die Startzeit mal zwei!
  • das Programm benötigt immer mehr Speicher (HDD & RAM)
  • das Programm wird immer langsamer, und dadurch steigen die Wahrscheinlichkeiten für Nebeneffekte und neue (unerwünschte) Verhalten (vor allem bei komplexen, zeitkritischen Systemen)
  • die benötigten PCs für die Software benötigen immer mehr CPUs (Cores)
  • bei kleinen Anpassungen/Änderungen für einen Kunden, beschwert sich mindestens ein anderer Kunde über neuen Fehler (von einer Funktion, die bis dahin immer tadellos funktionierte)
  • Hotline ist ständig vollbeschäftigt, muss lange Telefonate mit enttäuschten/verärgerten Kunden/User führen
  • Techniker, Service-, Troubleshooting- und 3rd-Level-Support-Kollegen haben immer mehr zu tun, oder sind ebenfalls ständig beschäftigt
  • man sucht immer mehr und länger nach Fehler/Bugs und unerwartetes/unerwünschtes Verhalten
  • man benötigt für egal welche Tests, immer irgendwelche Hardware (SPS/PLCs, Sensoren, Netzteile, USB-Dongles, spezielles Kabel udg.) und kann diese Tests nicht durchführen, wenn das eine oder andere Hardware fehlt
  • und vieles mehr

Jetzt, wo der Damm gebrochen ist, ist man die Aufgaben-Flut ausgeliefert. Man hat keinerlei Kontrolle über die Aufgaben, wie ein Autofahrer mit über 100 km/h auf Glatteis. Ab jetzt kann man nur taktisch, Ad hoc und „husch husch“ (eher „Pfusch Pfusch“) auf die Aufgaben reagieren. Für strategisches und langfristiges Planen, Entwerfen und Programmieren, sowie dessen (Teil)Automatisierung ist es erstens zu spät, und zweitens keine Zeit da, da die Kunden warten. Das Dach brennt! Die Entwickler springen von einer dringenden Aufgabe zu Nächste, wie ein Ping-Pong-Ball. Die Entwickler laufen gestresst von einem Bugfix/Reengineering zum Anderen, wie eine Feuerwehr-Truppe die zig Brände an unterschiedlichen Orten gleichzeitig löschen muss.
Die Kosten steigen. Für neue Projekte ist kaum Zeit da. Für Modernisierung (z. B. Umstieg von Windows Forms auf WPF/UWP) schon gar nicht. Ein Liefertermin nach dem Anderen wird überschritten. Die Firma beginnt nun auch noch Pönalen zu zahlen. Die Kunden sind verärgert und unzufrieden. Die Inhaber, CEOs, CFOs, Abteilungsleiter und Team-Leader sind gestresst und verärgert. Die Entwickler auch. Eine: lose-lose-lose Situation.
Man hat sich Niemals-Endende-Baustellen geschaffen.
Da kann man sich selbst gratulieren.

The Good, the Bad and the Ugly Code(r)

Hässlicher Code ist wie ein Kondom, Taschentuch oder Toilettenpapier. Man kann es nur einmal verwenden.

Guter Code hat einen Mehrwert. Wie die Weihnachtskeks-Schablonen/-Formen kann man ihn immer wieder verwenden/einsetzen.

Nomen est omen: Eine Funktion oder Prozedur tut genau das, was der Name sagt, nicht mehr und nicht weniger. Gilt auch für Namensräume, Klassen, Events, Variablen etc. Der Name muss 100%ig selbsterklärend, eindeutig und unmissverständlich sein.

Eleganter Code ist: kurz, klar, einfach, eindeutig, einprägsam und effizient. Alles andere ist plumper (dahin-gerotzter) Code.

Intelligente Software-Entwickler schreiben den einfachsten, kürzesten und flachsten Code (Eleganz!).
Schlechte Software-Entwickler schreiben die kompliziertesten, längsten Zeilen und mehrfach verschachtelter Code.

Faule, aber clevere Software-Entwickler haben jeden Tag weniger zu tun als der Tag zuvor, während fleißige, aber schlechte Software-Entwickler jeden Tag gleich viel oder sogar mehr als der Tag zuvor tun müssen. (Siehe „Wenn die Dämme brechen“!)

Guter Code wird getestet, denn nur guter Code kann getestet werden. Schlechter Code kann gar nicht getestet werden. Die Aussagekraft der Tests von schlechtem Code = 0.

Code-Optimierung erfolgt erst nach vollständiger Implementierung und erfolgreichem Testen, nicht davor oder währenddessen.

To comment, or not to comment – that is the question

Eine kurze Geschichte übers Kommentieren und Kommentare

Ich wünsche die Kollegen beim Neuschreiben des Codes in C, C++, C#, Java,… oder WasAuchImmer++ viel Geduld, Kraft und Nervenstärke.
Wenn Kommentare genauso kryptisch sind, wie der Code
Was machen diese 15 Zeilen?
Man kann die Kommentare übersetzen und erahnen was diese Zeilen tun

Back to the Future…
Kommentare sind wichtige Informationen für die Zukunft.
Kommentare sind wichtige Informationen aus der Vergangenheit.
Kommentare sind Notizen/Erklärungs-Ergänzungen für sich selbst, Kolleginnen und die Nachwelt (neue Kolleginnen, wenn man nicht mehr in der Firma (oder am Leben) ist).

Ich lese immer wieder Sätze mit Rufzeichen am Ende, wie „Jede Code-Zeile muss kommentiert werden!“, oder „Pro Methode, mindestens drei Zeilen Kommentare!“ usw.

An der Uni hieß es „Alles muss kommentiert werden!“.

Dann hieß es (eine Show namens „Linux Nerds vs. Geeks“ glaube ich) „Wenn man schönen/guten Code schreibt, braucht man keine Kommentare“ (sagte der Linux Grafik-Programmierer-Guru!).

Irgendwann sagte ein ehemaliger Kollege „Kommentare verwirren nur“ und dass „Nur Amateure und Doofis kommentieren“ …

Mein derzeitiger Standpunkt ist:

  1. Extrem ist und bleibt extrem. Daher entscheide ich mich für die goldene Mitte.
  2. Bevor ich Kommentiere stelle ich mir folgende Fragen:
    1. Muss ich Kommentieren?
    2. Reicht der Name nicht?
    3. Ist der Name gut gewählt?
    4. Sollte ich nicht lieber den Namen ändern?
    5. Wie groß ist der Informationsgehalt des Kommentars?
    6. Bietet der Kommentar eine zusätzliche oder ergänzende Information, die sonst nicht (oder sehr schwer) zu lesen/sehen/wissen (Code) wäre?
    7. Erleichtert/Verkürzt der Kommentar die Suche/Recherche oder das Verständnis des Algorithmus?
    8. Lässt der Kommentar Raum für falsche Interpretationen zu?
    9. Hilft der Kommentar Fehler zu minimieren, oder verursacht es mehr Fehler durch Missverständnis?

Nehmen wir den (C#) Code von Unten als Beispiel und stellen wir uns gemeinsam pro Kommentar die obigen neun Fragen unter Punkt zwei:

public class Person
{
    /// <summary>
    /// Gets/Sets the ID of Person.
    /// </summary>
    public int ID { get; set; }

    /// <summary>
    /// Gets/Sets the first/given name of Person.
    /// </summary>
    public string FirstName { get; set; }

    /// <summary>
    /// Gets/Sets the last/second name of Person.
    /// </summary>
    public string LastName { get; set; }

    /// <summary>
    /// Gets/Sets the day of birth of Person.
    /// </summary>
    public DateTime BirthDay { get; set; }

    /// <summary>
    /// Returns the whole/full name beginning with FirstName then LastName in uppercase.
    /// </summary>
    /// <returns></returns>
    public string GetName()
    {
        // todo: check!
        return $"{FirstName} {LastName.ToUpper()}"; // NG! PGH 2021-07-05 V0.8.15
    }
}

Die Property-Namen sagen eindeutig, unmissverständlich und klar aus, was sie beinhalten, und deren Kommentare haben/bringen keine zusätzliche/ergänzende Informationen… Also ⇒ Weg damit!

Der Kommentar von der Methode „GetName“ möchte, und MUSS, etwas erklären, was der Name leider nicht tut. Zusätzlich verwirrt dieser Kommentar.
Innerhalb der Methode „GetName“ steht ein „todo“ Kommentar, was kein Mensch entschlüsseln kann: „check!“ Check-What? Was soll gecheckt/überprüft werden FirstName, LastName oder Return-Value (string)?
Am Ende der Return-Zeile steht wieder kryptische Buchstaben-Suppe:

  1. NG!“ steht für…??? No Guns? No Game? Next Generation? Not Good? No Grade? Network Grid?
  2. Danach steht „PGH„, meine Initialien plus Datum und anscheinend eine Version:
    1. Gibt es für sowas Versionsverwaltungs-Systemen (CVS, SVN und GIT). Dort kann man all das lesen, von Anbeginn der Zeit (Projekt-Geburt) bis zum letzten Commit: Wer (Name, E-Mail-Adresse) hat, wann (Datum + Uhrzeit mit Millisekunden) was (Verzeichnis, Datei, Zeile) in welche Version geändert (gelöscht, hinzugefügt, umbenannt, ergänzt, entfernt,…) hat.
    2. Was sollen die Kolleginnen damit anfangen? Mehrwert? Informationsgehalt?

Das Ganze könnte man auch so schreiben:

public class Person
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDay { get; set; }

    /// <summary>
    /// Returns full name, like: "John DOE", "Alice BOYLE".
    /// </summary>
    /// <returns></returns>
    public string GetFullName()
    {
        return $"{FirstName} {LastName?.ToUpper()}";
    }
}

Ich habe diese Beispiele nicht aus der Luft gegriffen.
Ähnliche, und teilweise noch schlimmere Fälle aus der Praxis (was mir unter die Augen gekommen sind) werde ich noch hinzufügen. Entweder hier oder im neuen Beitrag.

Englisch, Deutsch oder Denglisch?

Der Kunde zahlt fürs Code-Schreiben und nicht fürs Code-Lesen!

In einer Firma, wo alle Software-Entwickler und IT’ler Deutschsprachige waren, hat der Inhaber & CEO uns befohlen alle Kommentare (nur Kommentare) auf Deutsch zu schreiben. Ein Jahr später, nachdem das Projekt im Verzug war und die Entwicklung des Software-Systems nicht schnell genug vorangegangen war, wurden drei neue Software-Entwickler aus Spanien eingestellt. Die neuen Kollegen waren alle sehr gute Software-Entwickler und beherrschten souverän ihr Fach, jedoch verstanden kein Wort Deutsch. Danach wurden noch mindestens fünf weitere Software-Entwickler, ebenfalls aus Spanien, eingestellt…
Nun hatten wir alle Hände voll zu tun. Wir drei Deutschsprachigen müssten alle Kommentare nun auf Englisch übersetzen und ersetzen, anstatt unsere Aufgaben nachzugehen.

In eine andere Firma, hatte ich das „Vergnügen“ Code auf Denglisch zu lesen. Manche Namen waren Deutsch, manche waren Englisch, und der Rest eine beliebige Mischung aus Deutsch und Englisch. Dass alle Programm-Elemente (Interfaces, Klassen, Delegates, Events, Methoden, Properties etc.) in Microsoft .NET Frameworks auf US-Englisch waren, ist natürlich trivial…

Das Lesen der Code war extrem schwierig und eine Herausforderung an/für sich. Beispiel für Variable-Namen:
messValue, measWert, measValue, messWert (diese Namen standen für: measured value, bzw. gemessener Wert).
Auch wenn man alle Namen auf Deutsch schreibt, die Hälfte von Code ist und bleibt auf Englisch, da die Schlüsselwörter und alles Andere in MS .NET Frameworks in US-Englisch ist, wie: string.IsNullOrEmpty(…), PropertyChanged, File.Exist(…), using(var x = new MemoryStream(…)), usw. usf.

Deswegen behaupte ich: Es kann kein Code auf „Deutsch“ geschrieben werden, wenn man das versucht, dann entsteht immer ein Code auf „Denglisch“. Man stelle sich nur vor, jemand würde Denglisch schreiben oder reden:
„I war very froh to sehen you nochmal“.

Ich verwende gerne US englische Namen (Color statt Colour, Synchronize statt Synchronise, usw.). Somit bleibt alles einheitlich, unmissverständlich, eindeutig, klar und der Lese-Fluss bleibt sehr flüssig.

Denglisch kostet mehr Zeit, mehr Zeit zum Lesen und mehr Zeit zum Verstehen.
Je öfter der Code von je mehr Kollegen (wieder)gelesen wird, desto mehr Zeit wird verschwendet!
Zeit ist Geld. Somit verursacht Code auf Denglisch unnötige zusätzliche Kosten.

Der Kunde zahlt fürs Code-Schreiben und nicht fürs Code-Lesen!

Naming System & Code lesen

Der Kunde zahlt fürs Code-Schreiben und nicht fürs Code-Lesen!

Um Code schneller lesen und durchsuchen zu können, habe ich im Laufe der Zeit, mein System für die Benennung einige Programmelemente verfeinert:

  • Interfaces beginnen mit „I„, wie: IAuthor, IBook, IPublisher,…

  • Abstrakte Klassen beginnen mit „A„, wie: AAuthor, ABook, APublisher,…

  • Enums beginnen (bei mir) mit „E„, wie: „EOperationMode“, „EError“, „EDeviceType“, „EPeriodKind“,…

  • Helper-Klassen enden mit „Helper„, wie: „FileHelper“, „CommandHelper“, „SecurityHelper“,…

  • Model-Klassen (MVC, MVVM) enden mit „Model„, wie: ServiceModel, MachineModel, ConfigModel, ProductModel, CategoryModel,…

  • DTO-Klassen enden mit „DTO„, wie: PersonDTO, ProductDTO,…

  • POCO-Klassen enden mit „POCO„, wie: PersonPOCO, ProductPOCO,…

  • Delegates enden mit „Handler„, wie: CommandInvokedHandler, ValueChangedHandler, SafeModeActivationChangedHandler, StreamOpeningHandler, StreamOpenedHandler, StreamClosingHandler, StreamClosedHandler, UserLoggedInHandler, UserLoggedOutHandler,…

  • Event-Handling-Methoden beginnen mit „On“ plus Event-Name, und können -wenn passend- mit „ed“ enden, also: „On<EventName>ed„, wie: OnCommandInvoked, OnValueChanged, OnSafeModeActivationChanged, OnStreamOpening, OnStreamOpened, OnStreamClosing, OnStreamClosed, OnUserLoggedIn, OnUserLoggedOut, OnMachineReplaced, OnErrorOccured,…

  • Funktionen/Methode, die etwas Berechnen und eine Zahl zurückgeben beginnen mit „Calc“ oder „Calculate“ (und NIEMALS mit „Get“!), wie: CalculateCircleSurface(…), CalculateStandardDeviation(…) oder kurz: CalcStdDeviation(…)

  • Methoden die asynchron laufen enden mit „Async“, wie: CalcStdDeviationAsync(IEnumerable pTooManyNumbers),…

  • Boolsche Properties beginnen mit „Is„, „Has„, „Use„, „Contains“ udg., wie: „IsEnabled“, „IsReady“, „IsEmpty“, „HasElement“, „ContainsError“, „ContainsNull“, „UseForceMode“, „DoBackup“,…

  • Properties die Exception werfen können, werden NICHT als Properties, sondern mit „Get…()“ und „Set…()“ implementiert (so wie es die C# Sprach-Designer Anders Hejlsberg, Bill Wagner & Co vorgesehen haben), denn ein Property sollte niemals Exceptions werfen, schon gar nicht der Getter. Ausgenommen sind die Indexer (wegen IndexOutOfRangeException).

  • „Get…“ Methoden welche Exceptions abfangen und behandeln enden mit:
    • OrNull“ wenn bei Exception NULL zurückgeliefert wird
    • OrDefault“ wenn bei Exception Default-Wert (z. B. default(int)) zurückgeliefert wird

  • Member-Variablen beginnen mit „m“ oder „m_„, wie: mFirstName oder m_FirstName

  • Parameter beginnen mit „p“ wie: CalcRectSurface(int pWidth, int pHeight), SetTopLeft(int pX, int pY),…

  • Konstanten werden GROSS_GESCHRIEBEN: DEFAULT_VALUE, MAX_VALUE,…

Der Beispiel-Code unten erfüllt die oben erwähnten Regeln.
Versuche herauszufinden: Welcher Name ist ein Property, welcher ein Parameter, welcher eine Member-Variable und welcher eine Konstante:

Konstanten, Parameter, Properties etc. sind eindeutig zu erkennen

Syntax Highlighting & Code lesen

Der Kunde zahlt fürs Code-Schreiben und nicht fürs Code-Lesen!

Die Zeiten wo alles in Grün auf schwarzem Hintergrund stand (Borland Turbo Pascal/CPP Editor udg.) sind lange vorbei.
IDEs wie Visual Studio & Co bieten von Haus aus Syntax Highlighting.
Die Einfärbung der Schlüsselwörter, Kommentare, Typen etc. machen das Lesen von Code leichter, flüssiger und angenehmer.
Für mich jedoch reichte die Einfärbung für C# in Visual Studio nicht.
Ich möchte schnell auf einem Blick, ohne zu lesen erkennen, ob bei ein Programm-Element (in C#) es sich um ein Interface, Klasse, Enum, Delegate, Event oder „Magic Number“ handelt.
Dazu habe ich für Interfaces, Delegates, Enums und Zahlen eigene Farben zugewiesen (siehe Bild 1!)

  1. Kommentare sehe ich deutlicher (Dunkelgrün statt Standard Leichtgrau)
  2. Enums sind eindeutig sofort zu erkennen (Orange)
  3. Delegates sind ebenfalls eindeutig zu erkennen (Violett)
  4. Interfaces haben eine andere Farbe als (abstrakte) Klassen oder Structs
  5. Events sind Braun
  6. Magic Numbers“ haben bei mir keine Chance, da sie sofort erkannt werden 😉
  7. Unterscheidung zwischen Enums und Konstanten sind ebenfalls leichter
Bild 1


So gelangt man zu Farben- und Font-Style von Programm-Elementen in Visual Studio (Bild 2):

Bild 2

Color Theme & Code lesen

Seit Visual Studio 2012 wird „Dark Theme“ (Tools → Options → Environment → General → Color Theme) als Standard verwendet.
Das hat den Vorteil, dass die Augen mit weniger Licht belastet werden.
Jedoch ist es so, dass „Dark Theme“ sich eher für Grafik, 3D und Animations-Editoren eignet, um die Tool-Leisten weg- und das Bild/Modell (Bild, 3D Objekt udg.) in die Mitte zu rücken (dadurch wird automatisch die Fokussierung auf das Bild/Modell geleitet). Für das Lesen und Schreiben von Text (hier Code) ist es eher ungeeignet (laut Augen-Mediziner). Die Augen müssen sich mühsam auf die hellen Buchstaben fokussieren. Vor allem, wenn durch große Fenster, was heutzutage modern ist, das Sonnenlicht auf dem Bildschirm fällt, oder über helle Flächen auf dem Bildschirm reflektiert wird.


Deshalb probiert es selbst aus!
Nachdem ich eine Zeit lang extreme Augen- und Kopfschmerzen hatte, und deshalb zum Augenarzt gehen müsste, bin ich auf dem „Light“ Theme umgestiegen. Das reflektierte Sonnenlicht ist seitdem kein Thema (für mich) mehr.
Für detaillierte Informationen, siehe hier und hier!
Anmerkung: bei OLED und AMOLED Bildschirm, wird oft Dark Theme zwecks Energiesparen eingesetzt.

Augen & Code lesen

Der Kunde zahlt fürs Code-Schreiben und nicht fürs Code-Lesen!

Wenn wir ein Buch, eine Zeitung, ein Magazin oder nur ein A4 Blatt lesen, dann halten wir es so, dass die obere Kante etwas weiter hinten ist, und die untere Kante näher zu uns ist. Beim Lesen nach Unten bewegen sich die Augen weiter nach Innen (zu Nase), und beim Lesen nach Oben bewegen sich die Augen auseinander.
Wir halten ein Buch niemals parallel vor unserem Kopf, wir neigen den Kopf etwas nach Unten.
Kurze Zeilen unter einander machen den Lese-Fluss leichter.
Lange Zeilen stören den Lese-Fluss, da die Augen sich auch horizontal bewegen müssen. Bei sehr lange Zeilen muss sich sogar noch der Kopf sich horizontal drehen.
Je mehr Augenmuskeln beansprucht werden, desto früher wird man Müde (Kopf-Schmerzen). Man kann sich schlecht konzentrieren/fokussieren.
Deshalb ist das Lesen während einer Zug- oder Busfahrt (für längere Zeit) nicht ratsam.

Das gilt auch für Code lesen. Deshalb sollte darauf geachtet werden das beim Code schreiben, die Zeilen möglichst kurz und untereinander stehen.

// Hindert den Lesefluss
if ((a > B && C < D) && !(x == y || y != z)) dataCommunicator.SomeMethod(a, b, c, true);
// Noch mehr Code...

// Macht den Lesefluss leichter
if ((a > B && C < D) && !(x == y || y != z))
    dataCommunicator.SomeMethod(a, b, c, true);
// Noch mehr Code...

Naming-Conventions

Nomen est omen
Namen spielen beim Schreiben von Code eine essenzielle Rolle.
Gut gewählte Namen können viele Grübeleien, Verwirrungen und Missverständnisse sowie Fehler vermeiden, und machen den Code leichter lesbar.
Gute Namen sind:

  • Selbsterklärend
  • Klar
  • Eindeutig
  • Unmissverständlich
  • Einprägsam
  • Sagen genau was das Ding tut oder enthält, nicht mehr und nicht weniger
  • Müssen nicht kommentiert werden

Zum Beispiel hier (C# Code):
Der Klassen-Name sagt aus „Ich enthalte Geometrie-Methoden“, die erste Methode sagt „Ich berechne eine Kreisfläche, wenn du mir die Radius-Länge gibst“, usw.
Man braucht nicht (mit F12 ins Visual Studio) sich den Code innerhalb einer Methode zu lesen, um zu wissen, was es tut.

    public class GeometryCalculator
    {
        public double CalculateCircleSurface(double radiusLength)
        {
            return radiusLength * radiusLength * Math.PI;
        }

        public double CalculateSquareSurface(double sideLength)
        {
            return sideLength * sideLength;
        }
    }

Nomen est omen… eben.

Ugly vs Clean Code (zum Aufwärmen)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Ugly.Code
{
    public class GeoCalc
    {
        public double GetCS(double value)
        {

            double val1;
            double calc;


            val1 = value;
            calc = Math.Pow(val1, 2) * Math.PI;


            return calc;


        }

        public double GetSS(double value)
        {

            double val1;
            double calc;


            val1 = value;
            calc = Math.Pow(val1, 2);


            return calc;
        }

        // Some other methods...
    }
}

Warum ist der Code oben „Ugly“?

  1. Ungenutzte „using“ Zeilen
  2. Der Klassen-Name sagt nicht, was die Klasse enthält/anbietet/tut
  3. Die Methoden-Namen sagen nicht, was sie tun oder berechnen/zurückliefern
  4. Die Parameter-Namen sagen nicht, was sie enthalten
  5. Die lokalen Variable-Namen sagen nicht, was sie enthalten
  6. Lokale Variablen kaschieren die Parameter und dessen Werte, machen den Algorithmus schwer verständlich
  7. Unnötige Leerzeilen verlängern unnötig die Methode. Man muss mehr scrollen und mit den Augen rauf & runterschauen.

Die Klasse „GeoCalc“ von Oben, könnte man auch so schreiben:

using System;

namespace Clean.Code
{
    public class GeometryCalculator
    {
        public double CalculateCircleSurface(double radiusLength)
        {
            return radiusLength * radiusLength * Math.PI;
        }


        public double CalculateSquareSurface(double sideLength)
        {
            return sideLength * sideLength;
        }

        // Some other methods...
    }
}