Zaawansowane techniki obiektowe

Rok akademicki: 2026/27
Zadanie Laboratoryjne do Części 2: Architektura Taktyczna Systemu Dowodzenia Flotą Orbitalną (Wzorce Strukturalne, Kompozycja Kolekcji, Copy-on-Write i Autofac)
Prowadzący: Tomasz Goluch
Wersja: 1.0
Wymagany standard: C# 12 / .NET 8+
Maksymalna ocena zadania: 100 punktów (100%)
Forma realizacji: Kontynuacja projektu OrbitalFleet z Części 1 (lub nowy moduł taktyczny tworzony od zera)


1. Kontekst Biznesowy i Architektoniczny Projektu

W Części 1 zaprojektowałeś rdzeń kreacyjny silnika OrbitalFleet.Engine, odpowiadający za asynchroniczną produkcję okrętów w stoczniach, budowę Okrętu Flagowego oraz wielowątkową replikację kwantową. Gdy flota znalazła się na orbicie operacyjnej, dowództwo napotkało szereg krytycznych wyzwań strukturalnych i pamięciowych:

  1. Integracja obcych i przestarzałych systemów radarowych: Przejęte stacje nasłuchowe generują surowe strumienie danych w niekompatybilnym formacie polarnym, które trzeba w locie tłumaczyć, deszyfrować, wzmacniać oraz chronić przed nieautoryzowanym odczytem.
  2. Eksplozja kombinatoryczna klas komunikacji i uzbrojenia: Próba połączenia \(N\) typów dyrektyw bojowych z \(M\) platformami transmisyjnymi za pomocą dziedziczenia doprowadziła do powstania \(N \times M\) trudnych w utrzymaniu podklas.
  3. Złożoność koordynacji uderzenia orbitalnego: Wyprowadzenie salwy wymaga ręcznej synchronizacji kilkunastu niskopoziomowych podsystemów (radarów, komputerów balistycznych, matryc energetycznych i transmiterów).
  4. Zarządzanie hierarchią formacji i topologią sieci przesyłowej: Rozkazy muszą być propagowane identycznie dla pojedynczego myśliwca, eskadry i całej armady, a łączenie węzłów sieci telemetrycznej nie może wymagać pisania \(N^2\) przeciążeń metod.
  5. Wyczerpanie pamięci RAM w pasie asteroid: Symulacja sektora zawierającego \(100\,000\) asteroid i dronów bojowych, z których każdy przechowuje ciężką siatkę geometryczną 3D i widmo mineralne, powoduje natychmiastowy błąd OutOfMemoryException.

Twoim zadaniem jest rozbudowa systemu o moduł taktyczny wykorzystujący wszystkie wzorce strukturalne GoF, strategię Copy-on-Write (CoW) oraz deklaratywne mechanizmy strukturalne kontenera Autofac (RegisterAdapter, RegisterDecorator, RegisterComposite).


2. Inicjalizacja Środowiska (Krok 0: Rozbudowa Solucji lub Nowy Projekt)

Zadanie możesz zrealizować jako bezpośrednie rozszerzenie solucji OrbitalFleet z Części 1 lub zainicjalizować czystą strukturę projektową poniższymi poleceniami:

# Jeśli kontynuujesz projekt z Części 1, możesz od razu przejść do implementacji w OrbitalFleet.Core.
# Jeśli tworzysz czystą solucję pod Część 2:
dotnet new sln -n OrbitalFleet

dotnet new classlib -n OrbitalFleet.Core -f net8.0
dotnet new xunit -n OrbitalFleet.Tests -f net8.0
dotnet new console -n OrbitalFleet.App -f net8.0

dotnet sln add OrbitalFleet.Core/OrbitalFleet.Core.csproj
dotnet sln add OrbitalFleet.Tests/OrbitalFleet.Tests.csproj
dotnet sln add OrbitalFleet.App/OrbitalFleet.App.csproj

dotnet add OrbitalFleet.Tests/OrbitalFleet.Tests.csproj reference OrbitalFleet.Core/OrbitalFleet.Core.csproj
dotnet add OrbitalFleet.App/OrbitalFleet.App.csproj reference OrbitalFleet.Core/OrbitalFleet.Core.csproj

