Zaawansowane techniki obiektowe

Rok akademicki: 2026/27
Zadanie Laboratoryjne do Części 1: Architektura Asynchronicznego Systemu Dowodzenia Flotą Orbitalną (Wzorce Kreacyjne, Wielozadaniowość i IoC/DI)
Prowadzący: Tomasz Goluch
Wymagany standard: C# 12 / .NET 8+
Maksymalna ocena zadania: 100 punktów (100%)
Forma realizacji: Projekt tworzony całkowicie od zera (Greenfield) w architekturze współbieżnej


1. Kontekst Biznesowy i Architektoniczny Projektu

Projektujesz jądro współbieżnego systemu symulacji kosmicznej w czasie rzeczywistym – OrbitalFleet.Engine.

System operuje w środowisku wielowątkowym, w którym równolegle działają pętla symulacji fizyki świata, asynchroniczne stocznie produkcyjne, potoki replikacji kwantowej oraz rozproszone zadania taktyczne w sektorach bojowych.

W takich warunkach klasyczne implementacje obiektowe zawodzą:

Twoim celem jest zaprojektowanie skalowalnego, asynchronicznego i odpornego na współbieżność systemu opartego na wzorcach kreacyjnych, paradygmacie async/await oraz kontenerze Inversion of Control (IoC/DI). W logice biznesowej obowiązuje bezwzględny zakaz stosowania operatora new do powoływania instancji serwisów, jednostek i fabryk.


2. Inicjalizacja Środowiska (Krok 0: Przygotowanie Solucji od Zera)

Otwórz terminal w pustym folderze roboczym i skonfiguruj strukturę solucji:

# 1. Utworzenie pustej solucji
dotnet new sln -n OrbitalFleet

# 2. Utworzenie projektu rdzenia domeny (Class Library)
dotnet new classlib -n OrbitalFleet.Core -f net8.0

# 3. Utworzenie projektu testów jednostkowych i współbieżnych (xUnit)
dotnet new xunit -n OrbitalFleet.Tests -f net8.0

# 4. Utworzenie projektu konsoli symulacyjnej (Console App)
dotnet new console -n OrbitalFleet.App -f net8.0

# 5. Dołączenie projektów do solucji
dotnet sln add OrbitalFleet.Core/OrbitalFleet.Core.csproj
dotnet sln add OrbitalFleet.Tests/OrbitalFleet.Tests.csproj
dotnet sln add OrbitalFleet.App/OrbitalFleet.App.csproj

# 6. Dodanie referencji
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

# 7. Instalacja kontenera Autofac oraz asercji do testów wielowątkowych
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 FluentAssertions

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

┌─────────────────────────────────────────────────────────────────────────────────┐
│              ARCHITEKTURA SYSTEMU ASYNCHRONICZNEJ FLOTY ORBITALNEJ              │
├─────────────────────────────────────────────────────────────────────────────────┤
│  [M1: 20 pkt] FleetEngineCoordinator   ──► Thread-Safe Singleton / Atomic Tick  │
│  [M2: 24 pkt] Asynchroniczne Stocznie  ──► Fabryka Abstrakcyjna + SemaphoreSlim │
│  [M3: 20 pkt] Konstruktor Okrętu       ──► Fluent Step Builder + Async Warmup   │
│  [M4: 16 pkt] Replikacja Roju          ──► Prototyp + Parallel.ForEachAsync     │
│  [M5: 20 pkt] Orkiestracja Zadań       ──► Autofac Scoped Tasks + Clean SOLID   │
└─────────────────────────────────────────────────────────────────────────────────┘

Kamień Milowy 1: Odporny Singleton i Asynchroniczny Koordynator Zegara (20 pkt)

W systemie symulacji musi istnieć dokładnie jeden centralny koordynator zegara i parametrów świata: FleetEngineCoordinator. Koordynator zarządza globalnym czasem gry, rejestruje asynchroniczne pętle robocze i udostępnia atomowy licznik klatek (Ticks).

