C# Top-Entwurfsmuster
Design Patterns sind wiederverwendbare Lösungen für gängige Softwareentwicklungsprobleme und bieten Vorlagen, um objektorientierten Code effizienter und wartbarer zu strukturieren und zu implementieren. Sie helfen Entwicklern, Probleme bei der Erstellung, Strukturierung und Kommunikation von Objekten auf flexible und skalierbare Weise zu lösen. Design Patterns dienen als Best-Practice-Konzepte, die Entwickler beim Schreiben von besserem Code anleiten. Eines der grundlegenden Prinzipien bei der Softwareentwicklung ist das Single Responsibility Principle (SRP), das Teil der SOLID-Prinzipien ist.
In seinem Video "Design Patterns: Single Responsibility Principle Explained Practically in C# (The S in SOLID)" geht Tim Corey auf das Single Responsibility Principle (SRP) ein, hebt seine Bedeutung für das Softwaredesign hervor und gibt praktische Einblicke, wie es effektiv umgesetzt werden kann. Dieser Artikel bietet einen kurzen Überblick über die wichtigsten Erkenntnisse aus dem Video und unterstreicht die Bedeutung von SRP für die Erstellung von sauberem, wartbarem Code.
Einführung in SRP
Bei der Softwareentwicklung sind die SOLID-Prinzipien entscheidend für die Erstellung von wartbarem und skalierbarem Code. Sie sorgen dafür, dass der Code leicht zu verstehen, zu testen und zu ändern ist. Die fünf Prinzipien - Single Responsibility Principle (SRP), Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP) und Dependency Inversion Principle (DIP) - sind integraler Bestandteil des objektorientierten Designs und können innerhalb von Design Patterns angewendet werden, um Lösungen robuster zu machen.
Durch die Anwendung von Entwurfsmustern in C# können Entwickler gängige Probleme effektiver lösen. Ob es um die Erstellung von Objekten, die Definition von Baumstrukturen oder die Sicherstellung der Wiederverwendbarkeit mit einzelnen Instanzen geht, Design Patterns bieten vordefinierte Lösungen, die die Softwarearchitektur verbessern. Muster wie die Factory-Methode, Builder und Singleton bieten flexible, wiederverwendbare Lösungen, während Verhaltens- und Strukturmuster helfen, die Komplexität zu verwalten und die Kommunikation innerhalb von Systemen zu verbessern. Durch das Erlernen und die Nutzung dieser Muster können Entwickler Systeme aufbauen, die einfacher zu warten und zu erweitern sind.
Tim erörtert das Konzept von SRP und betont, dass es für Entwickler entscheidend ist, sicherzustellen, dass ihr Code den Best Practices entspricht. SRP besagt, dass eine Klasse nur eine Verantwortung oder einen Grund zum Ändern haben sollte. Dieser Grundsatz trägt dazu bei, einen sauberen, wartbaren und skalierbaren Code zu erhalten.
Demo-Code-Übersicht
Tim richtet eine einfache Konsolenanwendung in C# ein, die nach dem Vor- und Nachnamen des Benutzers fragt, diese Namen validiert und dann einen Benutzernamen generiert. Die ursprüngliche Implementierung verstößt gegen SRP, was eine hervorragende Gelegenheit bietet, zu demonstrieren, wie man Code refaktorisieren kann, um diesen Grundsatz einzuhalten.
SRP Erklärt
Tim erklärt SRP, indem er die verschiedenen Verantwortlichkeiten innerhalb der ursprünglichen Klasse hervorhebt:
- Benutzerinteraktion: Handhabung von Begrüßungsmeldungen und Aufforderungen.
- Datenerfassung: Erfassen des Vor- und Nachnamens des Benutzers.
- Validierung: Validierung der eingegebenen Namen.
- Benutzername generieren: Generierung eines Benutzernamens aus den eingegebenen Namen.
Jede dieser Aufgaben stellt einen anderen Grund für die Änderung der Klasse dar und verstößt gegen SRP.
Refactoring zur Einhaltung von SRP
Tim zeigt, wie man den Code refaktorisiert, um SRP zu folgen, indem jede Verantwortung in eine eigene Klasse extrahiert wird. Dieser Ansatz stellt sicher, dass jede Klasse nur einen einzigen Grund für eine Änderung hat, wodurch der Code modularer und leichter zu pflegen ist.
Praktisches Beispiel
Tim bietet ein praktisches Beispiel für Refactoring:
using System;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Welcome to my application");
Console.Write("Enter your first name: ");
string firstName = Console.ReadLine();
Console.Write("Enter your last name: ");
string lastName = Console.ReadLine();
if (string.IsNullOrWhiteSpace(firstName) || string.IsNullOrWhiteSpace(lastName))
{
Console.WriteLine("You did not give us valid information!");
Console.ReadLine();
return;
}
var userName = $"{firstName.Substring(0, 1)}{lastName}".ToLower();
Console.WriteLine($"Your username is {userName}");
Console.WriteLine("Press enter to close...");
Console.ReadLine();
}
}
using System;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Welcome to my application");
Console.Write("Enter your first name: ");
string firstName = Console.ReadLine();
Console.Write("Enter your last name: ");
string lastName = Console.ReadLine();
if (string.IsNullOrWhiteSpace(firstName) || string.IsNullOrWhiteSpace(lastName))
{
Console.WriteLine("You did not give us valid information!");
Console.ReadLine();
return;
}
var userName = $"{firstName.Substring(0, 1)}{lastName}".ToLower();
Console.WriteLine($"Your username is {userName}");
Console.WriteLine("Press enter to close...");
Console.ReadLine();
}
}
Refactoring Schritt für Schritt
Schritt 1: Erstellen der Klasse StandardMessages
Zunächst erstellt Tim eine Klasse, die Standardmeldungen an den Benutzer verarbeitet. Diese Klasse verwaltet Begrüßungs- und Endnachrichten.
public class StandardMessages
{
public static void WelcomeMessage()
{
Console.WriteLine("Welcome to my application");
}
public static void EndApplication()
{
Console.WriteLine("Press enter to close...");
Console.ReadLine();
}
public static void ShowValidationErrorMessage()
{
Console.WriteLine("You did not give us valid information!");
}
}
public class StandardMessages
{
public static void WelcomeMessage()
{
Console.WriteLine("Welcome to my application");
}
public static void EndApplication()
{
Console.WriteLine("Press enter to close...");
Console.ReadLine();
}
public static void ShowValidationErrorMessage()
{
Console.WriteLine("You did not give us valid information!");
}
}
Ersetzen Sie in der Klasse Program die direkten Aufrufe von Console.WriteLine und Console.ReadLine durch Aufrufe der Methoden der Klasse StandardMessages:
class Program
{
static void Main(string[] args)
{
StandardMessages.WelcomeMessage();
// Other code...
StandardMessages.EndApplication();
}
}
class Program
{
static void Main(string[] args)
{
StandardMessages.WelcomeMessage();
// Other code...
StandardMessages.EndApplication();
}
}
Schritt 2: Erstellen der PersonDataCapture-Klasse
Als Nächstes erstellt Tim eine Klasse für die Erfassung des Vor- und Nachnamens der Person. Diese Klasse ist für die Erfassung von Benutzereingaben und die Rückgabe eines Person-Objekts verantwortlich.
public class PersonDataCapture
{
public static Person Capture()
{
Person output = new Person();
Console.Write("Enter your first name: ");
output.FirstName = Console.ReadLine();
Console.Write("Enter your last name: ");
output.LastName = Console.ReadLine();
return output;
}
}
public class PersonDataCapture
{
public static Person Capture()
{
Person output = new Person();
Console.Write("Enter your first name: ");
output.FirstName = Console.ReadLine();
Console.Write("Enter your last name: ");
output.LastName = Console.ReadLine();
return output;
}
}
Schritt 3: Erstellen der Personenklasse
Sie benötigen außerdem eine Person-Klasse, die den Vor- und Nachnamen des Benutzers enthält.
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
Ersetzen Sie in der Klasse Program die direkte Bearbeitung der Benutzereingabe durch einen Aufruf von PersonDataCapture.Capture:
class Program
{
static void Main(string[] args)
{
StandardMessages.WelcomeMessage();
Person user = PersonDataCapture.Capture();
// Other code...
StandardMessages.EndApplication();
}
}
class Program
{
static void Main(string[] args)
{
StandardMessages.WelcomeMessage();
Person user = PersonDataCapture.Capture();
// Other code...
StandardMessages.EndApplication();
}
}
Schritt 4: Erstellen der PersonValidator-Klasse
Als Nächstes erstellt Tim eine Klasse, die die Validierung des Vor- und Nachnamens der Person übernimmt. Diese Klasse ist dafür verantwortlich, dass die Namen keine Nullen oder Leerzeichen enthalten.
public class PersonValidator
{
public static bool Validate(Person person)
{
if (string.IsNullOrWhiteSpace(person.FirstName))
{
StandardMessages.ShowValidationErrorMessage("first name");
return false;
}
if (string.IsNullOrWhiteSpace(person.LastName))
{
StandardMessages.ShowValidationErrorMessage("last name");
return false;
}
return true;
}
}
public class PersonValidator
{
public static bool Validate(Person person)
{
if (string.IsNullOrWhiteSpace(person.FirstName))
{
StandardMessages.ShowValidationErrorMessage("first name");
return false;
}
if (string.IsNullOrWhiteSpace(person.LastName))
{
StandardMessages.ShowValidationErrorMessage("last name");
return false;
}
return true;
}
}
Ersetzen Sie in der Klasse Program den Validierungscode durch einen Aufruf von PersonValidator.Validate:
class Program
{
static void Main(string[] args)
{
StandardMessages.WelcomeMessage();
Person user = PersonDataCapture.Capture();
if (!PersonValidator.Validate(user))
{
StandardMessages.EndApplication();
return;
}
// Other code...
StandardMessages.EndApplication();
}
}
class Program
{
static void Main(string[] args)
{
StandardMessages.WelcomeMessage();
Person user = PersonDataCapture.Capture();
if (!PersonValidator.Validate(user))
{
StandardMessages.EndApplication();
return;
}
// Other code...
StandardMessages.EndApplication();
}
}
Schritt 7: Erstellen der AccountGenerator-Klasse
Tim verschiebt die Logik für die Generierung von Benutzernamen und die Erstellung von Konten in eine neue AccountGenerator-Klasse.
-
Erstellen der AccountGenerator-Klasse:
- Die Klasse enthält die Logik, um den Benutzernamen zu generieren und die Kontoerstellung zu simulieren.
public class AccountGenerator { public static void CreateAccount(Person user) { string username = $"{user.FirstName.Substring(0, 1)}{user.LastName}".ToLower(); Console.WriteLine($"Your username is: {username}"); } }public class AccountGenerator { public static void CreateAccount(Person user) { string username = $"{user.FirstName.Substring(0, 1)}{user.LastName}".ToLower(); Console.WriteLine($"Your username is: {username}"); } } -
Aktualisierung der Hauptklasse:
- Rufen Sie den AccountGenerator auf, um das Konto in der Hauptklasse zu erstellen.
class Program
{
static void Main(string[] args)
{
StandardMessages.WelcomeMessage();
Person user = PersonDataCapture.Capture();
bool isUserValid = PersonValidator.Validate(user);
if (!isUserValid)
{
StandardMessages.EndApplication();
return;
}
AccountGenerator.CreateAccount(user);
StandardMessages.EndApplication();
}
}
class Program
{
static void Main(string[] args)
{
StandardMessages.WelcomeMessage();
Person user = PersonDataCapture.Capture();
bool isUserValid = PersonValidator.Validate(user);
if (!isUserValid)
{
StandardMessages.EndApplication();
return;
}
AccountGenerator.CreateAccount(user);
StandardMessages.EndApplication();
}
}