# Instalacja kontenera Autofac oraz biblioteki asercji FluentAssertions
dotnet add OrbitalFleet.Core/OrbitalFleet.Core.csproj package Autofac
dotnet add OrbitalFleet.App/OrbitalFleet.App.csproj package Autofac
dotnet add OrbitalFleet.Tests/OrbitalFleet.Tests.csproj package Autofac
dotnet add OrbitalFleet.Tests/OrbitalFleet.Tests.csproj package FluentAssertions

3. Szczegółowe Wymagania i Podział Punktów (Łącznie: 100 pkt)

┌─────────────────────────────────────────────────────────────────────────────────┐
│           ARCHITEKTURA STRUKTURALNA SYSTEMU TAKTYCZNEGO ORBITALFLEET            │
├─────────────────────────────────────────────────────────────────────────────────┤
│  [M1: 22 pkt] Potok Sensorów         ──► Adapter + Dekorator (OCP) + Proxy      │
│  [M2: 18 pkt] Transmisja i Salwa     ──► Most (Bridge N+M) + Fasada Uderzenia   │
│  [M3: 20 pkt] Hierarchia i Topologia ──► Kompozyt + IEnumerable<T> ConnectTo    │
│  [M4: 22 pkt] Symulacja Pasa Asteroid──► Pyłek (Flyweight) + Prototyp + CoW     │
│  [M5: 18 pkt] Kontener IoC (Autofac) ──► RegisterAdapter / Decorator / Composite│
└─────────────────────────────────────────────────────────────────────────────────┘

Kamień Milowy 1: Potok Sensorów Dalekiego Zasięgu – Adapter, Dekorator i Pełnomocnik (22 pkt)

Flota przejęła starą stację nasłuchową operującą na klasie LegacyDeepSpaceRadar (symulującej niezarządzany lub przestarzały interfejs). Nowoczesny system kierowania ogniem wymaga jednak asynchronicznego kontraktu ITacticalSensor. Ponadto odczyty z sensora muszą być kaskadowo wzbogacane (deszyfrowane i wzmacniane) oraz chronione przed nieuprawnionym dostępem.

Wymagania szczegółowe:

  1. [6 pkt] Adapter Obiektowy (LegacyRadarAdapter):
    • Klasa LegacyDeepSpaceRadar udostępnia niekompatybilną metodę string ReadRawPolarHexDump(int sectorCode) zwracającą surowy ciąg szesnastkowy (np. "0xAF42:DIST=4500:AZIMUTH=180").
    • Zdefiniuj nowoczesny interfejs ITacticalSensor z metodą ValueTask<SensorReading> ScanSectorAsync(string sectorName, CancellationToken ct = default) oraz rekord SensorReading(string Sector, double DistanceKm, string Payload, bool IsDecrypted, double SignalStrength).
    • Zaimplementuj LegacyRadarAdapter : ITacticalSensor, który opakowuje LegacyDeepSpaceRadar, tłumaczy nazwę sektora na kod liczbowy i parsuje surowy zrzut na obiekt SensorReading.
  2. [8 pkt] Kaskadowy Dekorator w duchu OCP (SensorDecoratorBase):
    • Utwórz klasę bazową dekoratora implementującą ITacticalSensor oraz dwa konkretne dekoratory:
      1. QuantumDecryptionDecorator – wywołuje opakowany sensor, a następnie dekoduje Payload (np. dopisując znacznik [DECRYPTED-Q256]) i ustawia flagę IsDecrypted = true.
      2. SignalBoostDecorator – wzmacnia pole SignalStrength o mnożnik wzmocnienia (np. \(\times 2.5\)) oraz formatuje raport wyjściowy.
    • Dekoratory muszą umożliwiać dowolne kaskadowe składanie (tzw. „cebulka”: Boost(Decryption(Adapter(LegacyRadar)))).
  3. [8 pkt] Pełnomocnik Ochronny i Wirtualny (ClassifiedSensorProxy):
    • Zaimplementuj klasę ClassifiedSensorProxy : ITacticalSensor, która pełni podwójną rolę:
      • Pełnomocnik Ochronny (Protection Proxy): Sprawdza przekazany kontekst bezpieczeństwa dowódcy (SecurityClearanceLevel). Jeśli poziom jest niższy niż Secret, rzuca wyjątek UnauthorizedAccessException bez wywoływania sensora docelowego.
      • Pełnomocnik Wirtualny (Virtual Proxy): Przyjmuje w konstruktorze Lazy<ITacticalSensor> (lub Func<ITacticalSensor>) i odracza kosztowną alokację potoku sensora aż do momentu pierwszego pomyślnie autoryzowanego wywołania ScanSectorAsync.