Wymagania szczegółowe:

  1. [4 pkt] Struktura wzorca: Klasa sealed, ściśle prywatny konstruktor blokujący new, statyczna metoda dostępu GetInstance() lub właściwość Instance.
  2. [6 pkt] Bezpieczeństwo wątkowe: Pełna ochrona przed wyścigami w środowisku wielowątkowym zrealizowana za pomocą lock (Double-Checked Locking) lub System.Lazy<T>.
  3. [6 pkt] Ochrona przed refleksją i atomowość stanu:
    • Konstruktor weryfikuje, czy instancja już istnieje na stercie; próba ominięcia modyfikatora private przez Activator.CreateInstance musi skutkować rzuceniem wyjątku InvalidOperationException.
    • Klasa udostępnia metodę long AdvanceSimulationTick(), która inkrementuje globalny licznik w sposób atomowy przy użyciu Interlocked.Increment.
  4. [4 pkt] Test wyścigu pod obciążeniem: W projekcie testowym zaimplementuj test uruchamiający 10 000 równoległych wątków/zadań (Task.Run w pętli lub Parallel.For), które jednocześnie pobierają instancję i inkrementują licznik. Test musi dowieść, że powstał dokładnie jeden obiekt (Assert.Same) oraz że licznik osiągnął precyzyjną wartość 10 000 bez utraty danych.
💡 Wskazówki implementacyjne: Odporny Singleton i atomowość (Rozwiń)
public sealed class FleetEngineCoordinator
{
    private static FleetEngineCoordinator? _instance;
    private static readonly object _lock = new object();
    private long _simulationTick = 0;

    public long CurrentTick => Interlocked.Read(ref _simulationTick);
    public int MaxFleetCapacity { get; set; } = 500;

    private FleetEngineCoordinator()
    {
        if (_instance != null)
        {
            throw new InvalidOperationException("Naruszenie kontraktu Singletona! Wykryto nielegalne wywołanie konstruktora.");
        }
    }

    public static FleetEngineCoordinator GetInstance()
    {
        if (_instance == null)
        {
            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = new FleetEngineCoordinator();
                }
            }
        }
        return _instance;
    }

    public long AdvanceSimulationTick() => Interlocked.Increment(ref _simulationTick);
}

Test odporności na refleksję w xUnit:

[Fact]
public void Singleton_Should_ThrowException_When_Reflection_Bypasses_Private_Constructor()
{
    // Inicjalizacja pierwszej instancji
    var instance = FleetEngineCoordinator.GetInstance();

    // Próba wymuszenia utworzenia drugiego obiektu przez refleksję
    var ex = Assert.Throws<TargetInvocationException>(() =>
        Activator.CreateInstance(typeof(FleetEngineCoordinator), nonPublic: true));

    Assert.IsType<InvalidOperationException>(ex.InnerException);
}

Kamień Milowy 2: Asynchroniczne Stocznie Orbitalne – Fabryka Abstrakcyjna i SemaphoreSlim (24 pkt)

Produkcja okrętów kosmicznych jest procesem czasochłonnym i ograniczonym przepustowością doków montażowych stoczni. Klasyczne, synchroniczne fabryki zawieszają wątki wywołujące. Należy zaimplementować asynchroniczną fabrykę abstrakcyjną.

Gra posiada dwie frakcje gwiezdne:

Wymagania szczegółowe:

  1. [6 pkt] Abstrakcja asynchronicznej stoczni: Zdefiniuj interfejsy IEscortShip oraz ICapitalShip. Utwórz interfejs fabryki abstrakcyjnej IOrbitalShipyardFactory z asynchronicznymi metodami wytwórczymi:
    ValueTask<IEscortShip> BuildEscortAsync(string callsign, CancellationToken ct = default);
    ValueTask<ICapitalShip> BuildCapitalShipAsync(string callsign, CancellationToken ct = default);
  2. [8 pkt] Konkretne stocznie i kontrola przepustowości doków:
    • Zaimplementuj stocznie SolarShipyardFactory oraz VoidShipyardFactory.
    • Każda stocznia posiada ograniczoną liczbę fizycznych doków montażowych (np. maksymalnie 2 okręty budowane współbieżnie). Użyj SemaphoreSlim(2, 2) do asynchronicznego dławienia równoległych zamówień (await _dockSemaphore.WaitAsync(ct)).
    • Zasymuluj proces montażu poprzez await Task.Delay(ms, ct).
  3. [6 pkt] Asynchroniczna koordynacja ognia i kompatybilność frakcyjna:
    • Klasa ICapitalShip musi posiadać asynchroniczną metodę taktyczną: Task<string> CoordinateEscortAsync(IEscortShip escort, CancellationToken ct = default);
    • Jeśli eskortowiec pochodzi z tej samej frakcji, metoda po krótkiej synchronizacji systemów zwraca raport o 100% synergii bojowej. Jeśli wykryty zostanie obcy format danych (inna frakcja), zgłaszany jest błąd taktyczny lub rzucany dedykowany wyjątek domeny IncompatibleDoctrineException.
  4. [4 pkt] Metoda Wytwórcza w Bazowym Dowódcy Eskadry:
    • Klasa bazowa SquadronCommander posiada szablon metody Task DeployPatrolAsync(), który wywołuje wirtualną/abstrakcyjną metodę wytwórczą CreateEscortShip() i asynchronicznie wyprowadza jednostki na orbitę.
