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

C# - wzorce projektowe

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<TResult> { }

public class CreateUserCommand : ICommand
{
    public string Name { get; set; }
}

public class GetUserQuery : IQuery<User>
{
    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ć

Udostępnij:

Powiązane posty