Refaktoryzacja kodu do Clean Architecture – BattleService w Core
Masz wrażenie, że Twój Program.cs robi wszystko naraz – od wyświetlania UI po symulację logiki biznesowej? 🚨
To klasyczny objaw spaghetti code.
W tym artykule pokażę Ci, jak przenieść logikę do Service Layer, zyskać czystość architektury i przygotować kod pod testy jednostkowe. Wszystko w praktycznym przykładzie refaktoryzacji RPG w C# i .NET.
Dlaczego Program.cs robi za dużo?
W trakcie pracy nad aplikacjami w C# często spotykamy się z problemem:
- UI, orkiestracja i logika biznesowa znajdują się w jednym pliku,
- kod jest trudny do testowania i rozszerzania,
- każda zmiana w logice wymusza modyfikację warstwy prezentacji.
Typowe objawy złej architektury
- 80+ linii kodu w
Program.cs
- mieszanie
Console.WriteLine
z logiką walki - brak możliwości uruchomienia testów jednostkowych
- chaos i trudność w rozwoju aplikacji
👉 Rozwiązanie? Wydzielenie logiki do Service Layer.
Czym jest Service Layer?
Service Layer to dodatkowa warstwa w Clean Architecture, która:
- enkapsuluje logikę biznesową,
- oddziela Core (domena) od Presentation (UI),
- umożliwia reużycie w różnych kontekstach (API, GUI, testy),
- czyni aplikację łatwiejszą do testowania.
📌 Analogia:
- UI (kelner) – przyjmuje zamówienia,
- Service (kucharz) – przygotowuje danie,
- Repository (magazyn) – dostarcza składniki.
Kelner nie gotuje, kucharz nie obsługuje klientów – tak samo powinno być w Twoim kodzie.
Krok 1: Definiujemy interfejs IBattleService
W Core/Interfaces tworzymy prosty kontrakt dla logiki walki:
using DevHobby.Code.RPG.Core.Entities;
namespace DevHobby.Code.RPG.Core.Interfaces;
public interface IBattleService
{
Postac Sylumuj(IList<Postac> uczestnicy);
}
Dlaczego tak?
- Prosty kontrakt – jeden use case = jedna metoda,
- Elastyczność – w przyszłości możemy dodać różne tryby walki (tournament, battle royale, team battle).
Krok 2: Implementacja BattleService
W Core/Services umieszczamy pełną logikę walki:
using DevHobby.Code.RPG.Core.Entities;
using DevHobby.Code.RPG.Core.Interfaces;
namespace DevHobby.Code.RPG.Core.Services;
public class BattleService : IBattleService
{
private readonly Random random = new Random();
public event Action<string>? KomunikatWygenerowany;
public Postac Sylumuj(IList<Postac> uczestnicy)
{
// Symulacja walki
while (uczestnicy.Count(p => p.PunktyZycia > 0) > 1)
{
// Losowanie atakującego spośród żywych postaci
var zywePostacie = uczestnicy.Where(p => p.PunktyZycia > 0).ToList();
if (zywePostacie.Count <= 1) break;
var atakujacy = zywePostacie[random.Next(zywePostacie.Count)];
// Losowanie celu (różnego od atakującego)
Postac cel;
do
{
cel = zywePostacie[random.Next(zywePostacie.Count)];
} while (cel == atakujacy);
// Wykonanie ataku
atakujacy.Atakuj(cel);
// Krótka pauza, aby śledzić przebieg walki
Thread.Sleep(1000);
// Kontratak
if (cel.PunktyZycia > 0)
{
cel.Atakuj(atakujacy);
}
// Czasem boharter się leczy
if (atakujacy is Bohater bohater && atakujacy.PunktyZycia < 10 && atakujacy.PunktyZycia > 0)
{
GenerujKomunikat($"\n{atakujacy.Imie} używa mikstury!");
bohater.Lecz(30);
}
Thread.Sleep(1000); // Pauza dla dramatyzmu
}
return uczestnicy.First(p => p.PunktyZycia > 0);
}
protected void GenerujKomunikat(string tresc)
{
KomunikatWygenerowany?.Invoke(tresc);
}
}
⚠️ Uwaga: Console.WriteLine
i Thread.Sleep
nie powinny być w Service Layer. To odpowiedzialność UI.
Krok 3: Czyszczenie Program.cs
Przed refaktoryzacją: 80 linii chaosu.
Po refaktoryzacji: czysta orkiestracja.
using DevHobby.Code.RPG.Core.Services;
using DevHobby.Code.RPG.Infrastructure;
using DevHobby.Code.RPG.Infrastructure.Data;
public class Program
{
static void Main()
{
Console.WriteLine("Nacisnij Enter aby zobaczyć Bohaterów Gry");
Console.ReadLine();
Console.WriteLine("=== ARENA WALKI ===\n");
// Tworzenie bohaterów
var factory = new PostacFactory();
var repostory = new JsonPostacRepository(factory);
var battleService = new BattleService();
battleService.KomunikatWygenerowany += Console.WriteLine;
// Wczytanie z pliku konfiguracyjnego
var wszystkiePostacie = repostory.PobierzPostacie("postacie.json");
wszystkiePostacie.ForEach(p => p.KomunikatWygenerowany += Console.WriteLine);
// Pokazujemy status przed walką
wszystkiePostacie.ForEach(p => p.PokazStatus());
Console.WriteLine("Nacisnij Enter aby rozpocząć Grę");
Console.ReadLine();
Console.WriteLine("\n--- WALKA ROZPOCZĘTA! ---");
// CAŁA LOGIKA WALKI W JEDNEJ LINII!
var zwyciezca = battleService.Sylumuj(wszystkiePostacie);
// Podsumowanie
Console.WriteLine("\n=== WALKA ZAKOŃCZONA ===");
wszystkiePostacie.ForEach(p => p.PokazStatus());
if (zwyciezca != null)
Console.WriteLine($"\n🏆 {zwyciezca.Imie} WYGRYWA!");
else
Console.WriteLine("\n 💥 Wszyscy polegli w walce!");
}
}
👉 Teraz Program.cs robi tylko orkiestrację – tak, jak powinien.
Efekty refaktoryzacji
- ✅ Czytelność – Program.cs odpowiada tylko za start aplikacji.
- ✅ Single Responsibility – każda klasa ma jeden cel.
- ✅ Testowalność – logika walki może być sprawdzona w testach jednostkowych.
- ✅ Rozszerzalność – nowe tryby walki = nowe serwisy, bez modyfikacji UI.
Zobacz też
To teraz Twoja kolej! 💪
- Wydziel logikę biznesową do Service Layer w swoim projekcie.
- Podziel się w komentarzu: gdzie w Twoim kodzie UI miesza się z logiką biznesową?
- Subskrybuj YT , aby nie przegapić kolejnych materiałów o Clean Architecture w C#.