💡 Wskazówki implementacyjne: Asynchroniczna fabryka z SemaphoreSlim (Rozwiń)
public class SolarShipyardFactory : IOrbitalShipyardFactory
{
    private readonly SemaphoreSlim _dockSlots = new SemaphoreSlim(2, 2);

    public async ValueTask<IEscortShip> BuildEscortAsync(string callsign, CancellationToken ct = default)
    {
        await _dockSlots.WaitAsync(ct);
        try
        {
            // Symulacja czasu montażu na orbicie
            await Task.Delay(50, ct); 
            return new SolarInterceptor(callsign);
        }
        finally
        {
            _dockSlots.Release();
        }
    }

    public async ValueTask<ICapitalShip> BuildCapitalShipAsync(string callsign, CancellationToken ct = default)
    {
        await _dockSlots.WaitAsync(ct);
        try
        {
            await Task.Delay(150, ct);
            return new SolarDreadnought(callsign);
        }
        finally
        {
            _dockSlots.Release();
        }
    }
}

Kamień Milowy 3: Hermetyczny Fluent Step Builder z Asynchroniczną Kalibracją (20 pkt)

Okręt Flagowy (Flagship) to najpotężniejsza jednostka we flocie. Jej konstrukcja wymaga ścisłej sekwencji montażu podzespołów, a proces kończy się asynchroniczną procedurą rozruchu i kalibracji reaktora antymaterii.

Wymagania szczegółowe:

  1. [6 pkt] Hermetyzacja i niemutowalność: Klasa Flagship posiada właściwości dostępne tylko do odczytu z zewnątrz (init lub internal set). Konstruktor klasy Flagship jest niepubliczny. Klient nie może utworzyć statku przez new Flagship().
  2. [8 pkt] Bezpieczny łańcuch typów (Step Builder): Kolejność konfiguracji jest wymuszana na poziomie kompilacji: SetFlagshipName → SelectHullClass → InstallEngineCore → ConfigureArmament → AddSubsystems (opcjonalne) → BuildAndIgniteAsync(). Pominięcie kroku lub wywołanie metod w złej kolejności nie może się skompilować!
  3. [6 pkt] Asynchroniczna faza rozruchu i walidacja: Metoda kończąca budowę: Task<Flagship> BuildAndIgniteAsync(CancellationToken ct = default) weryfikuje integralność danych (brak pustych nazw) oraz asynchronicznie symuluje rozruch reaktora (Task.Delay), zgłaszając błąd, jeśli reaktor nie osiągnie masy krytycznej.
💡 Wskazówki implementacyjne: Asynchroniczny Fluent Step Builder (Rozwiń)
public interface INameStep { IHullStep SetName(string name); }
public interface IHullStep { IEngineStep SelectHull(string hullType); }
public interface IEngineStep { IWeaponStep InstallEngine(string engineModel); }
public interface IWeaponStep { IOptionalStep ConfigureArmament(string primaryWeapon); }

public interface IOptionalStep
{
    IOptionalStep AddAuxiliarySubsystem(string subsystemName);
    Task<Flagship> BuildAndIgniteAsync(CancellationToken ct = default);
}

public class FlagshipBuilder : INameStep, IHullStep, IEngineStep, IWeaponStep, IOptionalStep
{
    private Flagship _flagship = new Flagship();

    private FlagshipBuilder() { }
    public static INameStep Start() => new FlagshipBuilder();

    public IHullStep SetName(string name)
    {
        if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Nazwa okrętu nie może być pusta.");
        _flagship.Name = name;
        return this;
    }

    public IEngineStep SelectHull(string hullType) { _flagship.HullType = hullType; return this; }
    public IWeaponStep InstallEngine(string engineModel) { _flagship.EngineCore = engineModel; return this; }
    public IOptionalStep ConfigureArmament(string weapon) { _flagship.PrimaryWeapon = weapon; return this; }

    public IOptionalStep AddAuxiliarySubsystem(string subsystem)
    {
        _flagship.Subsystems.Add(subsystem);
        return this;
    }

    public async Task<Flagship> BuildAndIgniteAsync(CancellationToken ct = default)
    {
        // Asynchroniczna sekwencja inicjalizacji podzespołów
        await Task.Delay(20, ct); 
        _flagship.IsCoreIgnited = true;

        var constructed = _flagship;
        _flagship = new Flagship(); // Reset stanu budowniczego
        return constructed;
    }
}

