Zum Fußzeileninhalt springen
Iron Academy Logo
C# Allgemeine Probleme

Refactoring von bedingten Wenns in C#: Vermeidung von bedingtem Durcheinander mit Derek Comartin

Derek Comartin
11m 03s

In C# sind bedingte Anweisungen wie die if-Anweisung, die if else-Anweisung und die switch-Anweisung wichtige Werkzeuge. Aber was passiert, wenn diese Konstrukte übermäßig verwendet werden - vor allem in Verbindung mit Enums? Derek Comartin, in seinem Video "Enums Aren't Evil. Conditionals Everywhere Are" führt uns durch einen detaillierten Refactor, der die weit verbreitete bedingte Logik durch sauberere, besser wartbare Muster ersetzt.

In diesem Artikel gehen wir Dereks Argumentation Schritt für Schritt durch und verwenden seine Zeitstempel als Anker. Wir werden auch untersuchen, wie sich seine Ideen auf gängige bedingte Muster in C# wie den ternären Operator, die else-Anweisung und switch-case-Strukturen anwenden lassen - und aufzeigen, wo diese in großen Codebasen Probleme verursachen können und wie Sie durch Refactoring ein besseres Design erreichen können.

Die bedingte If-Explosion: Das wahre Problem

Derek beginnt mit der Darstellung einer if-Anweisung, die den Produkttyp überprüft:

if (productType == ProductType.Template || productType == ProductType.Ebook)
if (productType == ProductType.Template || productType == ProductType.Ebook)

Auf den ersten Blick sieht das obige bedingte "if" einfach aus. Derek warnt jedoch davor, dass diese Art von Anweisung eine bestimmte Bedingung auswertet und einen Codeblock nur dann ausführt, wenn die Bedingung erfüllt ist - was problematisch wird, wenn diese Logik überall wiederholt wird.

Sie könnten diesen Block in einer anderen Methode oder Klasse wiederfinden:

if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)
if (offeringType == ProductType.Template || offeringType == ProductType.Ebook)

Derek erklärt, dass sich dieses Muster schnell über ein großes System ausbreitet. Die gleiche if else-Logik erscheint in mehreren Diensten, was zu Inkonsistenzen und Fehlern beim Hinzufügen eines neuen Enum-Wertes führt. Was passiert zum Beispiel, wenn Sie einen neuen Produkttyp wie Video einführen? Sie müssen daran denken, jeden einzelnen Block zu aktualisieren, in dem dieser bedingte Ausdruck vorkommt.

Wiederholung steigert die Komplexität

Im folgenden Beispiel geht Derek auf verschachtelte Konditionale ein. Innerhalb einer Methode prüft eine if else-Anweisung dasselbe Enum, und das Ergebnis wird an eine weitere Methode übergeben, die ebenfalls eine ähnliche Prüfung enthält.

Die Anweisung prüft auf Template oder Ebook und gibt etwas zurück - andernfalls wird null zurückgegeben. Derek merkt an, dass diese Redundanz den Code nicht nur länger macht, sondern auch Wartungsrisiken mit sich bringt. Dieselbe Logik wird in mehrere Dateien kopiert, was zu einem Kontrollfluss-Chaos führt.

Wenn Ihr System verlangt, dass Sie jedes Mal, wenn Sie eine Aufzählung berühren, einen Standardfall hinzufügen müssen, wissen Sie, dass etwas nicht in Ordnung ist.

Die Art, wie wir über Konditionale denken, ändern

Anstatt ständig Typen mit if else zu überprüfen, schlägt Derek vor, eine bessere Frage zu stellen:

Hat das Produkt herunterladbare Eigenschaften?

Dies ist ein besserer Ausdruck der Absicht. Sie macht Ihren Code besser lesbar und verringert die Abhängigkeit von der Enum-Funktionalität. Anstatt eine if-Anweisung mit zwei Bedingungen zu schreiben, wie:

if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)

Sie können diese Logik einfach in einem Modell kapseln und schreiben:

if (product.HasDownloadableResource())
if (product.HasDownloadableResource())

Dies gilt nur dann, wenn eine herunterladbare Ressource vorhanden ist, sodass keine komplexen bedingten Ausdrücke erforderlich sind.

Von If-Anweisungen zu gekapseltem Verhalten

Um das Kernproblem zu lösen, führt Derek einen DownloadableResource-Typ ein. Dieser Typ enthält eine Download-URL und einen Standard-Dateinamen. Sie wird zu einem erstklassigen Teil Ihrer Domäne, anstatt sich auf if-Anweisungen zu stützen, um sie abzuleiten.

Anstatt dies zu wiederholen:

if (product.Type == ProductType.Template)
{
    // Generate file name
}
else if (product.Type == ProductType.Ebook)
{
    // Generate file name
}
if (product.Type == ProductType.Template)
{
    // Generate file name
}
else if (product.Type == ProductType.Ebook)
{
    // Generate file name
}

Sie schreiben dies:

var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
    Console.WriteLine(downloadable.FileName);
}
var downloadable = product.GetDownloadableResource();
if (downloadable != null)
{
    Console.WriteLine(downloadable.FileName);
}

Dies vereinfacht die Logik drastisch und macht Verzweigungen mit else-Anweisungen oder sogar eine switch-Anweisung überflüssig.

Laufzeit statt Kompilierzeit: Eine strategische Verschiebung

Derek geht noch einen Schritt weiter, indem er eine wichtige Design-Entscheidung erläutert: die Verlagerung der Logik von der Kompilierzeit zur Laufzeit. Das bedeutet, dass das System zur Laufzeit abgefragt wird, um festzustellen, ob eine DownloadableResource für ein Produkt existiert. Wenn dies der Fall ist, handeln Sie danach. Wenn nicht, überspringen Sie es.

