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ą:
- Naiwny Singleton bez zabezpieczeń prowadzi do wyścigów pamięci i korupcji stanu globalnego,
- Synchroniczne tworzenie złożonych obiektów blokuje wątki puli
(
ThreadPool starvation), - Niewłaściwa implementacja klonowania obiektów w środowisku
wielozadaniowym wywołuje błędy współbieżnej modyfikacji kolekcji
(
InvalidOperationException: Collection was modified).
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:
- [4 pkt] Struktura wzorca: Klasa
sealed, ściśle prywatny konstruktor blokującynew, statyczna metoda dostępuGetInstance()lub właściwośćInstance. - [6 pkt] Bezpieczeństwo wątkowe: Pełna ochrona przed
wyścigami w środowisku wielowątkowym zrealizowana za pomocą
lock(Double-Checked Locking) lubSystem.Lazy<T>. - [6 pkt] Ochrona przed refleksją i atomowość stanu:
- Konstruktor weryfikuje, czy instancja już istnieje na stercie; próba
ominięcia modyfikatora
privateprzezActivator.CreateInstancemusi skutkować rzuceniem wyjątkuInvalidOperationException. - Klasa udostępnia metodę
long AdvanceSimulationTick(), która inkrementuje globalny licznik w sposób atomowy przy użyciuInterlocked.Increment.
- Konstruktor weryfikuje, czy instancja już istnieje na stercie; próba
ominięcia modyfikatora
- [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.Runw pętli lubParallel.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:
- Sojusz Solarny (Solar Alliance): Myśliwiec
Przechwytujący (
SolarInterceptor) oraz Pancernik Ogniowy (SolarDreadnought). - Konsorcjum Pustki (Void Syndicate):
Myśliwiec Maskujący (
VoidInfiltrator) oraz Krążownik Antymaterii (VoidCarrier).
Wymagania szczegółowe:
- [6 pkt] Abstrakcja asynchronicznej stoczni:
Zdefiniuj interfejsy
IEscortShiporazICapitalShip. Utwórz interfejs fabryki abstrakcyjnejIOrbitalShipyardFactoryz asynchronicznymi metodami wytwórczymi:ValueTask<IEscortShip> BuildEscortAsync(string callsign, CancellationToken ct = default); ValueTask<ICapitalShip> BuildCapitalShipAsync(string callsign, CancellationToken ct = default); - [8 pkt] Konkretne stocznie i kontrola przepustowości
doków:
- Zaimplementuj stocznie
SolarShipyardFactoryorazVoidShipyardFactory. - 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).
- Zaimplementuj stocznie
- [6 pkt] Asynchroniczna koordynacja ognia i kompatybilność
frakcyjna:
- Klasa
ICapitalShipmusi 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.
- Klasa
- [4 pkt] Metoda Wytwórcza w Bazowym Dowódcy Eskadry:
- Klasa bazowa
SquadronCommanderposiada szablon metodyTask DeployPatrolAsync(), który wywołuje wirtualną/abstrakcyjną metodę wytwórcząCreateEscortShip()i asynchronicznie wyprowadza jednostki na orbitę.
- Klasa bazowa
💡 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:
- [6 pkt] Hermetyzacja i niemutowalność: Klasa
Flagshipposiada właściwości dostępne tylko do odczytu z zewnątrz (initlubinternal set). Konstruktor klasyFlagshipjest niepubliczny. Klient nie może utworzyć statku przeznew Flagship(). - [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ć! - [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:
- [4 pkt] Kontrakt prototypu: Zdefiniuj interfejs
IQuantumCloneable<T>z metodamiT ShallowClone()orazT DeepClone(). - [4 pkt] Kopia płytka: Implementacja w oparciu o
MemberwiseClone(). - [8 pkt] Bezpieczna kopia głęboka i test
równoległości:
- Klasa
Flagshipposiada zagnieżdżony obiektCommanderProfile(imię, ranga, neuro-wskaźniki) oraz kolekcję podsystemówList<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.ForEachAsyncsklonuj 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.
- Klasa
💡 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:
- [6 pkt] Rejestracja w kontenerze IoC:
- Zarejestruj fabryki stoczniowe z kluczem/nazwą
(
Named<IOrbitalShipyardFactory>("Solar"),Named<IOrbitalShipyardFactory>("Void")). - Zarejestruj Singleton
FleetEngineCoordinatorjakoSingleInstance(). - Zarejestruj serwis operacyjny
ISectorOperationService.
- Zarejestruj fabryki stoczniowe z kluczem/nazwą
(
- [6 pkt] Izolacja cyklu życia w zadaniach asynchronicznych
(
ILifetimeScope):- Zdefiniuj serwis
ISectorContext(generujący unikalnyGuid SectorIdoraz ś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 osobnymusing (var scope = container.BeginLifetimeScope()). Dowiedź, że instancjeISectorContextw osobnych zadaniach posiadają unikalne identyfikatory, a usługi wewnątrz tego samego zadania współdzielą ten sam kontekst.
- Zdefiniuj serwis
- [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
newdla 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.Waiti.Resultsą surowo zabronione – używaj wyłącznieawait).
- W kodzie biznesowym i orkiestrującym nie może pojawić się
ani jedno słowo kluczowe
💡 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:
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.
- Test wyścigu wielowątkowego: 10 000 jednoczesnych zadań wywołujących
OrbitalShipyardAsyncTests:- Test sprawdzający, czy
SemaphoreSlimskutecznie 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.
- Test sprawdzający, czy
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.
ConcurrentPrototypeTests:- Test replikacji: Uruchomienie
Parallel.ForEachAsyncna 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).
- Test replikacji: Uruchomienie
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%) |