Dichiarazione di non responsabilità: poiché non ci sono ancora ottime risposte, ho deciso di pubblicare una parte di un ottimo post sul blog che ho letto qualche tempo fa, copiato quasi alla lettera. Puoi trovare il post completo del blog qui . Quindi eccolo qui:
Possiamo definire le seguenti due interfacce:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
Le IQuery<TResult>
Specifica un messaggio che definisce una query specifica con i dati restituisce utilizzando il TResult
tipo generico. Con l'interfaccia definita in precedenza possiamo definire un messaggio di query come questo:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
Questa classe definisce un'operazione di query con due parametri, che risulterà in un array di User
oggetti. La classe che gestisce questo messaggio può essere definita come segue:
public class FindUsersBySearchTextQueryHandler
: IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
private readonly NorthwindUnitOfWork db;
public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db)
{
this.db = db;
}
public User[] Handle(FindUsersBySearchTextQuery query)
{
return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray();
}
}
Ora possiamo consentire ai consumatori di dipendere IQueryHandler
dall'interfaccia generica :
public class UserController : Controller
{
IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler;
public UserController(
IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler)
{
this.findUsersBySearchTextHandler = findUsersBySearchTextHandler;
}
public View SearchUsers(string searchString)
{
var query = new FindUsersBySearchTextQuery
{
SearchText = searchString,
IncludeInactiveUsers = false
};
User[] users = this.findUsersBySearchTextHandler.Handle(query);
return View(users);
}
}
Immediatamente questo modello ci offre molta flessibilità, perché ora possiamo decidere cosa iniettare nel UserController
. Possiamo iniettare un'implementazione completamente diversa, o una che avvolge l'implementazione reale, senza dover apportare modifiche a UserController
(ea tutti gli altri utenti di quell'interfaccia).
L' IQuery<TResult>
interfaccia ci fornisce il supporto in fase di compilazione quando si specifica o si inserisce IQueryHandlers
il codice. Quando cambiamo invece il FindUsersBySearchTextQuery
to return UserInfo[]
(implementandolo IQuery<UserInfo[]>
), la UserController
compilazione fallirà, poiché il vincolo di tipo generico su IQueryHandler<TQuery, TResult>
non sarà in grado di mappare FindUsersBySearchTextQuery
a User[]
.
IQueryHandler
Tuttavia, l'iniezione dell'interfaccia in un consumatore presenta alcuni problemi meno evidenti che devono ancora essere risolti. Il numero di dipendenze dei nostri consumatori potrebbe diventare troppo grande e può portare a un'iniezione eccessiva del costruttore, quando un costruttore accetta troppi argomenti. Il numero di query eseguite da una classe può cambiare frequentemente, il che richiederebbe modifiche costanti nel numero di argomenti del costruttore.
Possiamo risolvere il problema di dover iniettarne troppi IQueryHandlers
con un ulteriore livello di astrazione. Creiamo un mediatore che si trova tra i consumatori e i gestori di query:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
La IQueryProcessor
è un'interfaccia non generica con un metodo generico. Come puoi vedere nella definizione dell'interfaccia, IQueryProcessor
dipende IQuery<TResult>
dall'interfaccia. Questo ci consente di avere supporto in fase di compilazione nei nostri consumatori che dipendono da IQueryProcessor
. Riscriviamo il UserController
per usare il nuovo IQueryProcessor
:
public class UserController : Controller
{
private IQueryProcessor queryProcessor;
public UserController(IQueryProcessor queryProcessor)
{
this.queryProcessor = queryProcessor;
}
public View SearchUsers(string searchString)
{
var query = new FindUsersBySearchTextQuery
{
SearchText = searchString,
IncludeInactiveUsers = false
};
// Note how we omit the generic type argument,
// but still have type safety.
User[] users = this.queryProcessor.Process(query);
return this.View(users);
}
}
L' UserController
adesso dipende da un in IQueryProcessor
grado di gestire tutte le nostre domande. Il UserController
's SearchUsers
metodo chiama il IQueryProcessor.Process
metodo passando un oggetto query inizializzata. Poiché FindUsersBySearchTextQuery
implementa l' IQuery<User[]>
interfaccia, possiamo passarla al Execute<TResult>(IQuery<TResult> query)
metodo generico . Grazie all'inferenza del tipo C #, il compilatore è in grado di determinare il tipo generico e questo ci evita di dover dichiarare esplicitamente il tipo. È Process
noto anche il tipo restituito del metodo.
È ora responsabilità dell'attuazione del IQueryProcessor
trovare il diritto IQueryHandler
. Ciò richiede una digitazione dinamica e, facoltativamente, l'uso di un framework di inserimento delle dipendenze e può essere fatto con poche righe di codice:
sealed class QueryProcessor : IQueryProcessor
{
private readonly Container container;
public QueryProcessor(Container container)
{
this.container = container;
}
[DebuggerStepThrough]
public TResult Process<TResult>(IQuery<TResult> query)
{
var handlerType = typeof(IQueryHandler<,>)
.MakeGenericType(query.GetType(), typeof(TResult));
dynamic handler = container.GetInstance(handlerType);
return handler.Handle((dynamic)query);
}
}
La QueryProcessor
classe costruisce un IQueryHandler<TQuery, TResult>
tipo specifico in base al tipo dell'istanza di query fornita. Questo tipo viene utilizzato per chiedere alla classe contenitore fornita di ottenere un'istanza di quel tipo. Sfortunatamente dobbiamo chiamare il Handle
metodo usando la reflection (usando la parola chiave dymamic C # 4.0 in questo caso), perché a questo punto è impossibile eseguire il cast dell'istanza del gestore, poiché l' TQuery
argomento generico non è disponibile in fase di compilazione. Tuttavia, a meno che il Handle
metodo non venga rinominato o ottenga altri argomenti, questa chiamata non fallirà mai e, se lo desideri, è molto facile scrivere uno unit test per questa classe. L'uso della riflessione darà un leggero calo, ma non è nulla di cui preoccuparsi.
Per rispondere a una delle tue preoccupazioni:
Quindi sto cercando alternative che incapsulino l'intera query, ma comunque abbastanza flessibili da non scambiare solo i repository di spaghetti con un'esplosione di classi di comando.
Una conseguenza dell'utilizzo di questo design è che ci saranno molte classi piccole nel sistema, ma avere molte classi piccole / focalizzate (con nomi chiari) è una buona cosa. Questo approccio è chiaramente molto migliore di avere molti overload con parametri diversi per lo stesso metodo in un repository, poiché è possibile raggrupparli in una classe di query. Quindi ottieni ancora molte meno classi di query rispetto ai metodi in un repository.