Zaawansowane techniki obiektowe

Rok akademicki: 2026/27
Instrukcja laboratoryjna – Część 2: Wzorce Strukturalne, Kompozycja Architektoniczna, Metody Rozszerzające i Optymalizacja Pamięci (CoW)
Prowadzący: Tomasz Goluch
Wersja: 1.0


1. Wprowadzenie do Wzorców Strukturalnych

1.1. Cel i Istota Wzorców Strukturalnych

Celem drugiej części laboratorium jest poznanie i praktyczna implementacja wzorców strukturalnych (ang. Structural Patterns) w języku C# (standard C# 12 / .NET 8+) oraz środowisku mieszanym (C++/CLI). Wzorce strukturalne dotyczą struktury klas lub obiektów — koncentrują się na kompozycji klas i obiektów, czyli na tym, jak poszczególne byty są ze sobą powiązane i w jaki sposób tworzą większe, elastyczne struktury architektoniczne przy zachowaniu niskiego sprzężenia (Loose Coupling).

Podczas gdy wzorce kreacyjne (omówione w Części 1) odpowiadały na pytanie: „Jak powołać obiekt bez uzależniania kodu od konkretnej klasy?”, wzorce strukturalne odpowiadają na pytanie: „Jak połączyć niezależne klasy w spójny system bez wywoływania eksplozji podklas i bez łamania zasad SOLID?”.


1.2. Logiczna Kolejność Wzorców Strukturalnych

W niniejszej instrukcji wzorce strukturalne zostały uporządkowane w logiczny ciąg dydaktyczny i architektoniczny, dzielący je na dwie spójne grupy:

  1. Grupa I – Wzorce Opakowujące i Adaptujące Interfejsy (Wrappers):

    • Adapter (2.1): Punkt wyjścia (jedyny wzorzec w zestawieniu posiadający zarówno wariant klasowy, jak i obiektowy). Rozwiązuje problem niezgodności interfejsów w relacji \(1:1\), tłumacząc wywołania klienta na wywołania obcego komponentu (np. natywnej biblioteki C++).
    • Dekorator (2.2): Podobnie jak Adapter „owija” pojedynczy obiekt (\(1:1\)), ale zachowuje jego oryginalny interfejs. Zamiast tłumaczyć wywołania, pozwala kaskadowo i dynamicznie dokładać nowe zachowania zgodnie z zasadą Open/Closed (OCP).
    • Pełnomocnik / Proxy (2.3): Ma strukturę niemal bliźniaczą do Dekoratora (ten sam interfejs, relacja \(1:1\)), lecz zupełnie inną intencję biznesową: nie rozszerza funkcjonalności, ale kontroluje dostęp do obiektu docelowego (autoryzacja, leniwa inicjalizacja, audyt).
    • Fasada (2.4): Przenosi ideę opakowywania z pojedynczego obiektu na cały złożony podsystem (\(1:N\)). Dostarcza jeden prosty, wysokopoziomowy interfejs, ukrywając pod spodem skomplikowaną orkiestrację wielu klas (np. DbContext w Entity Framework Core).
  2. Grupa II – Wzorce Dekompozycji, Struktur Drzewiastych i Optymalizacji Pamięci:

    • Most / Bridge (3.1): Zamiast opakowywać istniejące klasy po fakcie (jak Adapter), projektuje architekturę od podstaw, oddzielając warstwę abstrakcji od warstwy implementacji za pomocą agregacji. Redukuje liczbę klas z iloczynu kartezjańskiego \(N \times M\) do sumy \(N + M\).
    • Kompozyt (3.2): Pozwala budować rekurencyjne struktury drzewiaste (część–całość), w których pojedyncze liście i całe gałęzie są traktowane przez klienta identycznie. W połączeniu z interfejsem IEnumerable<T> oraz metodami rozszerzającymi C# eliminuje eksplozję kombinatoryczną przeciążeń metod.
    • Pyłek / Flyweight i Copy-on-Write (3.3): Gdy struktury obiektowe (np. drzewa w Kompozycie lub masowe kolekcje) rozrastają się do setek tysięcy instancji, pojawia się problem wyczerpania pamięci RAM. Pyłek oddziela stan współdzielony (intrinsic) od kontekstowego (extrinsic), a w synergii z poznanym w Części 1 Prototypem tworzy mechanizm Copy-on-Write (CoW).
                        ┌──────────────────────────────────────────────┐
                        │        WZORCE STRUKTURALNE (GoF)             │
                        └──────────────────────┬───────────────────────┘
                                               │
                 ┌─────────────────────────────┴─────────────────────────────┐
                 ▼                                                           ▼