💡 Wskazówki implementacyjne: Adapter, Dekorator i Pełnomocnik (Rozwiń)
public enum SecurityClearanceLevel { Cadet, TacticalOfficer, Commander, Secret }

public record SensorReading(
    string Sector,
    double DistanceKm,
    string Payload,
    bool IsDecrypted = false,
    double SignalStrength = 1.0);

public interface ITacticalSensor
{
    ValueTask<SensorReading> ScanSectorAsync(string sectorName, CancellationToken ct = default);
}

// Niekompatybilny komponent (Adaptee)
public class LegacyDeepSpaceRadar
{
    public string ReadRawPolarHexDump(int sectorCode)
        => $"HEX_SECTOR_{sectorCode}:DIST=1250:RAW_SIGNAL_ALPHA";
}

// 1. Adapter Obiektowy
public class LegacyRadarAdapter : ITacticalSensor
{
    private readonly LegacyDeepSpaceRadar _legacyRadar;

    public LegacyRadarAdapter(LegacyDeepSpaceRadar legacyRadar)
    {
        _legacyRadar = legacyRadar;
    }

    public ValueTask<SensorReading> ScanSectorAsync(string sectorName, CancellationToken ct = default)
    {
        int code = Math.Abs(sectorName.GetHashCode() % 1000);
        string raw = _legacyRadar.ReadRawPolarHexDump(code);
        var reading = new SensorReading(sectorName, 1250.0, raw, IsDecrypted: false, SignalStrength: 1.0);
        return ValueTask.FromResult(reading);
    }
}

// 2. Baza Dekoratora oraz Konkretny Dekorator
public abstract class SensorDecoratorBase : ITacticalSensor
{
    protected readonly ITacticalSensor _innerSensor;

    protected SensorDecoratorBase(ITacticalSensor innerSensor) => _innerSensor = innerSensor;

    public virtual ValueTask<SensorReading> ScanSectorAsync(string sectorName, CancellationToken ct = default)
        => _innerSensor.ScanSectorAsync(sectorName, ct);
}

public class QuantumDecryptionDecorator : SensorDecoratorBase
{
    public QuantumDecryptionDecorator(ITacticalSensor innerSensor) : base(innerSensor) { }

    public override async ValueTask<SensorReading> ScanSectorAsync(string sectorName, CancellationToken ct = default)
    {
        var reading = await base.ScanSectorAsync(sectorName, ct);
        return reading with
        {
            Payload = $"[DECRYPTED-Q256]({reading.Payload})",
            IsDecrypted = true
        };
    }
}

// 3. Pełnomocnik Ochronny i Wirtualny (Protection + Virtual Proxy)
public class ClassifiedSensorProxy : ITacticalSensor
{
    private readonly Lazy<ITacticalSensor> _lazyRealSensor;
    private readonly SecurityClearanceLevel _currentClearance;

    public bool IsRealSensorInitialized => _lazyRealSensor.IsValueCreated;

    public ClassifiedSensorProxy(Func<ITacticalSensor> sensorFactory, SecurityClearanceLevel clearance)
    {
        _lazyRealSensor = new Lazy<ITacticalSensor>(sensorFactory, LazyThreadSafetyMode.ExecutionAndPublication);
        _currentClearance = clearance;
    }

    public ValueTask<SensorReading> ScanSectorAsync(string sectorName, CancellationToken ct = default)
    {
        if (_currentClearance < SecurityClearanceLevel.Secret)
        {
            throw new UnauthorizedAccessException($"Odmowa dostępu! Ranga {_currentClearance} nie posiada autoryzacji Secret.");
        }

        // Inicjalizacja obiektu docelowego następuje dopiero tutaj!
        return _lazyRealSensor.Value.ScanSectorAsync(sectorName, ct);
    }
}

Kamień Milowy 2: Most Komunikacji Podprzestrzennej i Fasada Uderzenia Orbitalnego (18 pkt)

Rozkazy bojowe na orbicie ewoluują w dwóch niezależnych wymiarach: rodzaju dyrektywy (\(N\)) oraz fizycznego medium transmisji (\(M\)). Dodatkowo dowódca potrzebuje jednej, prostej metody Fasady, która skoordynuje skanowanie, obliczenia balistyczne i transmisję rozkazu.