Kamień Milowy 4: Wielowątkowa Replikacja Kwantowa – Prototyp pod Obciążeniem (16 pkt)

W krytycznych momentach bitwy Okręt Flagowy lub jednostki eskorty mogą zostać poddane replikacji kwantowej. Wiele wątków taktycznych próbuje jednocześnie powielać jednostki za pomocą Parallel.ForEachAsync.

Wymagania szczegółowe:

  1. [4 pkt] Kontrakt prototypu: Zdefiniuj interfejs IQuantumCloneable<T> z metodami T ShallowClone() oraz T DeepClone().
  2. [4 pkt] Kopia płytka: Implementacja w oparciu o MemberwiseClone().
  3. [8 pkt] Bezpieczna kopia głęboka i test równoległości:
    • Klasa Flagship posiada zagnieżdżony obiekt CommanderProfile (imię, ranga, neuro-wskaźniki) oraz kolekcję podsystemów List<string> Subsystems.
    • Metoda DeepClone() musi całkowicie odizolować pamięć sterty: zduplikować obiekt dowódcy oraz utworzyć nową listę podsystemów.
    • Test wielowątkowej replikacji: Za pomocą Parallel.ForEachAsync sklonuj pojedynczy okręt bazowy na 1 000 niezależnych jednostek. Każdy wątek musi zmodyfikować imię dowódcy i dodać unikalny podsystem w swoim klonie. Test musi udowodnić, że stan pierwotnego okrętu pozostał nienaruszony, a żaden wątek nie rzucił błędu współbieżnej modyfikacji.
💡 Wskazówki implementacyjne: Odporny Prototyp i Kopia Głęboka (Rozwiń)
public class CommanderProfile
{
    public string Name { get; set; } = string.Empty;
    public string Rank { get; set; } = string.Empty;

    public CommanderProfile(string name, string rank) => (Name, Rank) = (name, rank);
    public CommanderProfile Clone() => new CommanderProfile(Name, Rank);
}

public class Flagship : IQuantumCloneable<Flagship>
{
    public string Name { get; internal set; } = string.Empty;
    public string HullType { get; internal set; } = string.Empty;
    public string EngineCore { get; internal set; } = string.Empty;
    public string PrimaryWeapon { get; internal set; } = string.Empty;
    public bool IsCoreIgnited { get; internal set; }
    public CommanderProfile Commander { get; set; } = new("Nieznany", "Kapitan");
    public List<string> Subsystems { get; set; } = new();

    internal Flagship() { }

    public Flagship ShallowClone() => (Flagship)this.MemberwiseClone();

    public Flagship DeepClone()
    {
        var copy = (Flagship)this.MemberwiseClone();
        copy.Commander = this.Commander.Clone();
        copy.Subsystems = new List<string>(this.Subsystems);
        return copy;
    }
}

Kamień Milowy 5: Orkiestracja w Zasięgach IoC/DI (Autofac) i Asynchroniczny Punkt Wejścia (20 pkt)

Wszystkie elementy systemu łączone są w całość w punkcie kompozycji aplikacji (Composition Root) za pomocą kontenera Autofac.

Wymagania szczegółowe:

  1. [6 pkt] Rejestracja w kontenerze IoC:
    • Zarejestruj fabryki stoczniowe z kluczem/nazwą (Named<IOrbitalShipyardFactory>("Solar"), Named<IOrbitalShipyardFactory>("Void")).
    • Zarejestruj Singleton FleetEngineCoordinator jako SingleInstance().
    • Zarejestruj serwis operacyjny ISectorOperationService.
  2. [6 pkt] Izolacja cyklu życia w zadaniach asynchronicznych (ILifetimeScope):
    • Zdefiniuj serwis ISectorContext (generujący unikalny Guid SectorId oraz śledzący lokalne zużycie zasobów w danej bitwie).
    • Zarejestruj go z czasem życia InstancePerLifetimeScope().
    • Wykaż w symulacji uruchomienie dwóch równoległych operacji sektorowych (Task.WhenAll), z których każda operuje w osobnym using (var scope = container.BeginLifetimeScope()). Dowiedź, że instancje ISectorContext w osobnych zadaniach posiadają unikalne identyfikatory, a usługi wewnątrz tego samego zadania współdzielą ten sam kontekst.
  3. [8 pkt] Czystość architektoniczna w punkcie wejścia (Program.cs):
    • W kodzie biznesowym i orkiestrującym nie może pojawić się ani jedno słowo kluczowe new dla serwisów, fabryk, jednostek czy koordynatora.
    • Aplikacja asynchronicznie powołuje flotę, zleca budowę okrętu flagowego builderem, wykonuje równoległą replikację prototypu i symuluje koordynację bojową bez blokowania głównego wątku (Task.Wait i .Result są surowo zabronione – używaj wyłącznie await).