┌─────────────────────────────────────────┐                 ┌─────────────────────────────────────────┐
│    GRUPA I: WZORCE OPAKOWUJĄCE          │                 │  GRUPA II: DEKOMPOZYCJA I OPTYMALIZACJA │
│         (WRAPPERS & FACADES)            │                 │       (TREES, BRIDGES & MEMORY)         │
├─────────────────────────────────────────┤                 ├─────────────────────────────────────────┤
│ 2.1. Adapter     (Zmiana interfejsu 1:1)│                 │ 3.1. Most      (Separacja osi N*M -> N+M│
│ 2.2. Dekorator   (Rozszerzanie OCP 1:1) │                 │ 3.2. Kompozyt  (Drzewa + IEnumerable<T>)│
│ 2.3. Pełnomocnik (Kontrola dostępu 1:1) │                 │ 3.3. Pyłek     (Współdzielenie RAM + CoW│
│ 2.4. Fasada      (Uproszczenie sys. 1:N)│                 │                 wraz z Prototypem)      │
└─────────────────────────────────────────┘                 └─────────────────────────────────────────┘

2. Wzorce Opakowujące i Adaptujące Interfejsy (Wrappers)

2.1. Adapter (Wrapper – Zakres Klasowy oraz Obiektowy)

Wzorzec Adapter – zwany też Wrapperem – pozwala na współpracę dwóch niezależnych interfejsów, które normalnie nie mogłyby ze sobą działać z powodu niezgodności sygnatur lub typów danych. Jest to realizowane przez klasę pośredniczącą (Adapter), która „owija” jeden obiekt, udostępniając interfejs, którego oczekuje klient (ITarget), jednocześnie delegując pracę do oryginalnego, niezgodnego obiektu (Adaptee).

Wyróżniamy dwa warianty wzorca:


A. Przykład 1: Porównanie Adaptera Klasowego i Obiektowego w C#

// Interfejs oczekiwany przez kod kliencki
public interface ITarget
{
    string GetRequest();
}

// Istniejąca klasa o niekompatybilnym interfejsie (np. z zewnętrznej biblioteki)
public class Adaptee
{
    public string GetSpecificRequest() => "Specyficzne dane z Adaptee.";
}

// 1. ADAPTER KLASOWY (dziedziczy po Adaptee, implementuje ITarget)
public class ClassAdapter : Adaptee, ITarget
{
    public string GetRequest() => $"[ClassAdapter]: {base.GetSpecificRequest()}";
}

// 2. ADAPTER OBIEKTOWY (agreguje/komponuje instancję Adaptee)
public class ObjectAdapter : ITarget
{
    private readonly Adaptee _adaptee;

    public ObjectAdapter(Adaptee adaptee)
    {
        _adaptee = adaptee;
    }

    public string GetRequest() => $"[ObjectAdapter]: {_adaptee.GetSpecificRequest()}";
}

B. Przykład 2: Adapter Obiektowy C++/CLI (Kod zarządzany C# ↔︎ Kod niezarządzany C++)

Poniższy przykładowy kod (UnmanagedClass i ManagedClass) prezentuje praktyczny adapter obiektowy używany do łączenia kodu zarządzanego (C#) z natywnym kodem niezarządzanym (C++). Klasa ManagedClass (napisana w dialekcie C++/CLI) jest adapterem, który posiada surowy wskaźnik m_Impl do obiektu UnmanagedClass na stercie natywnej i udostępnia zarządzane metody oraz właściwości w C#, tłumacząc wywołania oraz typy danych (System::String^ ↔︎ LPCWSTR) na natywne wywołania C++.

Plik nagłówkowy UnmanagedClass.h oraz kod klasy UnmanagedClass.cpp (Natywny C++):
// UnmanagedClass.h
#pragma once
#include <Windows.h>

class UnmanagedClass {
public:
    LPCWSTR GetPropertyA();
    void MethodB(LPCWSTR);
};
// UnmanagedClass.cpp
#include "UnmanagedClass.h"
#include <iostream>

LPCWSTR UnmanagedClass::GetPropertyA() 
{ 
    return L"empty string"; 
}

void UnmanagedClass::MethodB(LPCWSTR theNativeString)
{ 
    std::wcout << theNativeString << std::endl;
}
Kod klasy adaptera ManagedClass.h (C++/CLI):
#pragma once
#include "UnmanagedClass.h"
#include <vcclr.h> // Wymagane dla PtrToStringChars

using namespace System;

public ref class ManagedClass {
public:
    // Alokacja obiektu natywnego na stercie C++ w konstruktorze
    ManagedClass() : m_Impl(new UnmanagedClass) {}

    // Zwolnienie obiektu natywnego w destruktorze (kompilowanym do IDisposable::Dispose)
    ~ManagedClass() {
        this->!ManagedClass();
    }

protected:
    // Zwolnienie obiektu natywnego w finalizatorze (gdy programista zapomni o Dispose/using)
    !ManagedClass() {
        if (m_Impl != nullptr) {
            delete m_Impl;
            m_Impl = nullptr;
        }
    }

public:
    property String^ PropertyA {
        String^ get() {
            return gcnew String(m_Impl->GetPropertyA());
        }
    }

    void MethodB(String^ theString) {
        // Przypięcie wskaźnika (pinning), aby Garbage Collector nie przesunął napisu w pamięci
        pin_ptr<const WCHAR> str = PtrToStringChars(theString);
        m_Impl->MethodB(str);
    }

private:
    UnmanagedClass* m_Impl;
};

💡 Uwaga dotycząca bezpieczeństwa pamięci w C++/CLI:
W języku C++/CLI zapis ~ManagedClass() generuje metodę IDisposable.Dispose(), natomiast !ManagedClass() generuje finalizator Finalize() uruchamiany przez Garbage Collector. Wywołanie this->!ManagedClass() wewnątrz ~ManagedClass() oraz wyzerowanie wskaźnika (m_Impl = nullptr) zabezpiecza aplikację przed błędem podwójnego zwolnienia pamięci (Double Free), a kompilator C++/CLI automatycznie dopisuje GC::SuppressFinalize(this).

Użycie w kodzie C#:
using ManagedClass mc = new();
mc.MethodB("some string");
var p = mc.PropertyA;
Console.WriteLine(p);

Przykładowe wyjście:

some string
empty string

Alternatywa w nowoczesnym .NET: Microsoft DI i Autofac

Kontener Autofac posiada wbudowane, dedykowane wsparcie dla wzorca Adapter poprzez metodę RegisterAdapter, która automatycznie opakowuje zarejestrowane usługi w docelowy interfejs:

// 1. Natywne wsparcie dla Adaptera w Autofac (RegisterAdapter):
builder.RegisterType<Adaptee>();

// Autofac automatycznie wykrywa potrzebę adaptacji Adaptee -> ITarget:
builder.RegisterAdapter<Adaptee, ITarget>(adaptee => new ObjectAdapter(adaptee));

// 2. Odpowiednik w Microsoft.Extensions.DependencyInjection:
services.AddTransient<Adaptee>();
services.AddTransient<ITarget>(sp => new ObjectAdapter(sp.GetRequiredService<Adaptee>()));
Dlaczego podejście oparte na kontenerze DI jest lepsze?
  1. Przezroczystość dla klienta: Klasa biznesowa żąda w konstruktorze wyłącznie ITarget i nie ma pojęcia, że pod spodem kontener w locie opakowuje niekompatybilny serwis Adaptee.
  2. Automatyczna adaptacja kolekcji w Autofac: Jeśli zarejestrujemy kilka różnych obiektów typu Command, jedno wywołanie builder.RegisterAdapter<Command, ToolbarButton>(cmd => new ToolbarButton(cmd)) automatycznie wygeneruje kolekcję IEnumerable<ToolbarButton> dla wszystkich komend w systemie.

2.2. Dekorator (Decorator – Zakres Obiektowy)

Wzorzec Dekorator pozwala na dynamiczne dodawanie nowych odpowiedzialności do obiektów w czasie wykonywania programu (runtime), bez modyfikacji ich kodu źródłowego. Stanowi to bezpośrednią realizację zasady Open/Closed Principle (OCP) z SOLID oraz elastyczną alternatywę dla dziedziczenia podklas. Dekoratory „opakowują” oryginalny komponent i dodają do niego własne zachowanie przed lub po delegacji pracy do opakowanego komponentu.

Wyróżniamy następujące elementy tego wzorca:

Interfejs IComponent wraz z implementującymi go klasami konkretnego komponentu i abstrakcyjnego dekoratora:

public interface IComponent
{
    string Operation();
}

public class ConcreteComponent : IComponent
{
    public string Operation()
    {
        return "ConcreteComponent";
    }
}

public abstract class Decorator : IComponent
{
    protected IComponent _component;

    public Decorator(IComponent component)
    {
        this._component = component;
    }

    // Dekorator deleguje całą podstawową pracę do opakowanego komponentu.
    public virtual string Operation()
    {
        if (this._component != null)
        {
            return this._component.Operation();
        }
        else
        {
            return string.Empty;
        }
    }
}

Kod konkretnych klas dekoratorów (ConcreteDecoratorA oraz ConcreteDecoratorB):

// Konkretne dekoratory wywołują opakowany obiekt i modyfikują jego wynik.
public class ConcreteDecoratorA : Decorator
{
    public ConcreteDecoratorA(IComponent comp) : base(comp)
    {
    }

    // Dekoratory mogą wywoływać implementację klasy bazowej (base.Operation()),
    // zamiast odwoływać się do pola _component bezpośrednio.
    public override string Operation()
    {
        return $"ConcreteDecoratorA({base.Operation()})";
    }
}

public class ConcreteDecoratorB : Decorator
{
    public ConcreteDecoratorB(IComponent comp) : base(comp)
    {
    }

    public override string Operation()
    {
        return $"ConcreteDecoratorB({base.Operation()})";
    }
}

Użycie w kodzie:

IComponent simple = new ConcreteComponent();
Console.WriteLine("Client: I get a simple component:");
Console.WriteLine("RESULT: " + simple.Operation());

// Zwróć uwagę, że dekoratory mogą opakowywać nie tylko proste komponenty,
// ale również inne dekoratory, tworząc kaskadowy łańcuch (tzw. "cebulkę"):
ConcreteDecoratorA decorator1 = new ConcreteDecoratorA(simple);
ConcreteDecoratorB decorator2 = new ConcreteDecoratorB(decorator1);

Console.WriteLine("Client: Now I've got a decorated component:");
Console.WriteLine("RESULT: " + decorator1.Operation());
Console.WriteLine("RESULT: " + decorator2.Operation());

Przykładowe wyjście:

Client: I get a simple component:
RESULT: ConcreteComponent
Client: Now I've got a decorated component:
RESULT: ConcreteDecoratorA(ConcreteComponent)
RESULT: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))
🧅 Dla dociekliwych: Wzorzec Dekoratora w klasie System.IO.Stream w .NET (Kliknij, aby rozwinąć)

Jednym z najlepszych przykładów wzorca Dekorator w bibliotece standardowej .NET (BCL) jest hierarchia strumieni System.IO.Stream.

Gdyby twórcy .NET chcieli za pomocą dziedziczenia obsłużyć wszystkie kombinacje strumieni plikowych, sieciowych, buforowanych, kompresowanych i szyfrowanych, powstałyby dziesiątki sztywnych klas (np. BufferedGZipEncryptedFileStream). Zamiast tego zastosowano wzorzec Dekoratora:

// 1. Konkretny komponent bazowy (zapis surowych bajtów do pliku)
using Stream fileStream = new FileStream("raport.bin", FileMode.Create);

// 2. Dekorator 1: Buforowanie zapisów w pamięci RAM (poprawa IOPS)
using Stream bufferedStream = new BufferedStream(fileStream, bufferSize: 4096);

// 3. Dekorator 2: Szyfrowanie strumienia w locie algorytmem AES
using Stream cryptoStream = new CryptoStream(bufferedStream, aes.CreateEncryptor(), CryptoStreamMode.Write);

// 4. Dekorator 3: Kompresja danych algorytmem GZip przed zaszyfrowaniem
using Stream gzipStream = new GZipStream(cryptoStream, CompressionMode.Compress);

// Klient operuje wyłącznie na abstrakcji Stream – dane przechodzą przez wszystkie warstwy!
using var writer = new StreamWriter(gzipStream);
await writer.WriteLineAsync("Tajne dane telemetryczne.");

Alternatywa w nowoczesnym .NET: Microsoft DI i Autofac

W aplikacjach korporacyjnych wzorzec Dekoratora jest podstawowym sposobem realizacji zagadnień przekrojowych (Cross-Cutting Concerns): logowania, pomiaru czasu wykonania, obsługi pamięci podręcznej (Caching) oraz ponawiania prób (Retry). Kontener Autofac wspiera ten wzorzec natywnie za pomocą metody RegisterDecorator:

// 1. Natywne kaskadowe dekorowanie w Autofac:
builder.RegisterType<ConcreteComponent>().As<IComponent>();

// Dekoratory owijają komponent dokładnie w kolejności rejestracji:
builder.RegisterDecorator<ConcreteDecoratorA, IComponent>();
builder.RegisterDecorator<ConcreteDecoratorB, IComponent>();

// 2. Wariant warunkowy w Autofac (dekoruj tylko w określonym kontekście):
builder.RegisterDecorator<ConcreteDecoratorB, IComponent>(
    condition: ctx => ctx.ImplementationType == typeof(ConcreteDecoratorA));

// 3. Odpowiednik w Microsoft DI (z pakietem Scrutor lub jawnie):
services.AddTransient<ConcreteComponent>();
services.AddTransient<IComponent>(sp =>
    new ConcreteDecoratorB(
        new ConcreteDecoratorA(
            sp.GetRequiredService<ConcreteComponent>())));
Dlaczego podejście oparte na kontenerze DI jest lepsze?
  1. Całkowita separacja od kodu biznesowego: Aby dodać warstwę buforowania (CachingDecorator) lub telemetrii (LoggingDecorator) do istniejącego serwisu, nie modyfikujemy ani jednej linijki w serwisie ani w kontrolerze – dodajemy tylko jedną linijkę RegisterDecorator w Composition Root.
  2. Łatwe włączanie/wyłączanie warstw z konfiguracji: Na środowisku deweloperskim możemy wstrzykiwać czysty serwis, a na produkcyjnym owijać go w dekorator metryk na podstawie flagi z appsettings.json.

2.3. Pełnomocnik (Proxy – Zakres Obiektowy)

Wzorzec Pełnomocnik (znany powszechnie pod angielską nazwą Proxy) dostarcza obiekt zastępczy (ang. surrogate) lub kontroler dostępu do innego obiektu rzeczywistego (RealSubject).

W zależności od przeznaczenia wyróżniamy cztery klasyczne rodzaje pełnomocników:

  1. Pełnomocnik Ochronny (Protection / Security Proxy): Kontroluje uprawnienia i autoryzuje klienta przed przekazaniem wywołania do obiektu docelowego.
  2. Pełnomocnik Wirtualny (Virtual Proxy): Opóźnia moment utworzenia kosztownego obiektu docelowego aż do chwili, gdy jego metoda zostanie faktycznie wywołana po raz pierwszy (Lazy Initialization).
  3. Pełnomocnik Zdalny (Remote Proxy): Reprezentuje lokalnie obiekt znajdujący się w innej przestrzeni adresowej lub na zdalnym serwerze (np. klient gRPC lub WCF).
  4. Pełnomocnik Logujący / Sprytne Odniesienie (Smart Reference / Logging Proxy): Rejestruje audyt wywołań, zlicza referencje lub zarządza blokadami synchronizacyjnymi.

Przykład implementacji (Pełnomocnik Ochronny i Wirtualny):

// 1. Subject (Wspólny interfejs)
public interface IDataAccess
{
    void GrantAccess(string user);
}

// 2. RealSubject (Właściwa, chroniona implementacja)
public class DatabaseAccess : IDataAccess
{
    public void GrantAccess(string user)
    {
        Console.WriteLine($"Dostęp do bazy danych przyznany użytkownikowi: {user}");
    }
}

// 3. Proxy (Kontroluje dostęp do RealSubject)
public class SecurityProxy : IDataAccess
{
    private DatabaseAccess _realSubject;
    private string _allowedUser = "Admin";

    public SecurityProxy(DatabaseAccess realSubject)
    {
        _realSubject = realSubject;
    }

    public void GrantAccess(string user)
    {
        if (CheckAccess(user))
        {
            _realSubject.GrantAccess(user); // Delegacja do RealSubject
        }
        else
        {
            Console.WriteLine($"Odmowa dostępu! Użytkownik {user} nie ma uprawnień.");
        }
    }

    private bool CheckAccess(string user)
    {
        return user == _allowedUser;
    }
}

Użycie w kodzie:

var realAccess = new DatabaseAccess();
IDataAccess proxy = new SecurityProxy(realAccess);

proxy.GrantAccess("Admin"); // Dostęp dozwolony
proxy.GrantAccess("Guest"); // Dostęp zabroniony

Przykładowe wyjście:

Dostęp do bazy danych przyznany użytkownikowi: Admin
Odmowa dostępu! Użytkownik Guest nie ma uprawnień.
🛡️ Dla dociekliwych: Dynamiczny Pełnomocnik (DynamicProxy) i Programowanie Aspektowe (AOP) w .NET (Kliknij, aby rozwinąć)

Ręczne pisanie klas Proxy dla dziesiątek interfejsów w dużym systemie prowadzi do powielania szablonowego kodu (boilerplate). Platforma .NET udostępnia wbudowaną klasę System.Reflection.DispatchProxy (a w ekosystemie zewnętrznym bibliotekę Castle.Core), które potrafią wygenerować klasę Pełnomocnika dynamicznie w pamięci w czasie działania programu (runtime):

using System.Reflection;

public class AuditLoggingProxy<T> : DispatchProxy where T : class
{
    private T _target = default!;

    protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
    {
        Console.WriteLine($"[AUDIT PROXY] Wywołanie metody: {targetMethod?.Name}");
        // Wywołanie metody na obiekcie rzeczywistym przez refleksję:
        return targetMethod?.Invoke(_target, args);
    }

    public static T Create(T target)
    {
        object proxy = Create<T, AuditLoggingProxy<T>>();
        ((AuditLoggingProxy<T>)proxy)._target = target;
        return (T)proxy;
    }
}

// Użycie dla dowolnego interfejsu bez pisania dedykowanej klasy Proxy:
IDataAccess auditedAccess = AuditLoggingProxy<IDataAccess>.Create(new DatabaseAccess());
auditedAccess.GrantAccess("Admin");

Na dokładnie tym samym mechanizmie (DynamicProxy) działają wszystkie popularne biblioteki do tworzenia atrap w testach jednostkowych (Moq, NSubstitute, FakeItEasy) oraz mechanizm Lazy Loading Proxies w Entity Framework Core!


Alternatywa w nowoczesnym .NET: Microsoft DI i Autofac

W ekosystemie Autofac (z rozszerzeniem Autofac.Extras.DynamicProxy) wzorzec Pełnomocnika realizuje się za pomocą przechwytywaczy (Interceptors), co pozwala na wdrożenie programowania zorientowanego aspektowo (AOP):

// 1. Rejestracja Pełnomocnika Ochronnego jako usługi zastępującej oryginał:
services.AddScoped<DatabaseAccess>();
services.AddScoped<IDataAccess, SecurityProxy>();

// 2. Dynamiczny Pełnomocnik (Interceptor) w Autofac + Castle.Core:
builder.RegisterType<SecurityInterceptor>(); // Implementuje IInterceptor

builder.RegisterType<DatabaseAccess>()
       .As<IDataAccess>()
       .EnableInterfaceInterceptors()
       .InterceptedBy(typeof(SecurityInterceptor));
Dlaczego podejście oparte na kontenerze DI jest lepsze?
  1. Uniwersalność (jeden Pełnomocnik dla stu interfejsów): Zamiast pisać OrderSecurityProxy, UserSecurityProxy i PaymentSecurityProxy, tworzymy jeden SecurityInterceptor, który kontener Autofac automatycznie podpina pod wybrane serwisy.
  2. Brak znajomości implementacji przez klienta: Klient nie może przypadkowo wywołać new DatabaseAccess() z pominięciem SecurityProxy, ponieważ klasa DatabaseAccess może być oznaczona jako internal, a na zewnątrz modułu udostępniany jest wyłącznie kontrakt IDataAccess.

2.4. Fasada (Facade – Zakres Obiektowy)

Wzorzec Fasada dostarcza uproszczony, zunifikowany interfejs do skomplikowanego podsystemu składającego się z wielu klas, bibliotek i operacji. Fasada ukrywa całą złożoność podsystemu przed klientem, ułatwiając jego użycie, a jednocześnie nie blokuje bezpośredniego dostępu do klas podsystemu, jeśli zaawansowany klient tego potrzebuje.

W poniższym przykładzie klasa Facade upraszcza współpracę między Subsystem1 i Subsystem2, wykonując złożoną sekwencję ich operacji za pomocą pojedynczej metody Operation().

Kod klas reprezentujących dwa podsystemy (Subsystem1 i Subsystem2):

// Podsystem może przyjmować żądania zarówno z fasady, jak i bezpośrednio od klienta.
// W każdym przypadku dla podsystemu Fasada jest po prostu kolejnym klientem
// i nie stanowi integralnej części samego podsystemu.
public class Subsystem1
{
    public string operation1()
    {
        return "Subsystem1: Ready!\n";
    }

    public string operationN()
    {
        return "Subsystem1: Go!\n";
    }
}

// Niektóre fasady mogą koordynować pracę wielu podsystemów jednocześnie.
public class Subsystem2
{
    public string operation1()
    {
        return "Subsystem2: Get ready!\n";
    }

    public string operationZ()
    {
        return "Subsystem2: Fire!\n";
    }
}

Kod klasy fasady (Facade):

// Klasa Fasady dostarcza prosty interfejs do złożonej logiki jednego lub
// kilku podsystemów. Deleguje żądania klienta do odpowiednich obiektów
// wewnątrz podsystemu oraz odpowiada za zarządzanie ich cyklem życia.
public class Facade
{
    protected Subsystem1 _subsystem1;
    protected Subsystem2 _subsystem2;

    // Konstruktor umożliwia wstrzyknięcie istniejących instancji podsystemów (zgodnie z DIP)
    // lub utworzenie domyślnych egzemplarzy, jeśli klient ich nie przekazał.
    public Facade(Subsystem1 subsystem1 = null, Subsystem2 subsystem2 = null)
    {
        this._subsystem1 = subsystem1 ?? new Subsystem1();
        this._subsystem2 = subsystem2 ?? new Subsystem2();
    }

    // Metody Fasady są wygodnymi skrótami do zaawansowanej funkcjonalności
    // podsystemów. Klienci korzystają jednak tylko z wycinka ich możliwości.
    public string Operation()
    {
        string result = "Facade initializes subsystems:\n";
        result += this._subsystem1.operation1();
        result += this._subsystem2.operation1();
        result += "Facade orders subsystems to perform the action:\n";
        result += this._subsystem1.operationN();
        result += this._subsystem2.operationZ();
        return result;
    }
}

Użycie w kodzie:

// Kod klienta może posiadać już utworzone niektóre obiekty podsystemu.
// W takim przypadku warto zainicjalizować Fasadę tymi obiektami,
// zamiast pozwalać Fasadzie na tworzenie nowych instancji.
Facade facade = new Facade();
Console.Write(facade.Operation());

Przykładowe wyjście:

Facade initializes subsystems:
Subsystem1: Ready!
Subsystem2: Get ready!
Facade orders subsystems to perform the action:
Subsystem1: Go!
Subsystem2: Fire!

Praktyczne zastosowanie w ekosystemie .NET: DbContext i SaveChanges() w Entity Framework Core

Doskonałym produkcyjnym przykładem wzorca Fasady w ekosystemie .NET jest klasa DbContext oraz metoda SaveChanges() z biblioteki Entity Framework Core:

Klient (Serwis Biznesowy)
  │
  └──► dbContext.SaveChanges()  ◄─── [ FASADA: DbContext ]
                                        │
        ┌───────────────────────────────┼───────────────────────────────┐
        ▼                               ▼                               ▼
┌──────────────────┐          ┌──────────────────┐            ┌──────────────────┐
│  ChangeTracker   │          │  Query / Command │            │   ADO.NET Core   │
│ (Wykrywanie zmian│          │  SQL Generator   │            │ • DbConnection   │
│  Added/Modified/ │          │ (Budowa drzew AST│            │ • DbTransaction  │
│  Deleted)        │          │  i parametrów @p)│            │ • DbCommand      │
└──────────────────┘          └──────────────────┘            └──────────────────┘
  1. Złożoność pod spodem: Aby zapisać dane w bazie relacyjnej bez EF Core, programista musiałby ręcznie: otworzyć połączenie (DbConnection), zarządzać transakcją (DbTransaction), wygenerować zapytania SQL (DbCommand), obsłużyć mapowanie parametrów chroniące przed SQL Injection oraz śledzić, które obiekty w pamięci uległy zmianie (Change Tracker).
  2. Fasada DbContext: Klasa ta udostępnia prosty zestaw właściwości i metod (DbSet<T>, Add, Remove), które pozwalają operować na obiektach domenowych w pamięci RAM.
  3. Rola SaveChanges(): Metoda ta działa jako główny punkt wejścia fasady, który „pod spodem” koordynuje cały skomplikowany proces: skanuje graf obiektów, otwiera transakcję, generuje zoptymalizowany wsadowy kod SQL dla wszystkich zmian jednocześnie, przesyła go do bazy i zatwierdza transakcję (Commit). Klient wywołuje tylko jedną komendę, nie martwiąc się o wewnętrzną mechanikę dostępu do danych. Jednocześnie, jeśli zaawansowany klient potrzebuje niskopoziomowego dostępu do połączenia lub transakcji, Fasada nie blokuje mu drogi (udostępniając np. dbContext.Database.BeginTransaction()).

Alternatywa w nowoczesnym .NET: Microsoft DI i Autofac

W architekturze opartej na wstrzykiwaniu zależności Fasada pomaga zwalczać antywzorzec Constructor Over-injection (sytuację, w której klasa kontrolera przyjmuje w konstruktorze 8–10 drobnych serwisów podsystemu). Zamiast tego grupujemy powiązane usługi w Fasadę (lub wykorzystujemy Autofac.Extras.AggregateService):

// 1. Klasyczna rejestracja Fasady z wstrzykiwaniem podsystemów (MS DI):
services.AddScoped<ISubsystem1, Subsystem1>();
services.AddScoped<ISubsystem2, Subsystem2>();
services.AddScoped<ISystemFacade, SystemFacade>();

// 2. Automatyczna Fasada Agregująca w Autofac (AggregateService):
// Wystarczy zdefiniować sam interfejs gromadzący podsystemy – Autofac sam wygeneruje klasę!
public interface IOrderSubsystemFacade
{
    IInventoryService Inventory { get; }
    IPaymentService Payment { get; }
    IShippingService Shipping { get; }
}

builder.RegisterAggregateService<IOrderSubsystemFacade>();
Dlaczego podejście oparte na kontenerze DI jest lepsze?
  1. Zgodność z DIP i pełna testowalność: W pierwowzorze konstruktor new Subsystem1() wewnątrz Facade na sztywno wiązał fasadę z konkretnymi klasami. Wstrzyknięcie interfejsów podsystemów przez kontener pozwala łatwo przetestować Fasadę za pomocą mocków.
  2. Redukcja sprzężenia w warstwie prezentacji: Kontroler ASP.NET Core wstrzykuje tylko jedną Fasadę zamiast dziesięciu serwisów pomocniczych.

3. Wzorce Dekompozycji, Struktur Drzewiastych i Optymalizacji Pamięci

3.1. Most (Bridge – Zakres Obiektowy)

Wzorzec Most (ang. Bridge) polega na rozdzieleniu abstrakcji od jej implementacji w taki sposób, aby oba te wymiary mogły być rozwijane i modyfikowane całkowicie niezależnie od siebie. Jest to realizowane poprzez agregację (kompozycję) zamiast dziedziczenia, co promuje luźne powiązania. Wzorzec ten bezpośrednio wspiera piątą zasadę SOLID – Dependency Inversion Principle (DIP) (moduły wyższego poziomu zależą od abstrakcji implementatora).

Problem eksplozji podklas (Class Explosion): \(N \times M \rightarrow N + M\)

Wzorzec Most rozwiązuje fundamentalny problem tzw. eksplozji podklas, który pojawia się, gdy próbujemy rozszerzać hierarchię klas w dwóch (lub więcej) niezależnych wymiarach jednocześnie poprzez dziedziczenie:

❌ PODEJŚCIE OPARTYM NA DZIEDZICZENIU (Iloczyn N * M klas):
                       [ Message ]
                      /           \
         [SystemMessage]         [UserMessage]
          /           \           /          \
 [SystemEmail]   [SystemSms]  [UserEmail]  [UserSms]  ... (N * M klas!)

✅ PODEJŚCIE Z WZORCEM MOST / BRIDGE (Suma N + M klas):
     ABSTRAKCJA (N)                         IMPLEMENTACJA (M)
   ┌─────────────────┐        Most        ┌────────────────────┐
   │     Message     │ ───(_sender)─────► │   IMessageSender   │
   └────────▲────────┘   (Kompozycja)     └─────────▲──────────┘
            │                                       │
   ┌────────┴────────┐                    ┌─────────┴──────────┐
   │ • SystemMessage │                    │ • EmailSender      │
   │ • UserMessage   │                    │ • SmsSender        │
   └─────────────────┘                    └────────────────────┘

🔗 Powiązanie ze wzorcem Budowniczy (z Części 1):
Kreacyjny wzorzec Budowniczy (Builder) jest strukturalnie powiązany ze wzorcem Most: klasa Director pełni w nim rolę Abstrakcji, natomiast interfejs IBuilder oraz konkretni budowniczowie pełnią rolę Implementacji wymienianych pod spodem.


Przykład implementacji (Komunikacja między platformami):

// 1. Implementor (Interfejs dla platform komunikacyjnych)
public interface IMessageSender
{
    void SendMessage(string subject, string body);
}

// 2. ConcreteImplementorA (Implementacja dla Email)
public class EmailSender : IMessageSender
{
    public void SendMessage(string subject, string body)
    {
        Console.WriteLine($"Wysłano wiadomość Email: '{subject}' - {body}");
    }
}

// 3. ConcreteImplementorB (Implementacja dla SMS)
public class SmsSender : IMessageSender
{
    public void SendMessage(string subject, string body)
    {
        Console.WriteLine($"Wysłano wiadomość SMS: '{subject}' - {body}");
    }
}

// 4. Abstraction (Klasa bazowa Wiadomości)
public abstract class Message
{
    protected IMessageSender _sender;

    // Bridge: Ustanawianie mostu do implementacji przez konstruktor
    public Message(IMessageSender sender)
    {
        _sender = sender;
    }

    public abstract void Send();
}

// 5. RefinedAbstraction (Wiadomość Systemowa)
public class SystemMessage : Message
{
    private string _subject;
    private string _body;

    public SystemMessage(IMessageSender sender, string subject, string body)
        : base(sender)
    {
        _subject = subject;
        _body = body;
    }

    public override void Send()
    {
        // Abstrakcja deleguje fizyczną wysyłkę przez Most do Implementatora:
        _sender.SendMessage($"SYSTEM: {_subject}", _body);
    }
}

Użycie w kodzie:

var email = new SystemMessage(new EmailSender(), "Awaria", "Serwer niedostępny.");
email.Send();

var sms = new SystemMessage(new SmsSender(), "Awaria", "Serwer niedostępny.");
sms.Send();

Przykładowe wyjście:

Wysłano wiadomość Email: 'SYSTEM: Awaria' - Serwer niedostępny.
Wysłano wiadomość SMS: 'SYSTEM: Awaria' - Serwer niedostępny.

Alternatywa w nowoczesnym .NET: Microsoft DI i Autofac

W nowoczesnych aplikacjach .NET Most jest naturalnym rezultatem wstrzykiwania zależności. Wybór konkretnego „Implementatora” po drugiej stronie mostu realizuje się poprzez usługi kluczowane (Keyed Services w .NET 8+) lub indeksowane (IIndex<TKey, TValue> w Autofac):

// 1. Rejestracja Implementatorów Mostu w Microsoft DI (.NET 8+):
services.AddKeyedTransient<IMessageSender, EmailSender>("Email");
services.AddKeyedTransient<IMessageSender, SmsSender>("Sms");

// 2. Rejestracja w Autofac z fabryką parametryzowaną:
builder.RegisterType<EmailSender>().Keyed<IMessageSender>(Channel.Email);
builder.RegisterType<SmsSender>().Keyed<IMessageSender>(Channel.Sms);
Dlaczego podejście oparte na kontenerze DI jest lepsze?
  1. Dynamiczne zestawianie Mostu w runtime: Klasa wyższego poziomu może otrzymać w konstruktorze IIndex<Channel, IMessageSender> i wybierać kanał transmisji dynamicznie na podstawie preferencji użytkownika lub stanu awaryjnego sieci.

3.2. Kompozyt (Composite – Zakres Obiektowy) i Idiomy Kolekcji C#

Wzorzec Kompozyt umożliwia traktowanie obiektów indywidualnych oraz złożonych grup obiektów (kompozycji) w jednolity, polimorficzny sposób. Struktura ta tworzy hierarchiczne struktury drzewiaste typu część–całość.

Elementy wzorca:


A. Przykład 1: Hierarchia militarna (Klasyczny Kompozyt GoF)

Wydanie rozkazu Move() dla dowódcy (CommanderComposite) powoduje, że dowódca sam wykonuje ruch, a następnie rekurencyjnie przekazuje rozkaz wszystkim swoim podległym jednostkom z kolekcji _children (zarówno pojedynczym żołnierzom, jak i podległym dowódcom niższych szczebli).

// Interfejs, po którym dziedziczy każdy komponent w drzewie
public interface IUnitComponent
{
    void Move();
}

// Liść (Leaf)
public class Soldier : IUnitComponent
{
    public void Move()
    {
        Console.WriteLine("I move after my commander");
    }
}

// Kompozyt (Composite)
public class CommanderComposite : IUnitComponent
{
    // Lista wszystkich dzieci przypisanych do dowódcy
    private ICollection<IUnitComponent> _children = new List<IUnitComponent>();

    // Rozkaz do ruszenia się delegowany w dół drzewa
    public void Move()
    {
        Console.WriteLine("I'm a commander and i go first");
        foreach (var item in _children)
        {
            item.Move();
        }
    }

    public void AddChild(IUnitComponent entity) => _children.Add(entity);
    public void RemoveChild(IUnitComponent entity) => _children.Remove(entity);
    public ICollection<IUnitComponent> GetChildren() => _children;
}
Użycie w kodzie:
// Powołanie do życia 1. dowódcy i dodanie podległych żołnierzy
CommanderComposite commander1 = new CommanderComposite();
Soldier soldier1 = new();
commander1.AddChild(soldier1);
commander1.AddChild(new Soldier());

// Powołanie do życia 2. dowódcy (nadrzędnego), któremu podlega 1. dowódca i dwaj żołnierze
CommanderComposite commander2 = new CommanderComposite();
commander2.AddChild(commander1);
commander2.AddChild(new Soldier());
commander2.AddChild(new Soldier());

Console.WriteLine("Wydanie rozkazu do poruszenia jednego żołnierza");
soldier1.Move();

Console.WriteLine("Wydanie rozkazu do poruszenia 1 dowódcy i wszystkich jego żołnierzy");
commander1.Move();

Console.WriteLine("Wydanie rozkazu do poruszenia 2 dowódcy i wszystkich jego żołnierzy");
commander2.Move();

Przykładowe wyjście:

Wydanie rozkazu do poruszenia jednego żołnierza
I move after my commander
Wydanie rozkazu do poruszenia 1 dowódcy i wszystkich jego żołnierzy
I'm a commander and i go first
I move after my commander
I move after my commander
Wydanie rozkazu do poruszenia 2 dowódcy i wszystkich jego żołnierzy
I'm a commander and i go first
I'm a commander and i go first
I move after my commander
I move after my commander
I move after my commander
I move after my commander

B. Przykład 2: Konstrukcja class ClassA : IEnumerable<ClassA> oraz Metody Rozszerzające a Zasada OCP

Stosowanie interfejsu IEnumerable<T> na poziomie pojedynczej klasy (która sama w sobie jest pojedynczym liściem, a nie typową kolekcją) to zaawansowany idiom języka C# pozwalający na ujednolicenie traktowania pojedynczych obiektów i ich grup.

Rozważmy model sieci neuronowej, w której występują pojedyncze neurony (Neuron) oraz całe warstwy neuronów (NeuronLayer). Chcemy umożliwić łączenie ze sobą elementów we wszystkich możliwych kombinacjach:

  1. Neuron → Neuron
  2. Neuron → NeuronLayer
  3. NeuronLayer → Neuron
  4. NeuronLayer → NeuronLayer

Bez wspólnego mianownika wymagałoby to napisania \(2 \times 2 = 4\) przeciążeń metody ConnectTo (a przy 3 typach jednostek już \(3 \times 3 = 9\) metod!).

Klasa reprezentująca pojedynczy neuron (Neuron) oraz warstwę (NeuronLayer):
using System.Collections;
using System.Collections.ObjectModel;

public class Neuron : IEnumerable<Neuron>
{
    public ICollection<Neuron> In { get; set; }
    public ICollection<Neuron> Out { get; set; }

    public Neuron()
    {
        In = new List<Neuron>();
        Out = new List<Neuron>();
    }

    // Jeśli pozostawimy tę metodę w klasie Neuron, będzie miała ona pierwszeństwo
    // dla przypadku Neuron -> Neuron. Jeśli ją usuniemy, wszystkie 4 przypadki
    // obsłuży jedna uniwersalna metoda rozszerzająca!
    public void ConnectTo(Neuron other)
    {
        Out.Add(other);
        other.In.Add(this);
    }

    // Pojedynczy obiekt (liść) "udaje" jednoelementową kolekcję zwracającą samą siebie:
    public IEnumerator<Neuron> GetEnumerator()
    {
        yield return this;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public class NeuronLayer : Collection<Neuron>
{
    public NeuronLayer(int count = 0)
    {
        while (count-- > 0)
            Add(new Neuron());
    }
}
Metoda rozszerzająca ConnectTo a zasada Open/Closed (OCP):

Zasada Open/Closed (Otwarte-Zamknięte) głosi, że elementy systemu powinny być otwarte na rozszerzanie, ale zamknięte na modyfikację. Metody rozszerzające (ang. extension methods) są w środowisku .NET jednym z najbardziej eleganckich narzędzi do realizacji tego postulatu:

public static class NeuronExtensions
{
    public static void ConnectTo(
        this IEnumerable<Neuron> self, IEnumerable<Neuron> other)
    {
        if (ReferenceEquals(self, other)) return;

        foreach (var from in self)
            foreach (var to in other)
            {
                from.Out.Add(to);
                to.In.Add(from);
            }
    }
}

🔍 Priorytet wiązania metod w kompilatorze C#:
Zwróć uwagę, że jeśli w klasie Neuron pozostawimy metodę instancyjną public void ConnectTo(Neuron other), kompilator C# nada jej wyższy priorytet niż metodzie rozszerzającej przy wywołaniu neuronA.ConnectTo(neuronB). Jeśli jednak usuniemy metodę ConnectTo z wnętrza klasy Neuron, wówczas jedna statyczna metoda rozszerzająca ConnectTo obsłuży wszystkie 4 kombinacje połączeń.

Korzyści architektoniczne z tego podejścia:
  1. Polimorfizm kolekcji: Pojedynczy obiekt (liść) zachowuje się jak jednoelementowa kolekcja (yield return this).
  2. Redukcja złożoności (Combinatorial Explosion): Zamiast \(N^2\) przeciążeń metod piszemy dokładnie jedną metodę operującą na IEnumerable<Neuron>.
  3. Przezroczystość dla klienta: Kod kliencki nie musi sprawdzać instrukcją if, czy pracuje z jednym elementem, czy z całym grafem obiektów.
Użycie w kodzie:
Neuron a = new();
Neuron b = new();
a.ConnectTo(b); // 1. Neuron -> Neuron

var layer = new NeuronLayer(4);
b.ConnectTo(layer); // 2. Neuron -> Layer

NeuronLayer layer2 = new(4);
Neuron c = new();
layer2.ConnectTo(c); // 3. Layer -> Neuron

layer.ConnectTo(layer2); // 4. Layer -> Layer

Alternatywa w nowoczesnym .NET: Microsoft DI i Autofac

Kontener Autofac posiada dedykowaną metodę RegisterComposite, która pozwala zarejestrować wzorzec Kompozytu tak, aby automatycznie zbierał wszystkie zarejestrowane implementacje interfejsu (liście) i wstrzykiwał je do obiektu kompozytu:

// 1. Natywna rejestracja Kompozytu w Autofac (RegisterComposite):
builder.RegisterType<FileLogger>().As<ILogger>();
builder.RegisterType<DatabaseLogger>().As<ILogger>();

// CompositeLogger przyjmuje w konstruktorze IEnumerable<ILogger> (wszystkie liście),
// ale sam nigdy nie tworzy cyklu rekurencyjnego sam ze sobą!
builder.RegisterComposite<CompositeLogger, ILogger>();
Dlaczego podejście oparte na kontenerze DI jest lepsze?
  1. Automatyczna agregacja bez pętli rekurencyjnej: Bez RegisterComposite próba zarejestrowania klasy CompositeLogger : ILogger, która przyjmuje w konstruktorze IEnumerable<ILogger>, zakończyłaby się błędem przepełnienia stosu (StackOverflowException / Circular Dependency), ponieważ kontener próbowałby wstrzyknąć CompositeLogger do samego siebie. RegisterComposite rozwiązuje ten problem natywnie.

3.3. Pyłek (Flyweight – Zakres Obiektowy) i Mechanizm Copy-on-Write (CoW)

A. Wzorzec Pyłek (Flyweight)

Wzorzec Pyłek jest używany do radykalnej minimalizacji zużycia pamięci RAM poprzez współdzielenie (ang. sharing) instancji obiektów reprezentujących ten sam stan. Polega on na rygorystycznym podziale danych obiektu na dwie kategorie:

  1. Stan wewnętrzny (Intrinsic State): Niezmienny (immutable), niezależny od kontekstu i współdzielony przez tysiące obiektów (np. model siatki 3D drzewa, tekstura kory, nazwa gatunku). Przechowywany wewnątrz klasy Pyłka (TreeType).
  2. Stan zewnętrzny (Extrinsic State): Zmienny, unikalny dla każdego egzemplarza (np. współrzędne \((X, Y)\) pojedynczego drzewa na mapie). Przechowywany na zewnątrz i przekazywany do metod Pyłka jako parametr wywołania.
100 000 obiektów drzew na mapie (Forest)                 Pula Pyłków w RAM (TreeTypeFactory)
┌──────────────────────────────────┐                     ┌─────────────────────────────────┐
│ Drzewo #1:    X=10, Y=5   ───────┼────────────────────►│ TreeType "Dąb"                  │
│ Drzewo #2:    X=15, Y=6   ───────┼────────────────────►│ • Tekstura (4 MB)               │
│ ...                              │                     │ • Kolor: Green                  │
│ Drzewo #9999: X=20, Y=8   ───────┼───────────┐         └─────────────────────────────────┘
└──────────────────────────────────┘           │         ┌─────────────────────────────────┐
  (Tylko 8 bajtów na współrzędne               └────────►│ TreeType "Sosna"                │
   + 8 bajtów na wskaźnik referencji!)                   │ • Tekstura (4 MB)               │
                                                         └─────────────────────────────────┘

Przykład implementacji:

// 1. & 2. Flyweight (Współdzielony obiekt z wewnętrznym stanem)
public class TreeType
{
    // Stan wewnętrzny - niezmienny, współdzielony
    private readonly string _name;
    private readonly ConsoleColor _color;
    private readonly string _texture;

    public TreeType(string name, ConsoleColor color, string texture)
    {
        _name = name;
        _color = color;
        _texture = texture;
    }

    // Metoda przyjmuje stan zewnętrzny (pozycja X, Y)
    public void Draw(int x, int y)
    {
        Console.ForegroundColor = _color;
        Console.WriteLine($"Rysowanie {_name} o teksturze {_texture} w pozycji ({x}, {y}).");
        Console.ResetColor();
    }
}

// 3. FlyweightFactory (Zarządzanie pulą obiektów współdzielonych)
public class TreeTypeFactory
{
    private readonly IDictionary<string, TreeType> _cache = new Dictionary<string, TreeType>();

    public TreeType GetTreeType(string name, ConsoleColor color, string texture)
    {
        string key = $"{name}_{color}_{texture}";
        if (!_cache.ContainsKey(key))
        {
            Console.WriteLine($"Tworzenie nowego obiektu TreeType: {name}");
            _cache.Add(key, new TreeType(name, color, texture));
        }
        return _cache[key];
    }
}

// 4. Client (Użycie i zarządzanie stanem zewnętrznym)
public class Forest
{
    private readonly ICollection<(TreeType Type, int X, int Y)> _trees = new List<(TreeType, int, int)>();
    private readonly TreeTypeFactory _factory = new();

    public void PlantTree(string name, ConsoleColor color, string texture, int x, int y)
    {
        // Używamy Pyłka z fabryki – stan wewnętrzny jest dzielony
        TreeType type = _factory.GetTreeType(name, color, texture);
        // Stan zewnętrzny jest przechowywany na liście (X, Y)
        _trees.Add((type, x, y));
    }

    public void DrawForest()
    {
        foreach (var tree in _trees)
        {
            tree.Type.Draw(tree.X, tree.Y);
        }
    }
}
Użycie w kodzie:
Forest forest = new Forest();

// 1. Dąb - nie ma go w pamięci, więc zostanie stworzony
forest.PlantTree("Dąb", ConsoleColor.Green, "Gruby", 10, 5);
// 2. Sosna - nie ma jej w pamięci, więc zostanie stworzona
forest.PlantTree("Sosna", ConsoleColor.DarkGreen, "Cienki", 20, 8);
// 3. Kolejny Dąb - zostanie pobrany z pamięci podręcznej (brak komunikatu o tworzeniu)
forest.PlantTree("Dąb", ConsoleColor.Green, "Gruby", 15, 6);
// 4. Kolejna Sosna - zostanie pobrana z pamięci podręcznej (brak komunikatu o tworzeniu)
forest.PlantTree("Sosna", ConsoleColor.DarkGreen, "Cienki", 25, 1);

// Rysujemy cały las
forest.DrawForest();

Przykładowe wyjście:

Tworzenie nowego obiektu TreeType: Dąb
Tworzenie nowego obiektu TreeType: Sosna
Rysowanie Dąb o teksturze Gruby w pozycji (10, 5).
Rysowanie Sosna o teksturze Cienki w pozycji (20, 8).
Rysowanie Dąb o teksturze Gruby w pozycji (15, 6).
Rysowanie Sosna o teksturze Cienki w pozycji (25, 1).

B. Mechanizm Copy-on-Write (CoW): Synergia Pyłka i Prototypu

Mechanizm Copy-on-Write (Kopiowanie przy zapisie) to zaawansowana strategia optymalizacji, która łączy oszczędność pamięci wzorca Pyłek z elastycznością modyfikacji danych wzorca Prototyp (poznanego w Części 1).

Polega on na tym, że dopóki wiele obiektów korzysta z tego samego zasobu (typu referencyjnego) wyłącznie w trybie odczytu, współdzielą one jedną instancję Pyłka w pamięci. Dopiero w momencie, gdy któryś z obiektów spróbuje zmodyfikować swój stan, system wykonuje głęboką kopię (Clone) i odłącza zmodyfikowany egzemplarz od współdzielonego pyłka.

Jak współpracują te trzy mechanizmy?
  1. Pyłek (Flyweight) jako baza: Wydziela stan wewnętrzny, który domyślnie jest współdzielony przez tysiące obiektów w pamięci podręcznej (cache).
  2. Prototyp (Prototype) jako mechanizm kopiowania: Dostarcza metodę Clone() pozwalającą powielić obiekt bazowy bez wiązania kodu z jego konkretną klasą.
  3. Strategia Copy-on-Write (CoW) jako koordynator: Zarządza logiką „kiedy kopiować” – odracza alokację nowej pamięci do ułamka przypadków, w których faktycznie dochodzi do zapisu.
public class TreeModel : ICloneable
{
    public string Species { get; set; }
    public ConsoleColor LeafColor { get; set; }

    public TreeModel(string species, ConsoleColor leafColor)
    {
        Species = species;
        LeafColor = leafColor;
    }

    // Wzorzec Prototyp: Kopia na potrzeby odłączenia stanu
    public object Clone() => new TreeModel(Species, LeafColor);
}

public class CoWTreeInstance
{
    private TreeModel _sharedOrPrivateModel;
    public bool IsDetachedFromFlyweight { get; private set; } = false;
    public int X { get; }
    public int Y { get; }

    public CoWTreeInstance(TreeModel sharedFlyweight, int x, int y)
    {
        _sharedOrPrivateModel = sharedFlyweight;
        X = x;
        Y = y;
    }

    // Strategia Copy-on-Write: Klonujemy dopiero w momencie mutacji!
    public void MutateColor(ConsoleColor newAutumnColor)
    {
        if (!IsDetachedFromFlyweight)
        {
            _sharedOrPrivateModel = (TreeModel)_sharedOrPrivateModel.Clone();
            IsDetachedFromFlyweight = true;
        }
        _sharedOrPrivateModel.LeafColor = newAutumnColor;
    }

    public TreeModel CurrentModel => _sharedOrPrivateModel;
}
⚡ Dla dociekliwych: Wzorzec Pyłek w sercu środowiska wykonawczego .NET CLR – String Interning (Kliknij, aby rozwinąć)

Środowisko wykonawcze .NET CLR wykorzystuje wzorzec Pyłek (Flyweight) do zarządzania literałami tekstowymi string za pomocą tzw. Puli Internowania Napisów (String Intern Pool):

string s1 = "ZaawansowaneTechnikiObiektowe";
string s2 = "ZaawansowaneTechnikiObiektowe";

// Zwróci TRUE! Oba wskaźniki pokazują na ten sam adres fizyczny w puli Pyłków CLR:
bool sameMemoryAddress = ReferenceEquals(s1, s2); 

// Dynamicznie utworzony napis możemy ręcznie dołączyć do puli Pyłków:
string dynamicString = new StringBuilder("Zaawansowane").Append("TechnikiObiektowe").ToString();
string internedString = string.Intern(dynamicString);

Console.WriteLine(ReferenceEquals(s1, internedString)); // TRUE!

Ponieważ typ string w C# jest całkowicie niemutowalny (immutable), wszystkie identyczne stałe tekstowe w programie mogą bezpiecznie współdzielić jedną instancję na stercie. Z kolei każda próba modyfikacji napisu (np. s1.Replace(...)) działa w oparciu o semantykę Copy-on-Write: pozostawia oryginalny pyłek nienaruszony i alokuje nowy ciąg znaków.


Alternatywa w nowoczesnym .NET: Microsoft DI i Autofac

W aplikacjach wielowątkowych klasyczny Dictionary<string, TreeType> z pierwowzoru rzuci wyjątek przy równoczesnym dostępie z wielu wątków. W nowoczesnym .NET fabrykę pyłków rejestruje się jako bezpieczny wątkowo Singleton oparty na ConcurrentDictionary<TKey, TValue> lub systemowym IMemoryCache:

// Bezpieczna wątkowo Fabryka Pyłków rejestrowana jako Singleton w DI:
public class ThreadSafeTreeTypeFactory
{
    private readonly ConcurrentDictionary<string, TreeType> _cache = new();

    public TreeType GetTreeType(string name, ConsoleColor color, string texture)
    {
        string key = $"{name}_{color}_{texture}";
        // Atomowe pobranie lub utworzenie pyłka w środowisku współbieżnym:
        return _cache.GetOrAdd(key, _ => new TreeType(name, color, texture));
    }
}

// Rejestracja w Microsoft DI / Autofac:
services.AddSingleton<ThreadSafeTreeTypeFactory>();
builder.RegisterType<ThreadSafeTreeTypeFactory>().SingleInstance();

4. Podsumowanie Wzorców Strukturalnych i Porównanie Architektoniczne

4.1. Jak nie pomylić wzorców opakowujących? (Adapter vs Dekorator vs Pełnomocnik vs Fasada)

Cztery wzorce strukturalne polegają na „owijaniu” (wrapping) innych obiektów, przez co na pierwszy rzut oka ich kod wydaje się podobny. Różnią się jednak fundamentalnie intencją architektoniczną oraz wpływem na interfejs:

1. ADAPTER:      [ Klient ] ──► [ ITarget (Adapter) ] ──► [ Adaptee (Inny interfejs!) ]
                 Cel: Konwersja niekompatybilnego interfejsu (1:1).

2. DEKORATOR:    [ Klient ] ──► [ IService (Dekorator) ] ──► [ IService (Oryginał) ]
                 Cel: Ten sam interfejs + dynamiczne dodanie nowych zachowań (OCP).

3. PEŁNOMOCNIK:  [ Klient ] ──► [ IService (Proxy) ] ───?──► [ IService (Oryginał) ]
                 Cel: Ten sam interfejs + kontrola dostępu / leniwa inicjalizacja.

4. FASADA:       [ Klient ] ──► [ Prosta Fasada ] ──┬──► [ Podsystem A ]
                                                    ├──► [ Podsystem B ]
                                                    └──► [ Podsystem C ]
                 Cel: Nowy, uproszczony interfejs ukrywający złożony podsystem (1:N).

4.2. Zestawienie Wzorców Strukturalnych GoF z Nowoczesnymi Kontenerami IoC/DI

Klasyczny Wzorzec GoF Zakres Odpowiednik w Kontenerze IoC/DI i .NET Korzyść architektoniczna w nowoczesnym .NET
Adapter Klasowy / Obiektowy builder.RegisterAdapter<TFrom, TTo>() (Autofac), AutoMapper / Mapster, C++/CLI Wrapper. Automatyczna translacja niekompatybilnych serwisów w locie bez modyfikacji konsumenta.
Dekorator Obiektowy builder.RegisterDecorator<TDec, TService>() (Autofac), Scrutor, DelegatingHandler. Kaskadowe dokładanie logowania, cache'u i telemetrii zgodnie z OCP w jednej linijce konfiguracji.
Pełnomocnik (Proxy) Obiektowy DispatchProxy, Castle.DynamicProxy, EnableInterfaceInterceptors() w Autofac. Centralna kontrola uprawnień (AOP) i leniwe ładowanie bez pisania ręcznych klas pośredniczących.
Fasada Obiektowy DbContext (EF Core), RegisterAggregateService<T>() w Autofac, serwisy aplikacyjne. Ukrycie skomplikowanych podsystemów i eliminacja Constructor Over-injection w kontrolerach.
Most (Bridge) Obiektowy Wstrzykiwanie abstrakcji implementatora przez konstruktor, Keyed Services, IIndex<K,V>. Redukcja liczby klas z \(N \times M\) do \(N + M\); niezależny rozwój abstrakcji i platformy.
Kompozyt Obiektowy builder.RegisterComposite<TComp, TService>() (Autofac), IEnumerable<T> + metody rozszerzające. Jednolite traktowanie liści i drzew oraz eliminacja eksplozji przeciążeń metod (\(N^2 \rightarrow 1\)).
Pyłek (Flyweight) Obiektowy Singleton z ConcurrentDictionary, IMemoryCache, string.Intern w CLR + strategia CoW. Wielokrotna redukcja zużycia pamięci RAM przy setkach tysięcy obiektów współdzielących stan.

5. Zadania Laboratoryjne

Zadania laboratoryjne do Części 2 zostaną udostępnione w osobnym dokumencie:
📄 Zadanie Laboratoryjne do Części 2: Architektura Wzorców Strukturalnych, Kompozycji i Optymalizacji Pamięci (zad_lab_2_wzorce_strukt.md)


6. Literatura i Źródła

  1. Refactoring Guru: Wzorce projektowe w C# – Wzorce Strukturalne – https://refactoring.guru/pl/design-patterns/structural-patterns
  2. Piotr Łukasiewicz: Wzorzec projektowy Kompozyt (Composite) – https://www.plukasiewicz.net/Artykuly/DesignPatternComposite
  3. Cezary Walenciuk: Composite – Kompozycja – Wzorce Projektowe C# – https://cezarywalenciuk.pl/blog/programing/composite-kompozycja-wzorce-projektowe-c
  4. Infotraining: Wzorce projektowe w C-Sharp – https://infotraining.bitbucket.io/cs-dp/
  5. Dokumentacja Autofac: Adapters, Decorators, Composites & Interceptors (AOP) – https://autofac.readthedocs.io/en/latest/advanced/adapters-decorators.html
  6. Dokumentacja Microsoft Learn: C++/CLI Migration and Interop (Wrapper / Adapter Pattern) – https://learn.microsoft.com/en-us/cpp/dotnet/dotnet-programming-with-cpp-cli