Wzorce projektowe w C# i .NET – praktyczny przewodnik dla programistów

devkot.pl 1 dzień temu

Cześć, developerzy, dzisiaj przygotowałem dla Was obszerny przewodnik opisujący wzorce projektowe w C# i .NET. Znajdziesz tu szczegółowe opisy popularnych wzorców, sytuacje, w których warto je stosować, ich plusy i minusy oraz przykładowe implementacje. Na końcu omówimy również najczęstsze antywzorce i wzorce, które mogą przynieść więcej szkody niż pożytku.

Co to są wzorce projektowe?

Wzorce projektowe to sprawdzone rozwiązania powtarzalnych problemów projektowych w programowaniu obiektowym. Stosowanie ich umożliwia tworzenie czytelnego, łatwego do utrzymania i skalowalnego kodu.

Wzorce projektowe – szczegółowy opis

1. Factory Method (Metoda Wytwórcza)

Factory Method to wzorzec kreacyjny służący do tworzenia obiektów bez konieczności ujawniania szczegółów ich konstrukcji.

Kiedy stosować?

  • Gdy tworzenie obiektów jest skomplikowane.

  • Gdy chcesz zapewnić elastyczność tworzenia różnych typów obiektów.

Plusy:

  • Upraszcza proces tworzenia obiektów.

  • Zwiększa elastyczność aplikacji.

Minusy:

  • Może wprowadzać dodatkową złożoność w kodzie.

Przykład implementacji:

public interface ILogger { void Log(string message); } public class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine($"Console: {message}"); } } public class LoggerFactory { public static ILogger CreateLogger(string type) { return type switch { "console" => new ConsoleLogger(), _ => throw new ArgumentException("Unknown logger type") }; } }

2. Strategy (Strategia)

Strategia pozwala definiować zestawy algorytmów, które można łatwo zamieniać w trakcie działania aplikacji.

Kiedy stosować?

  • Gdy masz kilka wariantów algorytmu.

  • Gdy potrzebujesz dynamicznej zmiany algorytmów.

Plusy:

  • Ułatwia testowanie poszczególnych strategii.

  • Zwiększa elastyczność aplikacji.

Minusy:

  • Może prowadzić do zwiększenia liczby klas.

Przykład implementacji:

public interface IPricingStrategy { decimal CalculatePrice(decimal price); } public class RegularPricing : IPricingStrategy { public decimal CalculatePrice(decimal price) => price; } public class DiscountPricing : IPricingStrategy { public decimal CalculatePrice(decimal price) => price * 0.9m; } public class Product { private IPricingStrategy _pricingStrategy; public Product(IPricingStrategy strategy) { _pricingStrategy = strategy; } public decimal GetPrice(decimal price) { return _pricingStrategy.CalculatePrice(price); } }

3. Mediator

Mediator umożliwia komunikację między klasami bez bezpośrednich powiązań.

Kiedy stosować?

  • Gdy komunikacja między klasami staje się złożona.

  • W aplikacjach z dużą liczbą interakcji między komponentami.

Plusy:

  • Redukuje zależności między klasami.

  • Zwiększa czytelność kodu.

Minusy:

  • Mediator może stać się zbyt skomplikowany.

Przykład implementacji:

public interface IMediator { void Notify(object sender, string ev); } public class ConcreteMediator : IMediator { public void Notify(object sender, string ev) { Console.WriteLine($"Mediator received: {ev}"); } }

4. CQRS (Command Query Responsibility Segregation)

CQRS dzieli operacje odczytu od operacji zapisu.

Kiedy stosować?

  • W złożonych systemach, które wymagają wysokiej wydajności odczytu.

  • Gdy system ma wyraźnie różne wymagania dla odczytów i zapisów.

Plusy:

  • Poprawia wydajność.

  • Ułatwia skalowanie aplikacji.

Minusy:

  • Zwiększona złożoność architektury.

Przykład implementacji:

