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 TResulttipo 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 Useroggetti. 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 IQueryHandlerdall'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 IQueryHandlersil codice. Quando cambiamo invece il FindUsersBySearchTextQueryto return UserInfo[](implementandolo IQuery<UserInfo[]>), la UserControllercompilazione fallirà, poiché il vincolo di tipo generico su IQueryHandler<TQuery, TResult>non sarà in grado di mappare FindUsersBySearchTextQuerya User[].
IQueryHandlerTuttavia, 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 IQueryHandlerscon 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, IQueryProcessordipende IQuery<TResult>dall'interfaccia. Questo ci consente di avere supporto in fase di compilazione nei nostri consumatori che dipendono da IQueryProcessor. Riscriviamo il UserControllerper 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' UserControlleradesso dipende da un in IQueryProcessorgrado di gestire tutte le nostre domande. Il UserController's SearchUsersmetodo chiama il IQueryProcessor.Processmetodo passando un oggetto query inizializzata. Poiché FindUsersBySearchTextQueryimplementa 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. È Processnoto anche il tipo restituito del metodo.
È ora responsabilità dell'attuazione del IQueryProcessortrovare 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 QueryProcessorclasse 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 Handlemetodo 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' TQueryargomento generico non è disponibile in fase di compilazione. Tuttavia, a meno che il Handlemetodo 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.