Il progetto Snake Game Enterprise rappresenta un'implementazione moderna e sofisticata del classico gioco Snake, realizzato seguendo i principi dell'architettura software enterprise e i design pattern più avanzati. Questo sistema è stato progettato non solo per fornire un'esperienza di gioco coinvolgente, ma anche per dimostrare l'applicazione pratica di principi architetturali solidi in un contesto real-world.
L'architettura adottata segue una Clean Architecture stratificata, dove ogni livello ha responsabilità specifiche e ben definite. Il sistema è stato costruito utilizzando .NET 8 e C# 12, sfruttando le più recenti innovazioni linguistiche come i costruttori primari, le collection expressions e i pattern matching avanzati.
Il sistema è organizzato secondo una Layered Architecture che separa nettamente le responsabilità:
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ (User Interface & Input/Output) │
├─────────────────────────────────────────────────────────────┤
│ APPLICATION LAYER │
│ (Use Cases, Services, Orchestration) │
├─────────────────────────────────────────────────────────────┤
│ CORE LAYER │
│ (Business Logic, Game Engine, Physics) │
├─────────────────────────────────────────────────────────────┤
│ DOMAIN LAYER │
│ (Value Objects, Events, Repositories, Exceptions) │
├─────────────────────────────────────────────────────────────┤
│ INFRASTRUCTURE LAYER │
│ (External Services, Persistence, Audio) │
└─────────────────────────────────────────────────────────────┘
- Inversione delle Dipendenze: Tutti i layer superiori dipendono dalle astrazioni definite nei layer inferiori
- Separazione delle Responsabilità: Ogni classe ha un unico scopo ben definito
- Immutabilità: Ampio utilizzo di Value Objects immutabili e record types
- Event-Driven Design: Comunicazione asincrona attraverso eventi di dominio
- Testabilità: Architettura progettata per facilitare unit testing e integration testing
Il Domain Layer è il fondamento dell'intera architettura, contenendo i concetti di business fondamentali espressi attraverso Value Objects immutabili.
La classe Position rappresenta un esempio perfetto di Value Object:
public readonly struct Position(int x, int y) : IEquatable<Position>
{
public int X { get; } = x;
public int Y { get; } = y;
}Questa implementazione sfrutta il primary constructor di C# 12 per garantire immutabilità fin dalla costruzione. La Position non è semplicemente una coppia di interi, ma un concetto di dominio che incapsula:
- Semantica spaziale: Rappresenta una posizione nel campo di gioco
- Operazioni matematiche: Supporta addizione vettoriale attraverso operator overloading
- Conversioni naturali: Interoperabilità con tuple native tramite implicit operators
- Value semantics: Equality e hash code basati sui valori, non sui riferimenti
Il Value Object Score dimostra come incapsulare business logic direttamente nel dominio:
public readonly struct Score : IEquatable<Score>, IComparable<Score>
{
public int Value { get; }
public Score(int value)
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "Score cannot be negative");
Value = value;
}
public Score Add(int points) => new(Value + points);
public bool IsNewRecord(Score previousRecord) => Value > previousRecord.Value;
}Questa implementazione garantisce che:
- Invarianti di dominio: Un punteggio non può mai essere negativo
- Operazioni immutabili: Ogni modifica produce una nuova istanza
- Business methods: Logica di confronto record incapsulata nel dominio
Il sistema implementa il Domain Events Pattern per la comunicazione loose-coupled tra componenti:
public abstract record GameEvent : IGameEvent
{
public Guid EventId { get; } = Guid.NewGuid();
public DateTime OccurredAt { get; } = DateTime.UtcNow;
}
public record FoodEatenEvent(Position FoodPosition, int NewScore) : GameEvent;
public record LevelUpEvent(int NewLevel) : GameEvent;
public record CollisionEvent(Position CollisionPosition, string CollisionType) : GameEvent;Gli eventi utilizzano record types per garantire immutabilità e forniscono:
- Tracciabilità: Ogni evento ha ID univoco e timestamp
- Rich data: Informazioni complete sul contesto dell'evento
- Type safety: Eventi fortemente tipizzati per evitare errori runtime
Le interfacce repository definiscono contratti puliti per l'accesso ai dati:
public interface IRecordRepository
{
int LoadRecord();
void SaveRecord(int score);
bool CheckAndUpdateRecord(int score);
}Questa astrazione permette di:
- Isolare il dominio: Nessuna dipendenza da specifiche tecnologie di persistenza
- Facilitare testing: Mock facilmente implementabili
- Sostituibilità: Diverse implementazioni (file, database, cloud) intercambiabili
Il Core Layer contiene il motore di gioco e tutta la logica business-critical. La classe SnakeGameEngine rappresenta il cuore pulsante del sistema:
public class SnakeGameEngine(
GameConfig config,
IGameRenderer renderer,
IRecordManager recordManager,
ISoundManager soundManager,
IEventAggregator eventAggregator,
IGameSessionManager sessionManager) : IGameEngineIl motore implementa un game loop asincrono che:
- Gestisce sessioni: Delega la gestione delle singole partite al
GameSessionManager - Coordina componenti: Orchestra renderer, audio, input e persistenza
- Gestisce lifecycle: Supporto per start/stop graceful e cancellation token
- Error handling: Gestione robusta degli errori con fallback appropriati
Il sistema di fisica implementa algoritmi di collision detection ottimizzati:
public class CollisionDetector : ICollisionDetector
{
public bool IsOutOfBounds(Position position, int width, int height)
=> position.X < 0 || position.X >= width || position.Y < 0 || position.Y >= height;
public bool IsOnObstacle(Position position, List<Position> obstacles)
=> obstacles.Contains(position);
public bool IsSelfCollision(Position position, List<Position> snakeSegments)
=> snakeSegments.Contains(position);
}La physics engine sfrutta:
- Pure functions: Metodi senza side effects per predicibile behavior
- Optimized algorithms: Utilizzo efficiente delle equality semantics dei Value Objects
- Clear semantics: Nomi di metodi che esprimono chiaramente l'intent
La classe SnakeShape incapsula tutta la logica di gestione del serpente:
public class SnakeShape : ISnakeShape
{
private readonly List<Position> _segments;
public void Move(Position newHeadPosition)
{
_segments.Insert(0, newHeadPosition); // Aggiungi nuova testa
_segments.RemoveAt(_segments.Count - 1); // Rimuovi coda
}
public void Grow(Position newHeadPosition)
{
_segments.Insert(0, newHeadPosition); // Aggiungi testa, mantieni coda
}
}Questa implementazione dimostra:
- Encapsulation: Stato interno privato con API pubblica controllata
- Defensive copying: Le proprietà pubbliche restituiscono copie per preservare immutabilità
- Efficient operations: Algoritmi ottimizzati per le operazioni più frequenti
Il sistema di configurazione implementa adaptive behavior:
public class GameConfig
{
public void AdaptToFieldSize()
{
var fieldSize = Width * Height;
PointsPerLevel = Math.Max(
GameConstants.LevelProgression.MinimumPointsPerLevel,
(int)(fieldSize * GameConstants.LevelProgression.PointsPerLevelFieldPercentage));
MaxCactus = Math.Max(
GameConstants.ObstacleGeneration.MinimumObstacles,
(int)(fieldSize * GameConstants.ObstacleGeneration.ObstacleFieldPercentage));
}
}La configurazione adattiva garantisce:
- Scalabilità: Il gioco si adatta automaticamente a diverse dimensioni del campo
- Balance: Progressione di difficoltà proporzionale alla dimensione del campo
- Configurabilità: Parametri facilmente modificabili per tuning del gameplay
Il Application Layer orchestrea i use cases del sistema attraverso services dedicati. Il ScoreService esemplifica questa responsabilabilità:
public sealed class ScoreService(IRecordManager recordManager, IEventAggregator eventAggregator) : IScoreService
{
private Score _currentScore = Score.Zero;
public void AddPoints(int points)
{
if (points <= 0)
throw new ArgumentException("Points must be positive", nameof(points));
_currentScore = _currentScore.Add(points);
_eventAggregator.Publish(new ScoreChangedEvent(_currentScore.Value));
}
public bool UpdateRecordIfNeeded()
{
var currentRecord = GetRecordScore();
if (_currentScore.Value > currentRecord.Value)
{
_recordManager.CheckAndUpdateRecord(_currentScore.Value);
_eventAggregator.Publish(new NewRecordSetEvent(_currentScore.Value));
return true;
}
return false;
}
}Il service coordina:
- Domain operations: Utilizza Value Objects per operazioni business
- Event publishing: Notifica cambiamenti attraverso domain events
- Cross-cutting concerns: Integra persistenza e notifiche
Il sistema implementa il Command Pattern per operazioni del gioco:
public sealed class ChangeDirectionCommand(
GameLogic game,
int newDx,
int newDy,
IEventAggregator eventAggregator) : GameCommandBase(eventAggregator)
{
private readonly (int dx, int dy) _previousDirection = game.GetDirection();
public override void Execute() => _game.ChangeDirection(_newDx, _newDy);
public override void Undo() => _game.ChangeDirection(_previousDirection.dx, _previousDirection.dy);
public override bool CanUndo => true;
}Il Command Pattern fornisce:
- Undo/Redo capabilities: Operazioni reversibili per migliore UX
- Macro operations: Possibilità di comporre comandi complessi
- Audit trail: Tracciamento delle operazioni per debugging
L'EnterpriseEventAggregator implementa un sistema di messaggistica avanzato:
public sealed class EnterpriseEventAggregator : IEventAggregator, IDisposable
{
private readonly ConcurrentDictionary<Type, List<EventSubscription>> _subscriptions = [];
public async Task PublishAsync<T>(T gameEvent, CancellationToken cancellationToken = default)
where T : IGameEvent
{
// Esecuzione parallela degli handler con gestione priorità
var handlerTasks = subscriptionsSnapshot
.Where(s => s.CanHandle(gameEvent))
.Select(s => ExecuteHandlerSafelyAsync(s, gameEvent, cancellationToken));
await Task.WhenAll(handlerTasks);
}
}L'event aggregator supporta:
- Async processing: Gestione asincrona degli eventi per performance
- Priority handling: Esecuzione ordinata per priorità degli handler
- Error isolation: Failure di un handler non compromette gli altri
- Thread safety: Operazioni concurrent-safe per ambiente multi-threaded
Il GameMediator implementa il Mediator Pattern per request/response:
public sealed class Mediator(IServiceProvider serviceProvider) : IMediator
{
public async Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
{
var handler = _serviceProvider.GetService(handlerType) ??
throw new InvalidOperationException($"No handler registered for request type {requestType.Name}");
return await ((Task<TResponse>)method.Invoke(handler, [request, cancellationToken]));
}
}Il mediator facilita:
- Loose coupling: Sender e receiver non si conoscono direttamente
- Dynamic dispatch: Risoluzione runtime degli handler appropriati
- Cross-cutting concerns: Gestione uniforme di logging, validation, caching
Il GameFactory centralizza la creazione di oggetti di gioco:
public sealed class GameFactory(...) : IGameFactory
{
public GameLogic CreateGame()
{
Guard.ValidGameConfiguration(_config.Width, _config.Height);
var snakeShape = CreateSnakeShape();
return new GameLogic(_config.Width, _config.Height, _scoreService,
_levelService, snakeShape, _foodGenerator, _collisionDetector,
_obstacleManager, _eventAggregator, _gameProgressionService);
}
}La factory garantisce:
- Consistent creation: Oggetti sempre creati con dipendenze corrette
- Validation: Controlli di validità centralizzati
- Dependency resolution: Gestione automatica delle dipendenze
Il sistema audio implementa il Strategy Pattern per gestire diverse modalità di output:
public class SoundManager : ISoundManager
{
private IMidiPlayer _currentPlayer;
public void SetSoundEnabled(bool enabled)
{
_currentPlayer = enabled ? _midiPlayer : new SilentMidiPlayer();
}
}Il sistema audio supporta:
- Strategy switching: Cambio runtime tra audio attivo e silenzioso
- Async operations: Riproduzione non-bloccante per mantenere performance
- Event-driven: Risposta automatica agli eventi di gioco
Il sistema di persistenza implementa Repository e Unit of Work patterns:
public class FileRecordRepository : IRecordRepository
{
private const string RecordFilePath = "game_record.dat";
public bool CheckAndUpdateRecord(int score)
{
var currentRecord = LoadRecord();
if (score > currentRecord)
{
SaveRecord(score);
return true;
}
return false;
}
}La persistenza fornisce:
- Abstraction: Logica di storage nascosta dietro interfacce
- Atomic operations: Transazioni per operazioni multi-step
- Error handling: Gestione robusta di errori I/O
Il sistema di I/O console astrae le operazioni di sistema:
public interface IConsoleService
{
ConsoleKeyInfo ReadKey(bool intercept = false);
void SetCursorPosition(int left, int top);
bool KeyAvailable { get; }
Task InitializeAsync();
}L'astrazione console permette:
- Testability: Mock dell'input/output per unit tests
- Cross-platform: Potenziale supporto per diverse piattaforme
- Async initialization: Setup non-bloccante delle risorse
Il sistema di rendering implementa Template Method e Strategy patterns:
public class ConsoleGameRenderer : IGameRenderer
{
public void DrawGame(int width, int height, List<Position> snake,
Position food, List<Position> obstacles, int score, int level, int record)
{
_menuRenderer.DrawGameBoard(width, height);
DrawSnake(snake);
DrawFood(food);
DrawObstacles(obstacles);
DrawUI(score, level, record);
}
}Il rendering system supporta:
- Modular drawing: Componenti separati per diversi elementi di gioco
- Template rendering: Uso di template per layout consistenti
- Performance optimization: Rendering selettivo per minimizzare flickering
Il sistema di menu utilizza Composite Pattern per gestire UI complesse:
public class MenuRow
{
public required string Text { get; init; }
public required ConsoleColor Color { get; init; }
public bool IsCentered { get; init; } = false;
}Il menu system offre:
- Hierarchical structure: Menu e sottomenu componibili
- Flexible styling: Controllo granulare di colori e posizionamento
- Responsive layout: Adattamento automatico a diverse dimensioni console
- Separation of Concerns: Ogni layer ha responsabilità specifiche
- Dependency Inversion: Dipendenze puntano verso l'interno
- Testability: Architettura facilitante unit e integration testing
- Value Objects: Modellazione di concetti di business immutabili
- Domain Events: Comunicazione di cambiamenti significativi
- Repository Pattern: Astrazione dell'accesso ai dati
- Event Sourcing elements: Eventi come first-class citizens
- Pub/Sub Pattern: Disaccoppiamento attraverso eventi asincroni
- Event Aggregator: Hub centralizzato per la gestione eventi
- Command Pattern: Operazioni che modificano stato
- Query Methods: Operazioni di sola lettura
- Mediator Pattern: Gestione uniforme di commands e queries
- Audio Strategies: Diversi comportamenti audio (sound/silent)
- Food Generation: Algoritmi intercambiabili per posizionamento cibo
- Collision Detection: Diversi algoritmi di collision detection
- Game States: Gestione stati di gioco (Running, Paused, GameOver)
- Transition Logic: Logica controllata per cambio stato
- State Encapsulation: Comportamenti specifici per ogni stato
- Game Factory: Creazione centralizzata di oggetti di gioco
- Abstract Factory: Famiglie di oggetti correlati
- Builder Pattern elements: Costruzione step-by-step di oggetti complessi
- Event Aggregator: Notifica automatica di cambiamenti
- Domain Events: Osservazione di eventi di business
- Reactive Extensions: Reattività ai cambiamenti di stato
- Bootstrap: Il
Program.csconfigura DI container e inizializza host - Engine Start:
SnakeGameEngineavvia il game loop principale - Session Management:
GameSessionManagergestisce singole partite - Game Loop:
GameRunneresegue il ciclo render-input-update - Business Logic:
GameLogicprocessa regole di gioco e collisioni - Event Processing:
EventAggregatorpropaga eventi ai listeners - Presentation:
ConsoleGameRendereraggiorna la visualizzazione
User Input → InputHandler → GameRunner → GameLogic → Domain Events
↓
Domain Events → EventAggregator → Application Services → Infrastructure
↓
Infrastructure → Persistence/Audio → External Resources
Il sistema utilizza Microsoft.Extensions.DependencyInjection con extension methods per ogni layer:
services.AddCoreServices() // Physics, Game Engine, Configuration
.AddApplicationServices() // Use Cases, Services, Factories
.AddInfrastructureServices()// Persistence, Audio, Console
.AddPresentationServices(); // Rendering, Menu SystemIl progetto sfrutta le più recenti innovazioni del linguaggio:
- Primary Constructors: Sintassi concisa per dependency injection
- Collection Expressions: Inizializzazione elegante di collezioni con
[] - Pattern Matching: Switch expressions per logica decisionale
- Record Types: Immutabilità built-in per Value Objects e Events
- Global Using: Semplificazione delle dichiarazioni using
- File-scoped Namespaces: Riduzione dell'indentazione
- Struct Value Objects: Riduzione allocazioni heap per oggetti frequenti
- Async/Await: Operazioni non-bloccanti per I/O e rendering
- Concurrent Collections: Thread-safety per scenari multi-threaded
- Object Pooling elements: Riutilizzo di oggetti per performance
- Efficient Algorithms: Algoritmi ottimizzati per collision detection
- Exception Hierarchy: Eccezioni tipizzate per diversi scenari
- Graceful Degradation: Fallback behavior in caso di errori
- Cancellation Token: Supporto per operazioni cancellabili
- Defensive Programming: Validation e null-checking sistematici
- Domain Layer: 15 classi (Value Objects, Events, Contracts)
- Core Layer: 25 classi (Game Engine, Physics, Configuration)
- Application Layer: 20 classi (Services, Use Cases, Orchestration)
- Infrastructure Layer: 18 classi (External Services, I/O, Persistence)
- Presentation Layer: 12 classi (Rendering, UI Components)
- 90+ interfacce: Quasi ogni componente pubblico ha un'interfaccia
- Dependency Inversion: Nessuna dipendenza da implementazioni concrete
- Testability: Ogni componente facilmente mockabile per testing
- Value Objects: Tutti immutabili per design
- Record Types: Eventi immutabili con structural equality
- Concurrent Collections: Strutture dati thread-safe dove necessario
- Pure Functions: Metodi senza side effects dove possibile
Il sistema è progettato per essere facilmente estendibile:
- Nuovi Game Elements: Aggiunta di power-ups, nemici, bonus
- Algoritmi AI: Implementazione di bot players
- Multiplayer Support: Estensione per gioco multi-giocatore
- Different Renderers: Web, WPF, mobile renderers
- Persistence Backends: Database, cloud storage, cache
- Input Methods: Gamepad, touch, network input
- Strategy Pattern: Nuovi algoritmi intercambiabili
- Event System: Nuovi handler per eventi esistenti
- Dependency Injection: Sostituzione servizi esistenti
- Interface Segregation: Implementazione parziale di funzionalità
- Composition: Combinazione di comportamenti esistenti
Il progetto Snake Game Enterprise rappresenta un esempio eccellente di come i principi dell'ingegneria software moderna possano essere applicati anche a progetti apparentemente semplici. L'architettura implementata dimostra:
- Scalabilità: Architettura che supporta crescita di complessità
- Manutenibilità: Codice organizzato e facilmente comprensibile
- Testabilità: Struttura che facilita testing automatizzato
- Riusabilità: Componenti modulari riutilizzabili in altri contesti
- Performance: Ottimizzazioni per gaming real-time
- Over-engineering awareness: Bilanciamento tra robustezza e semplicità
- Pattern application: Uso appropriato di design patterns
- Modern C# features: Sfruttamento delle novità linguistiche
- Clean code principles: Codice leggibile e auto-documentante
Questo progetto serve come:
- Reference implementation: Esempio di architettura enterprise
- Learning platform: Studio di design patterns in azione
- Best practices showcase: Dimostrazione di principi SOLID
- Modern .NET: Utilizzo di tecnologie e linguaggi aggiornati
Il sistema dimostra che anche un gioco semplice può beneficiare di un'architettura sofisticata, risultando in un codebase maintainable, testable ed extensible che può servire come base per progetti più complessi o come piattaforma di apprendimento per sviluppatori che vogliono approfondire i principi dell'architettura software moderna.