Najlepsze wzorce projektowe C#
Wzorce projektowe są wielokrotnie używanymi rozwiązaniami typowych problemów z zakresu tworzenia oprogramowania, dostarczającymi szablony do strukturyzacji i implementacji kodu obiektowego w bardziej efektywny i łatwiejszy do utrzymania sposób. Pomagają programistom rozwiązywać problemy z tworzeniem obiektów, strukturą i komunikacją w elastyczny i skalowalny sposób. Wzorce projektowe służą jako najlepsze praktyki, które pomagają deweloperom w pisaniu lepszego kodu. Jedną z podstawowych zasad projektowania oprogramowania jest Zasada Pojedynczej Odpowiedzialności (SRP), która jest częścią zasad SOLID.
W swoim wideo, "Design Patterns: Single Responsibility Principle Explained Practically in C# (The S in SOLID)", Tim Corey bada Zasadę Pojedynczej Odpowiedzialności (SRP), podkreślając jej znaczenie w projektowaniu oprogramowania i dostarczając praktyczne wskazówki, jak skutecznie ją wdrażać. Ten artykuł oferuje zwięzły przegląd kluczowych wniosków z jego wideo, podkreślając znaczenie SRP w tworzeniu czystego, łatwego do utrzymania kodu.
Wprowadzenie do SRP
W projektowaniu oprogramowania zasady SOLID są kluczowe dla tworzenia łatwego do utrzymania i skalowania kodu. Zapewniają, że kod jest łatwy do zrozumieniuiuiuiuia, testowania i modyfikacji. Pięć zasad—Zasada Pojedynczej Odpowiedzialności (SRP), Zasada Otwartych/Zamkniętych (OCP), Zasada Podstawienia Liskov (LSP), Zasada Segregacji Interfejsów (ISP) i Zasada Odwrócenia Zależności (DIP)—są integralną częścią projektowania obiektowego i można je zastosować we wzorcach projektowych, aby uczynić rozwiązania bardziej solidnymi.
Stosując wzorce projektowe w C#, programiści mogą skuteczniej rozwiązywać typowe problemy. Niezależnie czy chodzi o tworzenie obiektów, definiowanie struktur drzewiastych, czy zapewnianie reużywalności dla pojedynczych instancji, wzorce projektowe dostarczają wstępnie zdefiniowanych rozwiązań, które zwiększają architekturę oprogramowania. Wzorce takie jak Metoda Fabryki, Konstruktor i Singleton dostarczają elastycznych, wielokrotnego użytku rozwiązań, podczas gdy wzorce behawioralne i strukturalne pomagają zarządzać złożonością i poprawiać komunikację w systemach. Ucząc się i wykorzystując te wzorce, programiści mogą budować systemy, które są łatwiejsze do utrzymania i rozwijania.
Tim omawia koncepcję SRP, podkreślając, że dla programistów kluczowe jest, aby ich kod spełniał najlepsze praktyki. SRP mówi, że klasa powinna mieć tylko jedną odpowiedziąlność lub powód do zmiany. Ta zasada pomaga utrzymać czysty, łatwy do utrzymania i skalowalny kod.
Przegląd kodu demo
Tim konfiguruje prostą aplikację konsolową w C#, która prosi użytkownika o imię i nazwisko, sprawdza te nazwy i generuje nazwę użytkownika. Początkowa implementacja narusza SRP, co stanowi doskonałą okazję do pokazania, jak można zrefaktorować kod, aby spełniał tę zasadę.
SRP Wyjaśnione
Tim wyjaśnia SRP, podkreślając różne odpowiedziąlności w początkowej klasie:
- Interakcja z użytkownikiem: Obsługa komunikatów powitalnych i wskazówek.
- Przechwytywanie danych: Przechwytywanie imienia i nazwiska użytkownika.
- Weryfikacja: Sprawdzanie poprawności wprowadzonych imion.
- Generowanie nazwy użytkownika: Generowanie nazwy użytkownika z wprowadzonych imion.
Każda z tych odpowiedziąlności stanowi inny powód do zmiany klasy, co narusza SRP.
Refaktoryzacja w celu spełnienia SRP
Tim pokazuje, jak zrefaktorować kod, aby przestrzegał SRP, wyodrębniając każdą odpowiedziąlność do osobnej klasy. To podejście zapewnia, że każda klasa ma jeden powód do zmiany, co sprawia, że kod jest bardziej modułowy i łatwiejszy do utrzymania.
Praktyczny przykład
Tim dostarcza praktyczny przykład refaktoryzacji:
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();
}
}
Refaktoryzacja krok po kroku
Krok 1: Tworzenie klasy StandardMessages
Najpierw Tim tworzy klasę do obsługi standardowych komunikatów wyświetlanych użytkownikowi. Ta klasa będzie zarządzać komunikatami powitalnymi i końcowymi.
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!");
}
}
W klasie Program zastąp bezpośrednie wywołania Console.WriteLine i Console.ReadLine wywołaniami metod w klasie 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();
}
}
Krok 2: Tworzenie klasy PersonDataCapture
Następnie Tim tworzy klasę do obsługi przechwytywania imienia i nazwiska osoby. Ta klasa będzie odpowiedziąlna za zbieranie danych od użytkownika i zwracanie obiektu Person.
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;
}
}
Krok 3: Tworzenie klasy Person
Potrzebujesz również klasy Person do przechowywania imienia i nazwiska użytkownika.
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; }
}
W klasie Program zastąp bezpośrednie przetwarzanie danych użytkownika wywołaniem 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();
}
}
Krok 4: Tworzenie klasy PersonValidator
Następnie Tim tworzy klasę do obsługi weryfikacji imienia i nazwiska osoby. Ta klasa będzie odpowiedziąlna za upewnienie się, że nazwy nie są null lub puste.
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;
}
}
W klasie Program zastąp kod weryfikacji wywołaniem 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();
}
}
Krok 7: Tworzenie klasy AccountGenerator
Tim przenosi logikę generowania nazwy użytkownika i tworzenia konta do nowej klasy AccountGenerator.
-
Tworzenie klasy AccountGenerator:
- Klasa zawiera logikę generowania nazwy użytkownika i symulacji tworzenia konta.
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}"); } } -
Aktualizacja klasy głównej:
- Wywołuj AccountGenerator, aby utworzyć konto w klasie głównej.
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();
}
}