Wymagania szczegółowe:

  1. [10 pkt] Wzorzec Most (TacticalDirective \(\rightarrow\) ISubspaceTransmitter):
    • Implementor (ISubspaceTransmitter): Interfejs definiujący niskopoziomową wysyłkę pakietu: Task<string> TransmitPacketAsync(string header, string payload, CancellationToken ct = default).
    • Konkretni Implementatorzy (\(M \ge 2\)): QuantumEntanglementTransmitter (natychmiastowa transmisja splątana) oraz HyperWaveRelayTransmitter (transmisja falowa dalekiego zasięgu).
    • Abstrakcja (TacticalDirective): Klasa abstrakcyjna przyjmująca ISubspaceTransmitter w konstruktorze (tworząc Most) i deklarująca metodę Task<string> BroadcastDirectiveAsync(string targetCoordinates, CancellationToken ct = default).
    • Udoskonalone Abstrakcje (\(N \ge 2\)): OrbitalBombardmentDirective oraz StealthReconDirective. Wykaż, że dowolną dyrektywę można połączyć z dowolnym transmiterem przy liczbie klas \(N + M\) zamiast \(N \times M\).
  2. [8 pkt] Wzorzec Fasady (OrbitalStrikeFacade):
    • Zbuduj podsystemy pomocnicze (wstrzykiwane przez interfejsy zgodnie z zasadą DIP): IBallisticComputer (obliczający wektor trajektorii) oraz IReactorPowerGrid (alokujący megawaty mocy do dział orbitalnych).
    • Zaimplementuj klasę OrbitalStrikeFacade, która udostępnia prostą metodę wysokiego poziomu: Task<StrikeExecutionReport> ExecuteCoordinatedStrikeAsync(string sectorName, CancellationToken ct = default).
    • Fasada pod spodem: (1) skanuje sektor przez ITacticalSensor, (2) weryfikuje zaszyfrowanie danych, (3) wylicza trajektorię w IBallisticComputer, (4) rezerwuje moc w IReactorPowerGrid, (5) rozgłasza rozkaz ogniowy przez Most (TacticalDirective) i zwraca zagregowany raport.
💡 Wskazówki implementacyjne: Most (Bridge) oraz Fasada (Rozwiń)
// 1. Implementor Mostu
public interface ISubspaceTransmitter
{
    Task<string> TransmitPacketAsync(string header, string body, CancellationToken ct = default);
}

public class QuantumEntanglementTransmitter : ISubspaceTransmitter
{
    public Task<string> TransmitPacketAsync(string header, string body, CancellationToken ct = default)
        => Task.FromResult($"[QUANTUM-LINK] {header} :: {body}");
}

public class HyperWaveRelayTransmitter : ISubspaceTransmitter
{
    public Task<string> TransmitPacketAsync(string header, string body, CancellationToken ct = default)
        => Task.FromResult($"[HYPER-WAVE] {header} :: {body}");
}

// 2. Abstrakcja Mostu
public abstract class TacticalDirective
{
    protected readonly ISubspaceTransmitter _transmitter;

    protected TacticalDirective(ISubspaceTransmitter transmitter)
    {
        _transmitter = transmitter;
    }

    public abstract Task<string> BroadcastDirectiveAsync(string targetCoordinates, CancellationToken ct = default);
}

public class OrbitalBombardmentDirective : TacticalDirective
{
    private readonly int _warheadYieldMegatons;

    public OrbitalBombardmentDirective(ISubspaceTransmitter transmitter, int warheadYieldMegatons)
        : base(transmitter)
    {
        _warheadYieldMegatons = warheadYieldMegatons;
    }

    public override Task<string> BroadcastDirectiveAsync(string targetCoordinates, CancellationToken ct = default)
        => _transmitter.TransmitPacketAsync("DIRECTIVE:BOMBARDMENT", $"TARGET={targetCoordinates};YIELD={_warheadYieldMegatons}MT", ct);
}

Kamień Milowy 3: Hierarchia Dowodzenia i Polimorficzna Sieć Taktyczna – Kompozyt, IEnumerable<T> i Metody Rozszerzające (20 pkt)

W tym kamieniu milowym zrealizujesz dwa komplementarne warianty wzorca Kompozyt: klasyczne drzewo dowodzenia GoF oraz zaawansowany idiom języka C# łączący IEnumerable<T> ze statycznymi metodami rozszerzającymi.

