Avertissement: comme il n'y a pas toutes les réponses grands encore, j'ai décidé de poster une partie d'un grand blog, j'ai lu il y a longtemps, repris presque mot pour mot. Vous pouvez trouver l'article complet ici. Il est donc ici:
On peut définir les deux interfaces suivantes:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
L' IQuery<TResult>
indique un message qui définit une requête spécifique avec les données qu'il retourne à l'aide de l' TResult
de type générique. Avec le défini précédemment interface, nous pouvons définir une requête de message comme ceci:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
Cette classe définit une opération de requête avec deux paramètres, ce qui aura pour résultat un tableau d' User
objets. La classe qui gère ce message peut être défini comme suit:
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();
}
}
Nous pouvons maintenant laisser les consommateurs dépendent du générique IQueryHandler
interface:
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);
}
}
Immédiatement ce modèle nous donne une grande flexibilité, car on peut maintenant décider de ce à injecter dans l' UserController
. Nous pouvons injecter un de complètement différent de la mise en œuvre, ou qui encapsule la mise en œuvre réelle, sans avoir à apporter des modifications à l' UserController
(et tous les autres consommateurs de cette interface).
L' IQuery<TResult>
interface nous donne au moment de la compilation de soutien lors de la spécification ou l'injection d' IQueryHandlers
dans notre code. Lors du changement de la FindUsersBySearchTextQuery
de revenir UserInfo[]
à la place (par la mise en œuvre de IQuery<UserInfo[]>
), l' UserController
échoue à compiler, depuis le générique de type contrainte sur IQueryHandler<TQuery, TResult>
de ne pas être en mesure de cartographier FindUsersBySearchTextQuery
de User[]
.
L'injection de l' IQueryHandler
interface à un consommateur, cependant, a certains moins évidents problèmes qui doivent encore être traités. Le nombre de dépendances de nos consommateurs pourraient devenir trop grand et peut conduire à des constructeur sur-injection - lorsqu'un constructeur prend trop d'arguments. Le nombre de requêtes à une classe exécute peuvent changer fréquemment, ce qui nécessiterait des changements constants dans le nombre d'arguments du constructeur.
Nous pouvons résoudre le problème d'avoir à injecter trop d' IQueryHandlers
avec une couche d'abstraction supplémentaire. Nous avons créer un médiateur qui se trouve entre les consommateurs et la demande des gestionnaires:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
L' IQueryProcessor
est un non-interface générique, avec une méthode générique. Comme vous pouvez le voir dans la définition de l'interface, l' IQueryProcessor
dépend de l' IQuery<TResult>
interface. Cela nous permet d'avoir le temps de compilation de l'aide dans notre consommateurs qui dépendent de l' IQueryProcessor
. Réécrivons l' UserController
à utiliser le nouveau 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
maintenant dépend de l' IQueryProcessor
qui peut s'occuper de nos requêtes. L' UserController
s' SearchUsers
des appels à la méthode de l' IQueryProcessor.Process
méthode de passage dans une initialisé objet de requête. Depuis l' FindUsersBySearchTextQuery
implémente l' IQuery<User[]>
interface, nous pouvons passer à la générique Execute<TResult>(IQuery<TResult> query)
méthode. Merci à C# inférence de type, le compilateur est capable de déterminer le type générique et cela nous évite d'avoir à déclarer explicitement le type. Le type de retour de la Process
méthode est également connue.
Il est désormais de la responsabilité de la mise en œuvre de l' IQueryProcessor
à trouver le bon IQueryHandler
. Cela demande un peu de typage dynamique, et éventuellement l'utilisation d'un framework Injection de Dépendance, et peut être fait avec seulement quelques lignes de code:
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);
}
}
L' QueryProcessor
classe de constructions spécifiques IQueryHandler<TQuery, TResult>
type en fonction du type de la requête de l'instance. Ce type est utilisé pour demander le récipient fourni de classe pour obtenir une instance de ce type. Malheureusement, nous devons appeler l' Handle
méthode à l'aide de la réflexion (en utilisant le C# 4.0 dymamic mot-clé dans ce cas), car à ce stade, il est impossible de lancer le gestionnaire d'exemple, depuis le générique de l' TQuery
argument n'est pas disponible au moment de la compilation. Cependant, à moins que l' Handle
méthode est renommé ou d'autres arguments, cet appel ne manquera jamais, et si vous le souhaitez, il est très facile d'écrire un test unitaire pour cette classe. L'utilisation de la réflexion donnera une légère baisse, mais rien de vraiment s'inquiéter.
La réponse à l'une de vos préoccupations:
Je suis donc à la recherche de solutions qui intègrent l'ensemble de la requête, mais
encore assez souple que vous n'êtes pas juste en remplaçant spaghetti
Référentiels pour une explosion de commande des classes.
Une conséquence de cette conception est qu'il y aura beaucoup de petites classes dans le système, mais le fait d'avoir beaucoup de petites/concentré classes (avec des noms clairs) est une bonne chose. Cette approche est clairement beaucoup mieux que d'avoir de nombreuses surcharges avec des paramètres différents pour la même méthode dans un référentiel, comme vous pouvez le groupe de personnes dans une classe de requête. Donc, vous avez encore beaucoup moins classes de requêtes que les méthodes dans un référentiel.