Podsumowanie i wnioski
W tej kończącej sekcji Tim Corey podsumowuje korzyści i wdrażanie Zasady Pojedynczej Odpowiedzialności (SRP) poprzez proces refaktoryzacji kodu demo. Podkreśla zalety podziału aplikacji na mniejsze, skoncentrowane klasy.
Kluczowe korzyści SRP
-
Uproszczona konserwacja kodu:
-
Każda klasa ma jedną odpowiedziąlność, co ułatwia lokalizację miejsc do wprowadzenia zmian. Na przykład, logika przechwytywania danych użytkownika jest jednoznacznie umieszczona w PersonDataCapture.
- Ta struktura ułatwia zrozumieniuiuiuiuie, ponieważ każdy, kto chce zmodyfikować weryfikację użytkownika, zna, aby sprawdzić PersonValidator.
-
-
Poprawiona czytelność:
- Z jasno określonymi odpowiedziąlnościami, główny przepływ programu staje się bardziej czytelny. Kod teraz czyta się jak zestaw klarownych, sekwencyjnych działań:
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(); -
Zmniejszona złożoność:
-
Małe klasy z ukierunkowanymi odpowiedziąlnościami mają tendencję do mniejszej liczby linii kodu, co sprawia, że są łatwiejsze do zrozumieniuiuiuiuia i utrzymania.
- Przykład: Metody klasy StandardMessages są zwięzłe i mają jednoznaczne cele, takie jak wyświetlanie komunikatu powitalnego lub kończenie aplikacji.
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}!"); } }
-
-
Łatwość wprowadzania zmian w kodzie:
-
Ponieważ każda klasa ma jeden powód do zmiany, modyfikowanie kodu w odpowiedzi na nowe wymagania staje się prostsze.
- Przykład: Jeśli wymaganiem jest zmiana komunikatu końcowego, zmiana zachodzi wyłącznie w metodzie StandardMessages.EndApplication.
-
-
Lepsze debugowanie i współpraca:
-
Z mniejszymi, dobrze zdefiniowanymi klasami, debugowanie staje się prostsze, ponieważ łatwo określić lokalizację problemu.
- Nowi deweloperzy mogą szybciej się wdrożyć, rozumiejąc jasną strukturę i odpowiedziąlności każdej klasy.
-
Zadbanie o wiele klas
Tim porusza często występującą obawę, że zastosowanie SRP skutkuje zbyt wieloma klasami, co czyni projekt nieporęcznym:
-
Nawigacja i zrozumieniuiuiuiuie:
-
Narzędzia takie jak IntelliSense w Visual Studio ułatwiają nawigację po wielu klasach. Na przykład, naciskając F12, przechodzi się bezpośrednio do definicji metod lub klas.
- Posiadanie wielu małych, zarządzalnych części może ułatwić zrozumieniuiuiuiuie całej aplikacji w porównaniu do dużych klas monolitycznych.
-
-
Wydajność i przechowywanie:
- Dodatkowe klasy nie wpływają znacząco na miejsce na dysku czy wydajność, biorąc pod uwagę nowoczesne możliwości przechowywania i obliczeń.
-
Równowaga i nadmiar:
-
Tim radzi, aby znaleźć równowagę. Jeśli odpowiedziąlność klasy powoduje, że staje się ona zbyt duża, zastanów się, czy ma wiele powodów do zmiany, co może wskazywać na potrzebę dalszego podziału.
- Sugeruje, że jeśli trzeba przescrollować obszernie przez klasę w Visual Studio, może być zbyt duża i wymagać podziału.
-
Praktyczna Implementacja
Tim zachęca programistów do wprowadzania SRP stopniowo, zwłaszcza w istniejących bazach kodu. Zacznij od małych zmian i nowego kodu, aby dostosować się do zasad SRP. Takie stopniowe podejście zapewnia sprawniejsze przejścia i ciągłe doskonalenie.
Wnioski
Przykład refaktoryzacji Tim Coreya pokazuje, jak przestrzeganie Zasady Pojedynczej Odpowiedzialności (SRP) prowadzi do czystszego, łatwiejszego do utrzymania kodu. Dzieląc odpowiedziąlności na mniejsze, skoncentrowane klasy, programiści mogą poprawić czytelność, debugowanie i współpracę w swoich bazach kodu. Ta podstawowa zasada wzorców projektowych SOLID otwiera drogę do bardziej zaawansowanych zasad i najlepszych praktyk w tworzeniu oprogramowania.
Aby uzyskać bardziej szczegółowe informacje i przykłady kodu, obejrzyj jego wideo i odwiedź jego kanał po więcej filmów o wzorcach projektowych.