💡 Wskazówki implementacyjne: Równoległe zadania w dedykowanych Lifetime Scopes (Rozwiń)
using Autofac;
using OrbitalFleet.Core;

var builder = new ContainerBuilder();

// 1. Rejestracja stoczni z identyfikatorem frakcji
builder.RegisterType<SolarShipyardFactory>().Named<IOrbitalShipyardFactory>("Solar");
builder.RegisterType<VoidShipyardFactory>().Named<IOrbitalShipyardFactory>("Void");

// 2. Rejestracja Singletonu silnika
builder.Register(c => FleetEngineCoordinator.GetInstance())
       .As<FleetEngineCoordinator>()
       .SingleInstance();

// 3. Kontekst sektora powiązany z zasięgiem operacji
builder.RegisterType<SectorContext>().As<ISectorContext>().InstancePerLifetimeScope();
builder.RegisterType<SectorOperationService>().As<ISectorOperationService>();

using var container = builder.Build();

// Równoległe uruchomienie dwóch operacji sektorowych w odrębnych wątkach i zasięgach DI
var mission1 = Task.Run(async () =>
{
    using var scope = container.BeginLifetimeScope();
    var service = scope.Resolve<ISectorOperationService>();
    await service.ExecuteSectorPatrolAsync("Solar", "Sektor Alfa");
});

var mission2 = Task.Run(async () =>
{
    using var scope = container.BeginLifetimeScope();
    var service = scope.Resolve<ISectorOperationService>();
    await service.ExecuteSectorPatrolAsync("Void", "Sektor Gamma");
});

await Task.WhenAll(mission1, mission2);

4. Wymagania Dotyczące Pakietu Testów Automatycznych

W projekcie OrbitalFleet.Tests zaimplementuj zestaw testów weryfikujących zachowanie systemu:

  1. FleetEngineCoordinatorTests:
    • Test wyścigu wielowątkowego: 10 000 jednoczesnych zadań wywołujących AdvanceSimulationTick(). Weryfikacja tożsamości referencyjnej oraz końcowej wartości licznika ticków.
    • Test próby obejścia konstruktora przez refleksję kończący się wyjątkiem InvalidOperationException.
  2. OrbitalShipyardAsyncTests:
    • Test sprawdzający, czy SemaphoreSlim skutecznie ogranicza liczbę równolegle budowanych statków do limitu doków stoczni.
    • Test weryfikujący asynchroniczną synergię taktyczną pomiędzy jednostkami tej samej frakcji oraz zgłoszenie błędu przy próbie połączenia systemów wrogich frakcji.
  3. FlagshipStepBuilderTests:
    • Test weryfikujący poprawność asynchronicznej budowy okrętu przez Fluent Step Builder.
    • Test sprawdzający, czy właściwości obiektu są niemutowalne po opuszczeniu buildera.
  4. ConcurrentPrototypeTests:
    • Test replikacji: Uruchomienie Parallel.ForEachAsync na 500 iteracji klonujących okręt prototypowy metodą DeepClone().
    • Modyfikacja stanu klonów w locie i asercja potwierdzająca, że prototyp bazowy pozostał całkowicie niezmieniony (niezależność referencyjna list i obiektów podrzędnych).

Uruchomienie wszystkich 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 Odporny Singleton i Licznik Double-Checked Lock / Lazy, blokada refleksji, operacje Interlocked, test 10 000 wątków. 20 pkt
2 Asynchroniczna Fabryka IOrbitalShipyardFactory, dławienie doków przez SemaphoreSlim, CoordinateEscortAsync, Metoda Wytwórcza. 24 pkt
3 Fluent Step Builder Maszyna stanów w interfejsach wymuszająca kolejność w compile-time, asynchroniczny zapłon BuildAndIgniteAsync. 20 pkt
4 Wielowątkowy Prototyp MemberwiseClone(), głęboka izolacja pamięci sterty, test pod obciążeniem Parallel.ForEachAsync. 16 pkt
5 Architektura IoC / DI Rejestracja w Autofac, odizolowane zasięgi per zadanie asynchroniczne (LifetimeScope), zero new. 20 pkt
SUMA 100 pkt (100%)