Jak wydzielić GameRunner i zastosować SRP – Refaktoryzacja do Clean Architecture [6/6] [Część 11]
Twój Program.cs
zaczyna przypominać centrum dowodzenia NASA?
Tworzy kontener DI, uruchamia grę, obsługuje błędy i jeszcze wyświetla wyniki?
To znak, że pora na refaktoryzację i wdrożenie zasady SRP (Single Responsibility Principle)!
W tym artykule pokażę Ci, jak krok po kroku wydzielić klasę GameRunner
, uporządkować odpowiedzialności i nadać Twojej aplikacji czytelność, testowalność i profesjonalną strukturę.
🧩 Czym jest SRP i dlaczego ma znaczenie
Zasada Single Responsibility Principle (SRP) mówi, że klasa powinna mieć tylko jeden powód do zmiany. W praktyce oznacza to, że każda klasa powinna odpowiadać za jedno, konkretne zadanie.
Przykład:
Program.cs
powinien tylko uruchamiać aplikację (bootstrap),GameRunner
powinien zarządzać przebiegiem gry,BattleService
– odpowiadać za logikę walki.
To prosty, ale kluczowy krok w stronę czystej architektury.
💡 Zobacz też: Tworzenie klas w C# – zasady i najlepsze praktyki
⚙️ Problem: przeładowany Program.cs
Wielu programistów zaczyna z czymś takim:
using DevHobby.Code.RPG.Application.Interfaces;
using DevHobby.Code.RPG.Infrastructure.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Program
{
static async Task Main(string[] args)
{
// Tworzymy i konfigurujemy Host
var host = CreateHostBuilder(args).Build();
// Uruchamiamy aplikację
await RunGameAsync(host);
}
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddRpgServices();
});
private static async Task RunGameAsync(IHost host)
{
Console.WriteLine("Nacisnij Enter aby zobaczyć Bohaterów Gry");
Console.ReadLine();
Console.WriteLine("=== ARENA WALKI ===\n");
try
{
// Tworzymy scope dla tej sesji gry
using var serviceScope = host.Services.CreateScope();
// DI Container automatycznie dostarcza wszystkie zależności
var gameService = serviceScope.ServiceProvider.GetRequiredService<IGameService>();
// Ładowanie postaci
gameService.PobierzPostacie(("postacie.json"));
// Podłączanie event handlerów
foreach (var postac in gameService.Postacie)
postac.KomunikatWygenerowany += Console.WriteLine;
// Wyświetlanie statusów przed walką
gameService.WyswietlStatusy();
Console.WriteLine("Nacisnij Enter aby rozpocząć Grę");
Console.ReadLine();
Console.WriteLine("\n--- WALKA ROZPOCZĘTA! ---");
// Start walki
var zwyciezca = gameService.Start();
// Podsumowanie
Console.WriteLine("\n=== WALKA ZAKOŃCZONA ===");
gameService.WyswietlStatusy();
if (zwyciezca != null)
Console.WriteLine($"\n🏆 {zwyciezca.Imie} WYGRYWA!");
else
Console.WriteLine("\n 💥 Wszyscy polegli w walce!");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Błąd: {ex.Message}");
}
}
}
Wygląda niewinnie, ale w miarę rozwoju projektu Program.cs
:
- tworzy zależności,
- inicjuje logikę,
- obsługuje błędy,
- uruchamia grę,
- wyświetla wyniki…
To klasyczny przypadek złamania SRP. Program.cs robi wszystko, więc ciężko go testować i rozwijać.
🧱 Rozwiązanie: wydzielenie GameRunner
Celem refaktoryzacji jest przeniesienie całej logiki uruchamiania gry do nowej klasy GameRunner
.
✳️ Krok 1: Utwórz klasę GameRunner
using DevHobby.Code.RPG.Application.Interfaces;
using DevHobby.Code.RPG.Core.Entities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace DevHobby.Code.RPG.ConsoleApp;
public class GameRunner
{
public async Task RunAsync(IHost host)
{
try
{
// Tworzymy scope dla tej sesji gry
using var serviceScope = host.Services.CreateScope();
// DI Container automatycznie dostarcza wszystkie zależności
var gameService = serviceScope.ServiceProvider.GetRequiredService<IGameService>();
await RunGameAsync(gameService);
}
catch (FileNotFoundException ex)
{
DisplayError($"Nie znaleziono pliku: {ex.Message}");
}
catch (InvalidOperationException ex)
{
DisplayError($"Błąd konfiguracji: {ex.Message}");
}
catch (Exception ex)
{
DisplayError($"Nieoczekiwany Błąd: {ex.Message}");
}
finally
{
// Ten kod wykona się ZAWSZE - nawet jeśli wystąpił błąd
WaitForExit();
}
}
private async Task RunGameAsync(IGameService gameService)
{
WaitForUserInput("Nacisnij Enter aby zobaczyć Bohaterów Gry");
DisplayWelcomeMessage();
InitializeGame(gameService);
SetupEventHandlers(gameService);
DisplayGameStatus(gameService);
WaitForUserInput("Nacisnij Enter aby rozpocząć Grę");
var zwyciezca = StartBattle(gameService);
DisplayResults(gameService, zwyciezca);
}
private void DisplayResults(IGameService gameService, Postac zwyciezca)
{
// Podsumowanie
Console.WriteLine("\n=== WALKA ZAKOŃCZONA ===");
gameService.WyswietlStatusy();
if (zwyciezca != null)
Console.WriteLine($"\n🏆 {zwyciezca.Imie} WYGRYWA!");
else
Console.WriteLine("\n 💥 Wszyscy polegli w walce!");
}
private Postac StartBattle(IGameService gameService)
{
Console.WriteLine("\n--- WALKA ROZPOCZĘTA! ---");
// Start walki
return gameService.Start();
}
private void DisplayGameStatus(IGameService gameService)
{
// Wyświetlanie statusów przed walką
gameService.WyswietlStatusy();
}
private void SetupEventHandlers(IGameService gameService)
{
// Podłączanie event handlerów
foreach (var postac in gameService.Postacie)
postac.KomunikatWygenerowany += Console.WriteLine;
}
private void InitializeGame(IGameService gameService)
{
// Ładowanie postaci
var configPath = GetConfigPath();
gameService.PobierzPostacie(configPath);
}
private string GetConfigPath()
{
return Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
Program.CONFIG_FILE_NAME);
}
private void DisplayWelcomeMessage()
{
Console.WriteLine("=== ARENA WALKI ===\n");
}
private void WaitForUserInput(string message)
{
Console.WriteLine(message);
Console.ReadLine();
}
private void DisplayError(string message)
{
Console.WriteLine($"❌ {message}");
}
private void WaitForExit()
{
Console.WriteLine("\nNaciśnij dowolny klawisz aby zakończyć...");
Console.ReadKey();
}
}
🧠 Co się zmieniło:
GameRunner
przejmuje odpowiedzialność za uruchamianie i obsługę wyjątków,Program.cs
pozostaje prosty i czysty,- logika gry jest łatwa do testowania i rozszerzania.
✳️ Krok 2: Uprość Program.cs
using DevHobby.Code.RPG.ConsoleApp;
using DevHobby.Code.RPG.Infrastructure.Extensions;
using Microsoft.Extensions.Hosting;
public class Program
{
// Stała zamiast "magic string" - łatwo zmienić w jednym miejscu
public const string CONFIG_FILE_NAME = "postacie.json";
static async Task Main(string[] args)
{
// Tworzymy i konfigurujemy Host
var host = CreateHostBuilder(args).Build();
// Uruchamiamy aplikację
var gameRunner = new GameRunner();
await gameRunner.RunAsync(host);
}
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddRpgServices();
});
}
Teraz Program.cs
pełni tylko jedną rolę – buduje kontener DI i uruchamia aplikację.
Resztą zajmuje się GameRunner
.
✳️ Krok 3: Zyskaj testowalność
Dzięki tej refaktoryzacji możesz teraz łatwo testować GameRunner
w izolacji:
[Fact]
public async Task RunAsync_ShouldHandleExceptionsGracefully()
{
var mockGameService = new Mock<IGameService>();
mockGameService.Setup(x => x.RunAsync()).ThrowsAsync(new Exception("Test error"));
var runner = new GameRunner(mockGameService.Object);
await runner.RunAsync();
// Sprawdź, że błąd został obsłużony, a aplikacja nie padła
}
💪 Korzyści z wydzielenia GameRunner
✔️ Czysty, czytelny Program.cs
✔️ Zasada SRP w praktyce
✔️ Łatwiejsze testy jednostkowe
✔️ Mniej błędów przy rozwoju projektu
✔️ Kod gotowy do rozbudowy i utrzymania
🚀 Podsumowanie
Refaktoryzacja Program.cs
i wydzielenie GameRunner
to prosta zmiana, która radykalnie poprawia czytelność, strukturę i testowalność Twojego projektu.
To właśnie esencja Clean Architecture — oddzielenie odpowiedzialności, klarowny przepływ zależności i logika podzielona na sensowne warstwy.
👉 Zobacz też: Wprowadzenie do Clean Architecture w C#
👉 Następny krok: Testowanie warstwy aplikacji w C# – praktyczne podejście
Jeśli ten artykuł pomógł Ci uporządkować Program.cs
,
zostaw komentarz poniżej 👇 i napisz, jakie klasy w Twoim projekcie potrzebują refaktoryzacji.
Chcesz więcej praktycznych przykładów Clean Architecture w C#? – C# Clean Architecture w Praktyce