È importante capire la tua applicazione in termini di architettura. Penso che la tua confusione sia probabilmente causata da una mancanza di comprensione delle perché le applicazioni sono strutturate come sono. Partendo dalle basi, un'architettura tradizionale "a strati", assomiglierà a qualcosa del tipo:
View -> Application -> Domain -> Persistence
L'idea è che ogni livello è completamente disaccoppiato dagli strati sopra di esso, in modo tale che il flusso delle dipendenze in "verso il basso". La gestione delle dipendenze e l'accoppiamento è la preoccupazione più critica di qualsiasi architettura! Ciò che sopra compie è la possibilità di "scambiare" qualsiasi layer in modo tale che sappiamo quali altri pezzi dell'applicazione possono essere modificati come risultato (i livelli direttamente sopra) . Con questa idea potente in mente, ha senso strutturare le tue dipendenze in un modo in cui:
Volatile Modules -> Stable Modules
Poiché il tuo Domain
è probabilmente il modulo meno volatile (quasi certamente non verrà sostituito), possiamo usare il DIP (principal di inversione delle dipendenze) per "sistemare" l'architettura stratificata sopra a:
View -> Application -> Domain <- Persistence
Ora abbiamo raggiunto l'architettura che stai descrivendo nella tua applicazione. Ora esaminiamo ogni livello e individuiamo ciò che ciascuno dovrebbe essere responsabile.
Il tuo livello View
è il luogo in cui vivono i tuoi controllori. Ogni Controller
semplicemente media il flusso di dati (e controllo) verso il tuo livello Application
. È importante sottolineare che i controller non contengono la logica aziendale . Contengono solo la logica necessaria per trasformare l'input dal mondo esterno in concetti che il tuo strato Application
può comprendere e trasformare l'output dal tuo strato Application
in concetti che il mondo esterno può comprendere. Se abbiamo "scambiato" il tuo layer View
(o ne abbiamo aggiunto un altro) da un'interfaccia web all'interfaccia della riga di comando, avresti avuto bisogno di nuovi controller. Gli input / output per View
sarebbero necessariamente diversi.
Il livello Application
viene utilizzato per eseguire casi d'uso per i tuoi utenti. Lo fa coordinando il tuo Domain
. Ancora una volta, è importante che questo livello non contenga logica aziendale. Ad esempio, per cambiare una password:
class ChangePasswordCommand
{
private string newPassword;
public string UserId { get; set; }
public string CurrentPassword { get; set; }
public string NewPassword
{
get { return newPassword; }
set {
if( value.Length > 50 ) {
throw new System.ArgumentException("New password must be less than 50 characters");
}
newPassword = value;
}
}
}
class ChangePasswordCommandHandler
{
private IUserRepository userRepository;
handle( ChangePasswordCommand cmd )
{
userId = cmd.UserId;
newPassword = cmd.NewPassword;
currentPassword = cmd.CurrentPassword;
user = this.userRepository.FindById(userId);
// this may raise a UserChangedPasswordDomainEvent
// or throw a PasswordIsNotDifferentException
user.changePassword(newPassword, currentPassword);
this.userRepository.save(user)
}
}
Si noti che non si verifica qui alcuna "logica" pertinente alla modifica effettiva di una password. L'invariante che ChangePasswordCommand
protegge non ha nulla a che fare con la tua logica aziendale . Cioè, il tuo CEO probabilmente non è preoccupato della lunghezza di una password Users
. La lunghezza della password invariante è un dettaglio tecnico del sistema (ad esempio il campo del database di backup è VARCHAR (50) o qualcosa del genere) e vogliamo "fallire velocemente".
Il gestore di comandi, di per sé, prende semplicemente un input che capisce ( ChangePasswordCommand
) e coordina il modello del dominio per eseguire il comando. Cose come la gestione delle transazioni dovrebbero verificarsi anche nel tuo livello Application
. Pensa a ogni caso d'uso come a un problema in 3 fasi:
- Ottieni pezzi di modello di dominio nella memoria necessari per eseguire il caso d'uso.
- Coordinate modello di dominio (cambia stato)
2.5. Gestisci / bolla le eccezioni
- Modello dominio persistente
Vale anche la pena notare che la dipendenza da IUserRepository
. Questa interfaccia è definita nel tuo livello Domain
, ma implementata nel tuo livello Persistence
(questo è il DI aggiunto).
Seguendo il flusso del controllo, raggiungiamo il tuo livello Persistence
. Questo è dove esiste il tuo ORM (e solo qui!). Ad esempio il UserRepository.FindById
potrebbe essere simile a:
public User FindById( string userId )
{
// 1. get UserEntity
// 2. map UserEntity to User
// 3. return User
}
Non c'è davvero bisogno di qualche DTO extra o altro a meno che non si intenda utilizzare una libreria per la mappatura che richiede un determinato formato. Ancora una volta, l'idea è che questo livello può essere completamente sostituito e NESSUN ALTRO parte della tua applicazione potrebbe nemmeno conoscere la differenza. Gli unici vettori di cambiamento per questo livello sono quando modifichi / aggiorni il tuo ORM o il Domain
.
Infine, ti mostriamo il Domain
. Questo è dove sta accadendo tutto. È qui che ci assicuriamo che 2 password non possano essere uguali e che modifichino il modello di dominio di conseguenza. Non c'è bisogno di troppe spiegazioni qui.
Infine, vorrei fare una nota qui che TUTTO QUI SOPRA è veramente necessario solo nel contesto della SCRITTURA ad un sistema. Puoi leggerlo da quel che vuoi (non c'è pericolo di uno stato non valido durante una lettura). Se hai bisogno di permessi utente, prendili. Non complicarlo troppo. Ottieni i dati necessari per il tuo View
come vuoi, MA assicurati di scrivere solo i dati attraverso i canali appropriati (il tuo Domain
). Questa è l'idea di CQRS.
Sei libero di creare un progetto di modello di lettura separato, ma in questo progetto non vi è alcun concetto o bisogno di una Domain
, quindi finirà per essere ridondante. Meglio definire solo alcuni servizi di lettura per organizzarlo, se lo desideri. Riesco a capire la confusione di alcuni metodi Controller
(letture) utilizzando un progetto e altri metodi (scritture) utilizzando un altro, ma un View
ne ha bisogno entrambi.