Refaktoryzacja do Clean Architecture – Application Service [4/6] [Część 9]
Wprowadzenie – po co nam Application Service?
Twój Program.cs robi wszystko naraz? Wyświetla komunikaty, tworzy obiekty, ładuje dane, zarządza eventami i jeszcze uruchamia logikę walki? To klasyczny sygnał, że czas na refaktoryzację do Clean Architecture w C#.
W tym wpisie pokażę Ci, jak krok po kroku wydzielić Application Service (GameService) i przygotować kod pod Dependency Injection. Dzięki temu kod stanie się czystszy, testowalny i zgodny z zasadami SOLID.
Czym jest Application Service w Clean Architecture?
Application Service to warstwa aplikacji, której zadania to:
- orkiestracja współpracy między serwisami,
- przechowywanie logiki biznesowej wysokiego poziomu,
- obsługa scenariuszy użycia (use case’ów),
- niezależność od UI i infrastruktury.
Zobacz też: Tworzenie klas w C#
Krok 1: Tworzenie klasy GameService
Zaczynamy od nowej klasy GameService
w warstwie Application.
public class GameService
{
private readonly IPostacRepository _postacRepository;
private readonly IBattleService _battleService;
private List<Postac> _postacie = new();
public GameService(IPostacRepository postacRepository, IBattleService battleService)
{
_postacRepository = postacRepository;
_battleService = battleService;
}
}
👉 Dzięki Dependency Injection serwis nie tworzy zależności samodzielnie – przyjmuje je w konstruktorze.
Krok 2: Wczytywanie postaci z pliku
Przenosimy logikę wczytywania danych z pliku.
public void PobierzPostacie(string sciezkaPliku)
{
_postacie = _postacRepository.PobierzPostacie(sciezkaPliku).ToList();
}
public IReadOnlyList<Postac> Postacie => _postacie.AsReadOnly();
✔️ Dane są enkapsulowane – udostępniamy tylko kolekcję IReadOnlyList
.
- Metoda
PobierzPostacie
przejmuje odpowiedzialność za wczytywanie danych z pliku. - Korzystamy z już istniejącego repository pattern.
- Dodajemy właściwość
Postacie
zwracającąIReadOnlyList
to zapewnia enkapsulację
Krok 3: Walidacja danych
Dodajemy walidacje parametrów i logiki biznesowej.
if (string.IsNullOrWhiteSpace(sciezkaPliku))
throw new ArgumentException("Ścieżka pliku nie może być pusta", nameof(sciezkaPliku));
if (!File.Exists(sciezkaPliku))
throw new FileNotFoundException($"Nie znaleziono pliku: {sciezkaPliku}");
if (_postacie.Count < 2)
throw new InvalidOperationException("Za mało postaci do gry.");
✔️ Dzięki temu błędy są obsługiwane jasno i precyzyjnie.
- Walidacja parametrów: Sprawdzamy czy ścieżka nie jest pusta.
- Walidacja systemu plików: Upewniamy się, że plik istnieje.
- Walidacja biznesowa: Sprawdzamy czy mamy wystarczająco postaci do gry.
- Używamy odpowiednich typów wyjątków dla różnych rodzajów błędów.
Krok 4: Symulacja walki
Dodajemy metodę Start()
, która uruchamia logikę walki.
public Postac Start()
{
return _battleService.Symuluj(_postacie);
}
👉 Program.cs staje się czystszy – zamiast całej logiki wywołujemy jedną metodę.
Krok 5: Wyświetlanie statusów
Logikę wyświetlania przenosimy do GameService.
public void WyswietlStatusy()
{
Postacie.ForEach(p => p.PokazStatus());
}
✔️ Możemy teraz używać tego wielokrotnie, bez duplikacji kodu.
Krok 6: Tworzenie interfejsu IGameService
Tworzymy interfejs, który jasno definiuje możliwości naszej aplikacji.
public interface IGameService
{
IReadOnlyList<Postac> Postacie { get; }
void PobierzPostacie(string sciezkaPliku);
Postac Start();
void WyswietlStatusy();
}
👉 Dzięki temu zyskujemy testowalność, łatwe mockowanie i luźne powiązania (Loose Coupling).
Krok 7: Refaktoryzacja Program.cs
Ostatecznie Program.cs staje się czystym punktem wejścia.
try
{
// Tworzenie bohaterów
var factory = new PostacFactory();
var repository = new JsonPostacRepository(factory);
var battleService = new BattleService();
battleService.KomunikatWygenerowany += Console.WriteLine;
var gameService = new GameService(repository, battleService);
// Ł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}");
}
Teraz kod jest przejrzysty, a logika wyciągnięta do Application Service.
Co zyskaliśmy?
✅ Separacja odpowiedzialności – każda klasa ma jasno określone zadanie.
✅ Łatwiejsze testowanie – możemy testować GameService niezależnie.
✅ Lepsza obsługa błędów – walidacje i wyjątki w odpowiednich miejscach.
✅ Czystszy kod – Program.cs odpowiada tylko za uruchomienie aplikacji.
✅ Przygotowanie pod Dependency Injection – kolejne kroki będą jeszcze prostsze.
Zobacz też
Podsumowanie
Refaktoryzacja do Application Service w Clean Architecture pozwala odchudzić Program.cs i wprowadzić klarowny podział odpowiedzialności.
Dzięki temu kod jest czystszy, bardziej testowalny i rozszerzalny.
Pamiętajcie
Refaktoring to nie przepisywanie od zera,
Refaktoryzacja to proces iteracyjny.
Nie musicie robić wszystkiego na raz.
To stopniowe ulepszanie przy zachowaniu funkcjonalności.
Najważniejsze to robić małe kroki i testować po każdej zmianie,
aby upewnić się, że funkcjonalność pozostaje nietknięta.
👉 W następnym kroku pokażę Ci, jak wprowadzić Dependency Injection Container i wyeliminować ręczne tworzenie obiektów.
📌 Co myślisz o takim podejściu do refaktoryzacji?
Zostaw komentarz pod postem lub na YouTube, a jeśli chcesz więcej praktycznych materiałów
– subskrybuj kanał DevHobby i nie przegap kolejnych odcinków 🚀