public interface ICommand { } public interface IQuery { } public class CreateUserCommand : ICommand { public string Name { get; set; } } public class GetUserQuery : IQuery { public int UserId { get; set; } }

5. Builder

Builder służy do budowania złożonych obiektów etapami.

Kiedy stosować?

  • Gdy obiekt ma wiele parametrów konfiguracyjnych.

Plusy:

  • Ułatwia tworzenie skomplikowanych obiektów.

  • Zwiększa czytelność kodu.

Minusy:

  • Może powodować nadmiar klas.

Przykład implementacji:

public class House { public string Walls { get; set; } public string Roof { get; set; } } public class HouseBuilder { private House _house = new House(); public HouseBuilder AddWalls(string walls) { _house.Walls = walls; return this; } public HouseBuilder AddRoof(string roof) { _house.Roof = roof; return this; } public House Build() => _house; }

Najczęstsze antywzorce – czego unikać?

Antywzorce projektowe

God Object

Opis: Klasa, która zawiera zbyt wiele odpowiedzialności.
Problem: Trudna do testowania i modyfikowania, narusza zasadę pojedynczej odpowiedzialności (SRP).

Spaghetti Code

Opis: Kod bez struktury, z wieloma nieczytelnymi zależnościami.
Problem: Utrudnia zrozumienie, refaktoryzację i testowanie.

Lava Flow

Opis: Stare fragmenty kodu, których nikt nie chce usunąć, bo nie wiadomo, czy są potrzebne.
Problem: Komplikuje aplikację i zwiększa ryzyko błędów.

Cargo Cult Programming

Opis: Stosowanie wzorców bez zrozumienia ich celu.
Problem: Nieefektywny, złożony lub błędny kod.

Poltergeist

Opis: Klasy o krótkim czasie życia, których jedyną rolą jest delegowanie zadań.
Problem: Zwiększają złożoność bez rzeczywistej wartości.

Golden Hammer

Opis: Nadużywanie jednego wzorca jako uniwersalnego rozwiązania.
Problem: Przeinżynierowanie i nadmiarowa architektura.

Hardcoding

Opis: Stałe wartości wpisane bezpośrednio w kod.
Problem: Brak elastyczności i trudności w utrzymaniu.

Wzorce, które bywają nadużywane

Singleton

Problem: Globalny stan, trudny do testowania i rozszerzania.
Kiedy szkodzi: Gdy używany jako kontener danych zamiast prawdziwego serwisu lub kontrolera zależności.

Abstract Factory

Problem: Zbyt złożony jak na typowe potrzeby.
Kiedy szkodzi: W prostych aplikacjach, gdzie wystarczyłby zwykły konstruktor lub metoda fabrykująca.

Service Locator

Problem: Ukrywa zależności między komponentami.
Kiedy szkodzi: Gdy zamiast wstrzykiwania zależności, serwisy są pobierane globalnie.

Observer

Problem: Może prowadzić do trudnych do śledzenia zależności.
Kiedy szkodzi: Gdy nie zarządza się cyklem życia obserwatorów lub przy dużej liczbie subskrybentów.

CQRS + Event Sourcing

Problem: Złożona architektura, wymaga dużo kodu pomocniczego.
Kiedy szkodzi: W prostych systemach CRUD, gdzie nie występuje potrzeba rozdzielania zapisu i odczytu.

Podsumowanie

Wzorce projektowe są kluczowym elementem w arsenale każdego programisty C# i .NET. Świadome ich stosowanie pomaga w budowaniu aplikacji łatwych w utrzymaniu, wydajnych i elastycznych. Jednak równie istotne jest rozpoznawanie antywzorców i unikanie nadużywania wzorców tam, gdzie są zbędne.

Powodzenia w świadomym projektowaniu kodu – niech wzorce będą Twoimi sprzymierzeńcami, a nie problemem!

Happy coding!

Warto zobaczyć

Idź do oryginalnego materiału