Er merkt an, dass dieser Schritt statische if else-Logik in Laufzeitabfragen umwandelt. Es könnte ein Datenbankaufruf hinzugefügt werden, aber es reduziert die verschachtelte if else-Logik und zentralisiert das Verhalten. Dies verbessert die Wartungsfreundlichkeit in großem Umfang.

Nutzung der Vererbung für herunterladbare Produkte

Ein weiterer Weg, den Derek erkundet, ist die Vererbung. Sie können eine abstrakte Basisklasse Product erstellen und dann abgeleitete Typen wie Ebook, Template oder OfflineCourse definieren.

Jeder überschreibt Methoden wie:

public virtual string GetDownloadUrl() { ... }
public virtual string GetDownloadUrl() { ... }

Dieser Ansatz ermöglicht es jedem Produkt, seine eigene Logik zu handhaben. Auch wenn dadurch eine Switch-Anweisung oder mehrere bedingte Anweisungen vermieden werden, weist Derek darauf hin, dass man trotzdem intern bedingte Ausdrücke schreiben kann, wenn man nicht vorsichtig ist.

Bessere Verkapselung ohne Vererbung

Wenn sich Vererbung schwerfällig anfühlt, schlägt Derek vor, explizite Typen wie DownloadableProduct zu verwenden, die ihre eigenen Eigenschaften und Methoden enthalten, ohne an eine Hierarchie gebunden zu sein.

In Ihrem Programm könnte dies wie folgt aussehen:

var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());
var downloader = new DownloadableProduct(product);
Console.WriteLine(downloader.GetDefaultFileName());

Es besteht keine Notwendigkeit für eine if else- oder switch-Anweisung, um das Verhalten zu bestimmen - jedes Objekt weiß, was zu tun ist.

Leichtgewichtiger Fix: Erweiterungsmethoden für Enums

Wenn Sie noch nicht bereit sind, Enums aufzugeben, schlägt Derek eine einfache Lösung vor: Erstellen Sie eine Erweiterungsmethode:

public static bool IsDownloadable(this ProductType type)
{
    return type == ProductType.Template || type == ProductType.Ebook;
}
public static bool IsDownloadable(this ProductType type)
{
    return type == ProductType.Template || type == ProductType.Ebook;
}

Anstatt nun zu schreiben:

if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)
if (product.Type == ProductType.Template || product.Type == ProductType.Ebook)

Sie können es vereinfachen zu:

if (product.Type.IsDownloadable())
if (product.Type.IsDownloadable())

Dies zentralisiert Ihre Logik und vermeidet die Wiederholung von geschweiften Klammern und Codeblöcken.

Vermeiden Sie die übermäßige Verwendung von ternären Operatoren und Switch

Derek warnt auch vor der übermäßigen Verwendung von Abkürzungen wie dem ternären Operator:

string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";
string filename = product.Type == ProductType.Template ? "template.pdf" : "default.pdf";

Obwohl es sich um eine gültige Syntax handelt, kann sie fehleranfällig und schwer zu lesen sein, wenn die Logik komplex wird. Vor allem, wenn Ihre Bedingung als falsch ausgewertet wird, könnte der falsche Wert auf subtile Weise zugewiesen werden.

Auch switch mit einer break-Anweisung und einem Standardfall tappt in diese Falle. Es ist besser, Objekte nach ihrem Verhalten zu fragen, als Switch-Case-Logik zu verwenden.

Abschluss: Intelligentere Steuerung mit weniger bedingtem Durcheinander

Zusammenfassend lässt sich sagen, dass Dereks Video kein Angriff auf Enums ist, sondern eine Kritik daran, wie wir bedingte if-Strukturen um sie herum verwenden. Durch die Verbreitung von if else- und switch-Anweisungen in Ihrer Codebasis erschweren Sie das Testen, Warten und Weiterentwickeln des Systems.

Unabhängig davon, ob Sie sich für Kapselung, Runtime Lookup, Vererbung oder einfache Erweiterungsmethoden entscheiden, bleibt das Ziel dasselbe: Reduzierung von Konditionalen und Verlagerung der Logik dorthin, wo sie hingehört.

Nicht vergessen:

  • Konditionale sind nicht schlecht.

  • Bedingte Unordnung ist.

  • Sauberer Code verlässt sich nicht auf mehrere if else-Anweisungen, die über Klassen verstreut sind.

  • Bewerten Sie Ihren Kontext und überarbeiten Sie ihn entsprechend.

Wie Derek sagt: "Es kommt auf den Kontext an." Aber eines ist sicher: Ein Produkt ist nicht immer nur ein Produkt - manchmal ist es auch ein Signal, das Design zu überdenken.

Hero Worlddot related to Refactoring von bedingten Wenns in C#: Vermeidung von bedingtem Durcheinander mit Derek Comartin
Hero Affiliate related to Refactoring von bedingten Wenns in C#: Vermeidung von bedingtem Durcheinander mit Derek Comartin

Verdienen Sie mehr, indem Sie teilen, was Sie lieben

Erstellen Sie Inhalte für Entwickler, die mit .NET, C#, Java, Python oder Node.js arbeiten? Verwandeln Sie Ihr Fachwissen in ein zusätzliches Einkommen!

Iron Support Team

Wir sind 24 Stunden am Tag, 5 Tage die Woche online.
Chat
E-Mail
Rufen Sie mich an