Zusammenfassung und Schlussfolgerungen
In diesem abschließenden Abschnitt fasst Tim Corey die Vorteile und die Umsetzung des Single Responsibility Principle (SRP) durch den Refactoring-Prozess des Demo-Codes zusammen. Er unterstreicht die Vorteile einer Aufteilung der Anwendung in kleinere, fokussierte Klassen.
Schlüsselvorteile von SRP
-
Vereinfachte Code-Wartung:
-
Jede Klasse hat eine einzige Zuständigkeit, so dass es einfacher ist, die Stellen zu finden, an denen Änderungen vorgenommen werden müssen. Die Logik der Benutzerdatenerfassung wird beispielsweise eindeutig unter PersonDataCapture eingeordnet.
- Diese Struktur vereinfacht das Verständnis, da jeder, der die Benutzervalidierung ändern möchte, weiß, dass er PersonValidator überprüfen muss.
-
-
Verbesserte Lesbarkeit:
- Mit klar definierten Zuständigkeiten wird der Hauptprogrammfluss besser lesbar. Der Code liest sich jetzt wie eine Reihe klarer, aufeinander folgender Aktionen:
StandardMessages.WelcomeMessage(); Person user = PersonDataCapture.Capture(); bool isUserValid = PersonValidator.Validate(user); if (!isUserValid) { StandardMessages.EndApplication(); return; } AccountGenerator.CreateAccount(user); StandardMessages.EndApplication();StandardMessages.WelcomeMessage(); Person user = PersonDataCapture.Capture(); bool isUserValid = PersonValidator.Validate(user); if (!isUserValid) { StandardMessages.EndApplication(); return; } AccountGenerator.CreateAccount(user); StandardMessages.EndApplication(); -
Reduzierte Komplexität:
-
Kleine Klassen mit konzentrierten Aufgaben haben in der Regel weniger Codezeilen, was sie leichter verständlich und wartbar macht.
- Beispiel: Die Methoden der Klasse StandardMessages sind prägnant und dienen einem einzigen Zweck, z. B. der Anzeige einer Willkommensnachricht oder dem Beenden der Anwendung.
public class StandardMessages { public static void WelcomeMessage() { Console.WriteLine("Welcome to my application"); } public static void EndApplication() { Console.WriteLine("Press enter to close..."); Console.ReadLine(); } public static void ShowValidationErrorMessage(string fieldName) { Console.WriteLine($"You did not give us a valid {fieldName}!"); } }public class StandardMessages { public static void WelcomeMessage() { Console.WriteLine("Welcome to my application"); } public static void EndApplication() { Console.WriteLine("Press enter to close..."); Console.ReadLine(); } public static void ShowValidationErrorMessage(string fieldName) { Console.WriteLine($"You did not give us a valid {fieldName}!"); } }
-
-
Einfache Code-Änderungen:
-
Da es für jede Klasse nur einen einzigen Änderungsgrund gibt, ist die Anpassung des Codes an neue Anforderungen sehr einfach.
- Beispiel: Wenn die Anforderung darin besteht, die Endnachricht zu ändern, erfolgt die Änderung ausschließlich in der Methode StandardMessages.EndApplication.
-
-
Bessere Fehlersuche und Kollaboration:
-
Mit kleineren, klar definierten Klassen wird die Fehlersuche einfacher, da Sie den Ort eines Problems leicht ausfindig machen können.
- Neue Entwickler können sich schneller einarbeiten, da sie die klare Struktur und die Zuständigkeiten der einzelnen Klassen verstehen.
-
Besorgnisse über viele Klassen ansprechen
Tim geht auf die häufige Sorge ein, dass durch die Anwendung von SRP zu viele Klassen entstehen, die das Projekt unübersichtlich machen:
-
Navigation und Verstehen:
-
Tools wie IntelliSense in Visual Studio machen die Navigation in mehreren Klassen einfach. Wenn Sie zum Beispiel F12 drücken, gelangen Sie direkt zur Definition von Methoden oder Klassen.
- Viele kleine, überschaubare Teile können das Verständnis der gesamten Anwendung im Vergleich zu großen monolithischen Klassen erleichtern.
-
-
Leistung und Speicherung:
- Die zusätzlichen Klassen wirken sich nicht wesentlich auf den Speicherplatz oder die Leistung aus, wenn man die modernen Speicher- und Rechenkapazitäten berücksichtigt.
-
Ausgewogenheit und Überschuss :
-
Tim rät, ein Gleichgewicht zu finden. Wenn die Zuständigkeit einer Klasse zu groß wird, sollten Sie prüfen, ob es mehrere Gründe für eine Änderung gibt, was darauf hindeutet, dass eine weitere Unterteilung erforderlich sein könnte.
- Er weist darauf hin, dass, wenn Sie in Visual Studio ausgiebig durch eine Klasse blättern müssen, diese möglicherweise zu groß ist und geteilt werden muss.
-
Praktische Umsetzung
Tim ermutigt Entwickler, SRP schrittweise anzuwenden, insbesondere in bestehenden Codebasen. Beginnen Sie mit kleinen Änderungen und neuem Code, um den SRP-Prinzipien zu entsprechen. Dieser schrittweise Ansatz gewährleistet reibungslosere Übergänge und kontinuierliche Verbesserungen.
Abschluss
Das Refactoring-Beispiel von Tim Corey zeigt, wie die Einhaltung des Single-Responsibility-Prinzips (SRP) zu saubererem, besser wartbarem Code führt. Durch die Aufteilung von Verantwortlichkeiten in kleinere, fokussierte Klassen können Entwickler die Lesbarkeit, das Debugging und die Zusammenarbeit innerhalb ihrer Codebases verbessern. Dieses Grundprinzip der SOLID-Entwurfsmuster ebnet den Weg für fortgeschrittenere Prinzipien und Best Practices in der Softwareentwicklung.
Ausführlichere Informationen und Code-Beispiele finden Sie in seinem Video und in seinem Channel für weitere Design Patterns-Videos.