Wymagania szczegółowe:

  1. [8 pkt] Drzewo Dowodzenia Flotą (Klasyczny Kompozyt GoF):
    • Zdefiniuj interfejs IFleetUnitComponent z metodami string GetUnitDesignation(), int CalculateTotalFirepower() oraz void ExecuteManeuver(string formationName).
    • Zaimplementuj liść StarfighterLeaf (pojedynczy okręt o określonej sile ognia) oraz kompozyt SquadronComposite (mogący zawierać zarówno pojedyncze myśliwce, jak i podległe eskadry).
    • Wywołanie CalculateTotalFirepower() na nadrzędnym kompozycie Armady musi rekurencyjnie zsumować siłę ognia wszystkich podległych liści i pod-eskadr w całym drzewie.
  2. [12 pkt] Polimorfizm Kolekcji (class RelayNode : IEnumerable<RelayNode>) i Eliminacja Eksplozji Kombinatorycznej (OCP):
    • Utwórz klasę pojedynczego węzła przekaźnikowego TacticalRelayNode : IEnumerable<TacticalRelayNode>, posiadającą kolekcje połączeń ICollection<TacticalRelayNode> IncomingLinks oraz ICollection<TacticalRelayNode> OutgoingLinks.
    • Zaimplementuj GetEnumerator() z użyciem yield return this;, dzięki czemu pojedynczy węzeł zachowuje się jak jednoelementowa kolekcja. (Nie umieszczaj metody łączenia wewnątrz klasy TacticalRelayNode!).
    • Utwórz klasę grupy węzłów RelayCluster : Collection<TacticalRelayNode>.
    • Zaimplementuj jedną statyczną metodę rozszerzającą ConnectMeshTo(this IEnumerable<TacticalRelayNode> source, IEnumerable<TacticalRelayNode> target), która obsłuży wszystkie 4 kombinacje połączeń:
      1. TacticalRelayNode \(\rightarrow\) TacticalRelayNode
      2. TacticalRelayNode \(\rightarrow\) RelayCluster
      3. RelayCluster \(\rightarrow\) TacticalRelayNode
      4. RelayCluster \(\rightarrow\) RelayCluster
    • Metoda musi chronić przed połączeniem obiektu z samym sobą (ReferenceEquals(source, target) oraz ReferenceEquals(from, to)).
💡 Wskazówki implementacyjne: Idiom IEnumerable<T> i metoda rozszerzająca ConnectMeshTo (Rozwiń)
using System.Collections;
using System.Collections.ObjectModel;

public class TacticalRelayNode : IEnumerable<TacticalRelayNode>
{
    public string NodeId { get; }
    public ICollection<TacticalRelayNode> IncomingLinks { get; } = new HashSet<TacticalRelayNode>();
    public ICollection<TacticalRelayNode> OutgoingLinks { get; } = new HashSet<TacticalRelayNode>();

    public TacticalRelayNode(string nodeId) => NodeId = nodeId;

