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!