    public IEnumerator<TacticalRelayNode> GetEnumerator()
    {
        yield return this;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public class RelayCluster : Collection<TacticalRelayNode>
{
    public RelayCluster(string clusterPrefix, int count)
    {
        for (int i = 1; i <= count; i++)
        {
            Add(new TacticalRelayNode($"{clusterPrefix}-{i}"));
        }
    }
}

public static class TacticalMeshExtensions
{
    public static void ConnectMeshTo(
        this IEnumerable<TacticalRelayNode> source,
        IEnumerable<TacticalRelayNode> target)
    {
        if (ReferenceEquals(source, target)) return;

        foreach (var from in source)
        {
            foreach (var to in target)
            {
                if (!ReferenceEquals(from, to))
                {
                    from.OutgoingLinks.Add(to);
                    to.IncomingLinks.Add(from);
                }
            }
        }
    }
}

Kamień Milowy 4: Pas Asteroid i Rój Dronów – Pyłek (Flyweight), Prototyp i Mechanizm Copy-on-Write (CoW) (22 pkt)

Sektor operacyjny składa się ze \(100\,000\) asteroid. Każda asteroida należąca do określonego typu geologicznego posiada ciężkie dane wewnętrzne (np. tablicę byte[] MeshVertices o rozmiarze \(4\text{ KB}\), nazwę klasy mineralnej oraz współczynnik odbicia światła Albedo). Tworzenie osobnego bufora dla każdej asteroidy zajęłoby ponad \(400\text{ MB}\) pamięci RAM.

Wymagania szczegółowe:

  1. [8 pkt] Współdzielony Pyłek (AsteroidIntrinsicModel) i Bezpieczna Wątkowo Fabryka (AsteroidFlyweightFactory):
    • Utwórz klasę AsteroidIntrinsicModel reprezentującą stan wewnętrzny (Intrinsic State): string MineralType, double StructuralIntegrity, string SurfaceState, byte[] HeavyMeshData.
    • Zaimplementuj fabrykę AsteroidFlyweightFactory wykorzystującą ConcurrentDictionary<string, AsteroidIntrinsicModel>, która gwarantuje, że dla danego klucza mineralnego w pamięci powstaje dokładnie jedna współdzielona instancja pyłka (nawet przy równoległym wywołaniu z wielu wątków).
    • Udostępnij właściwość int CachedFlyweightsCount, zwracającą liczbę unikalnych modeli w pamięci podręcznej.
  2. [10 pkt] Kontekst Zewnętrzny i Strategia Copy-on-Write (AsteroidEntity):
    • Klasa AsteroidEntity przechowuje stan zewnętrzny (Extrinsic State): współrzędne \((X, Y, Z)\) oraz referencję do AsteroidIntrinsicModel.
    • Klasa AsteroidIntrinsicModel musi implementować wzorzec Prototyp (metodę AsteroidIntrinsicModel DeepClone(), kopiującą m.in. tablicę HeavyMeshData).
    • Mechanizm Copy-on-Write (CoW): Dopóki asteroidy są tylko odczytywane, wszystkie egzemplarze tego samego typu wskazują referencyjnie na ten sam obiekt pyłka (IsDetachedFromFlyweight == false).
    • Dodaj metodę mutującą void ApplyThermalLaserHit(double damage, string newSurfaceState). W momencie jej wywołania pojedyncza trafiona asteroida musi automatycznie sklonować swój model przez DeepClone(), ustawić IsDetachedFromFlyweight = true i zmodyfikować wytrzymałość oraz stan powierzchni wyłącznie we własnej kopii, pozostawiając pozostałe \(99\,999\) asteroid nienaruszonymi!
  3. [4 pkt] Pomiar alokacji pamięci (GC.GetTotalMemory):
    • Zaimplementuj metodę diagnostyczną weryfikującą zużycie pamięci przy alokacji \(100\,000\) obiektów AsteroidEntity korzystających z 3 typów pyłków w porównaniu do podejścia naiwnego (bez współdzielenia).
💡 Wskazówki implementacyjne: Pyłek + Prototyp + Copy-on-Write (Rozwiń)
using System.Collections.Concurrent;

public class AsteroidIntrinsicModel
{
    public string MineralType { get; set; }
    public double StructuralIntegrity { get; set; }
    public string SurfaceState { get; set; }
    public byte[] HeavyMeshData { get; set; }

    public AsteroidIntrinsicModel(string mineralType, double integrity, string surfaceState, int meshSizeBytes = 4096)
    {
        MineralType = mineralType;
        StructuralIntegrity = integrity;
        SurfaceState = surfaceState;
        HeavyMeshData = new byte[meshSizeBytes];
    }

    // Wzorzec Prototyp wykorzystywany przez Copy-on-Write
    public AsteroidIntrinsicModel DeepClone()
    {
        var copy = (AsteroidIntrinsicModel)this.MemberwiseClone();
        copy.HeavyMeshData = (byte[])this.HeavyMeshData.Clone();
        return copy;
    }
}

public class AsteroidFlyweightFactory
{
    private readonly ConcurrentDictionary<string, AsteroidIntrinsicModel> _cache = new();

    public int CachedFlyweightsCount => _cache.Count;

    public AsteroidIntrinsicModel GetOrAddModel(string mineralType, double defaultIntegrity = 100.0)
    {
        return _cache.GetOrAdd(mineralType, key => new AsteroidIntrinsicModel(key, defaultIntegrity, "Pristine"));
    }
}

public class AsteroidEntity
{
    public int X { get; }
    public int Y { get; }
    public int Z { get; }
    public AsteroidIntrinsicModel Model { get; private set; }
    public bool IsDetachedFromFlyweight { get; private set; }

    public AsteroidEntity(int x, int y, int z, AsteroidIntrinsicModel sharedFlyweight)
    {
        (X, Y, Z) = (x, y, z);
        Model = sharedFlyweight;
        IsDetachedFromFlyweight = false;
    }

    // Strategia Copy-on-Write (CoW): klonowanie następuje wyłącznie przy zapisie/uszkodzeniu!
    public void ApplyThermalLaserHit(double damage, string newSurfaceState)
    {
        if (!IsDetachedFromFlyweight)
        {
            Model = Model.DeepClone();
            IsDetachedFromFlyweight = true;
        }

        Model.StructuralIntegrity -= damage;
        Model.SurfaceState = newSurfaceState;
    }
}

Kamień Milowy 5: Deklaratywna Orkiestracja Strukturalna w Kontenerze Autofac (18 pkt)

Wszystkie opracowane wzorce strukturalne należy połączyć w module konfiguracyjnym kontenera Autofac, wykorzystując jego natywne metody dedykowane wzorcom strukturalnym.

Wymagania szczegółowe:

  1. [8 pkt] Deklaratywna rejestracja Adaptera i Kaskady Dekoratorów:
    • Zarejestruj LegacyDeepSpaceRadar w kontenerze.
    • Użyj metody builder.RegisterAdapter<LegacyDeepSpaceRadar, ITacticalSensor>(radar => new LegacyRadarAdapter(radar)) do automatycznej adaptacji interfejsu.
    • Użyj metody builder.RegisterDecorator<QuantumDecryptionDecorator, ITacticalSensor>() oraz builder.RegisterDecorator<SignalBoostDecorator, ITacticalSensor>(). Dowiedź, że rozwiązanie zależności container.Resolve<ITacticalSensor>() automatycznie zwraca kompletny, wielowarstwowo udekorowany adapter!
  2. [6 pkt] Deklaratywna rejestracja Kompozytu (RegisterComposite) i Pyłka (SingleInstance):
    • Zdefiniuj interfejs ujścia telemetrii ITelemetrySink (np. ConsoleTelemetrySink, BlackBoxTelemetrySink) oraz klasę kompozytu CompositeTelemetrySink : ITelemetrySink przyjmującą IEnumerable<ITelemetrySink>.
    • Zarejestruj kompozyt metodą builder.RegisterComposite<CompositeTelemetrySink, ITelemetrySink>(), unikając błędu cyklicznej zależności.
    • Zarejestruj AsteroidFlyweightFactory jako SingleInstance().
  3. [4 pkt] Czysty Composition Root w Program.cs:
    • Zaprezentuj w aplikacji konsolowej pełny scenariusz taktyczny (skanowanie przez udekorowany adapter, wykonanie uderzenia przez Fasadę i Most, kompozycję sieci przekaźników oraz symulację trafienia asteroidy w modelu Copy-on-Write) bez użycia operatora new dla serwisów poza konfiguracją kontenera.
💡 Wskazówki implementacyjne: Natywne rejestracje strukturalne w Autofac (Rozwiń)
using Autofac;
using OrbitalFleet.Core;

var builder = new ContainerBuilder();

// 1. Rejestracja obcego komponentu oraz deklaratywnego Adaptera:
builder.RegisterType<LegacyDeepSpaceRadar>();
builder.RegisterAdapter<LegacyDeepSpaceRadar, ITacticalSensor>(
    legacy => new LegacyRadarAdapter(legacy));

// 2. Deklaratywne kaskadowe owijanie w Dekoratory (OCP):
builder.RegisterDecorator<QuantumDecryptionDecorator, ITacticalSensor>();
builder.RegisterDecorator<SignalBoostDecorator, ITacticalSensor>();

// 3. Rejestracja Kompozytu telemetrii bez ryzyka StackOverflowException:
builder.RegisterType<ConsoleTelemetrySink>().As<ITelemetrySink>();
builder.RegisterType<BlackBoxTelemetrySink>().As<ITelemetrySink>();
builder.RegisterComposite<CompositeTelemetrySink, ITelemetrySink>();

// 4. Fabryka Pyłków jako Singleton oraz Fasada Uderzenia:
builder.RegisterType<AsteroidFlyweightFactory>().SingleInstance();
builder.RegisterType<QuantumEntanglementTransmitter>().As<ISubspaceTransmitter>();
builder.RegisterType<BallisticComputer>().As<IBallisticComputer>();
builder.RegisterType<ReactorPowerGrid>().As<IReactorPowerGrid>();
builder.RegisterType<OrbitalStrikeFacade>();

using var container = builder.Build();

4. Wymagania Dotyczące Pakietu Testów Automatycznych

W projekcie OrbitalFleet.Tests zaimplementuj zestaw testów jednostkowych i integracyjnych:

  1. SensorPipelineTests (Adapter, Dekorator, Proxy):
    • Test weryfikujący poprawne tłumaczenie danych z LegacyDeepSpaceRadar przez LegacyRadarAdapter oraz kaskadowe działanie obu dekoratorów (IsDecrypted == true oraz wzmocniony SignalStrength).
    • Test weryfikujący ClassifiedSensorProxy: brak alokacji obiektu docelowego przed wywołaniem (IsRealSensorInitialized == false), rzucenie UnauthorizedAccessException dla rangi Cadet (nadal IsRealSensorInitialized == false) oraz poprawne wywołanie dla rangi Secret.
  2. BridgeAndFacadeTests (Most i Fasada):
    • Test krzyżowy potwierdzający współpracę dowolnej dyrektywy (OrbitalBombardmentDirective, StealthReconDirective) z dowolnym transmiterem (QuantumEntanglementTransmitter, HyperWaveRelayTransmitter).
    • Test integracyjny OrbitalStrikeFacade.ExecuteCoordinatedStrikeAsync sprawdzający kompletną sekwencję wywołań podsystemów.
  3. CompositeAndMeshTests (Kompozyt i IEnumerable<T>):
    • Test hierarchii dowodzenia sprawdzający rekurencyjne sumowanie siły ognia CalculateTotalFirepower() w zagnieżdżonym drzewie eskadr.
    • Test sprawdzający wszystkie 4 warianty metody rozszerzającej ConnectMeshTo (Węzeł–Węzeł, Węzeł–Klaster, Klaster–Węzeł, Klaster–Klaster) oraz blokadę pętli zwrotnej (node.ConnectMeshTo(node)).
  4. FlyweightCopyOnWriteTests (Pyłek, Prototyp, CoW i Pamięć):
    • Test tworzący \(100\,000\) obiektów AsteroidEntity na bazie 3 typów mineralnych. Asercja potwierdzająca, że CachedFlyweightsCount == 3, a referencje ReferenceEquals(a1.Model, a2.Model) dla tego samego minerału są identyczne.
    • Test strategii Copy-on-Write: wywołanie ApplyThermalLaserHit na jednej asteroidzie musi zmienić jej flagę IsDetachedFromFlyweight na true, zmodyfikować jej StructuralIntegrity, a jednocześnie pozostawić referencję i wytrzymałość pozostałych \(99\,999\) asteroid całkowicie nietkniętą!
  5. AutofacStructuralContainerTests:
    • Test potwierdzający, że container.Resolve<ITacticalSensor>() zwraca obiekt owinięty kolejno w LegacyRadarAdapter \(\rightarrow\) QuantumDecryptionDecorator \(\rightarrow\) SignalBoostDecorator, a container.Resolve<ITelemetrySink>() zwraca CompositeTelemetrySink agregujący wszystkie zarejestrowane ujścia.

Uruchomienie testów:

dotnet test --logger "console;verbosity=detailed"

5. Tabela Podsumowująca Oceny (Suma: 100 pkt / 100%)

Nr Zakres modułu Weryfikowane umiejętności, wzorce i mechanizmy Punkty
1 Adapter, Dekorator i Proxy LegacyRadarAdapter, kaskada dekoratorów w duchu OCP, ClassifiedSensorProxy (Protection + Virtual Proxy). 22 pkt
2 Most (Bridge) i Fasada Separacja wymiarów \(N \times M \rightarrow N + M\) w komunikacji podprzestrzennej, orkiestracja w OrbitalStrikeFacade. 18 pkt
3 Kompozyt i Idiom IEnumerable<T> Drzewo siły ognia floty, węzeł z yield return this, jedna metoda rozszerzająca ConnectMeshTo dla 4 relacji. 20 pkt
4 Pyłek, Prototyp i Copy-on-Write ConcurrentDictionary w fabryce pyłków, \(100\,000\) asteroid w RAM, izolacja mutacji przez CoW i DeepClone(). 22 pkt
5 Strukturalny IoC / DI (Autofac) Deklaratywne użycie RegisterAdapter, RegisterDecorator, RegisterComposite oraz SingleInstance(). 18 pkt
SUMA 100 pkt (100%)