Haftungsausschluss: Da es noch keine großartigen Antworten gibt, habe ich beschlossen, einen Teil eines großartigen Blogposts zu veröffentlichen, den ich vor einiger Zeit gelesen und fast wörtlich kopiert habe. Den vollständigen Blogbeitrag finden Sie hier . Hier ist es also:
Wir können die folgenden zwei Schnittstellen definieren:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
Das IQuery<TResult>
gibt eine Nachricht an, die eine bestimmte Abfrage mit den zurückgegebenen Daten unter Verwendung des TResult
generischen Typs definiert. Mit der zuvor definierten Schnittstelle können wir eine Abfragenachricht wie folgt definieren:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
Diese Klasse definiert eine Abfrageoperation mit zwei Parametern, die zu einem Array von User
Objekten führt. Die Klasse, die diese Nachricht verarbeitet, kann wie folgt definiert werden:
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();
}
}
Wir können jetzt die Verbraucher von der generischen IQueryHandler
Schnittstelle abhängig machen :
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);
}
}
Dieses Modell bietet uns sofort viel Flexibilität, da wir jetzt entscheiden können, was in das Modell injiziert werden soll UserController
. Wir können eine völlig andere Implementierung einfügen oder eine, die die eigentliche Implementierung umschließt, ohne Änderungen an der UserController
(und allen anderen Verbrauchern dieser Schnittstelle) vornehmen zu müssen .
Die IQuery<TResult>
Schnittstelle bietet uns Unterstützung bei der Kompilierung beim Angeben oder Einfügen IQueryHandlers
unseres Codes. Wenn wir das ändern FindUsersBySearchTextQuery
zurückzukehren UserInfo[]
statt (durch die Implementierung IQuery<UserInfo[]>
), das UserController
wird nicht kompilieren, da die generische Typ Einschränkung für IQueryHandler<TQuery, TResult>
nicht in der Lage sein , kartieren FindUsersBySearchTextQuery
zu User[]
.
Das Injizieren der IQueryHandler
Schnittstelle in einen Verbraucher weist jedoch einige weniger offensichtliche Probleme auf, die noch angegangen werden müssen. Die Anzahl der Abhängigkeiten unserer Verbraucher kann zu groß werden und zu einer Überinjektion des Konstruktors führen - wenn ein Konstruktor zu viele Argumente verwendet. Die Anzahl der Abfragen, die eine Klasse ausführt, kann sich häufig ändern, was ständige Änderungen der Anzahl der Konstruktorargumente erforderlich machen würde.
Wir können das Problem beheben, dass zu viele IQueryHandlers
mit einer zusätzlichen Abstraktionsebene injiziert werden müssen . Wir erstellen einen Mediator, der zwischen den Verbrauchern und den Abfragehandlern sitzt:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
Dies IQueryProcessor
ist eine nicht generische Schnittstelle mit einer generischen Methode. Wie Sie in der Schnittstellendefinition sehen können, IQueryProcessor
hängt dies von der IQuery<TResult>
Schnittstelle ab. Dies ermöglicht uns eine Unterstützung bei der Kompilierungszeit bei unseren Verbrauchern, die von der abhängig ist IQueryProcessor
. Lassen Sie uns das umschreiben UserController
, um das Neue zu verwenden 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);
}
}
Das UserController
hängt jetzt von einem ab IQueryProcessor
, der alle unsere Anfragen bearbeiten kann. Die Methode UserController
's SearchUsers
ruft die IQueryProcessor.Process
Methode auf, die ein initialisiertes Abfrageobjekt übergibt. Da das FindUsersBySearchTextQuery
die IQuery<User[]>
Schnittstelle implementiert , können wir es an die generische Execute<TResult>(IQuery<TResult> query)
Methode übergeben. Dank der C # -Typinferenz kann der Compiler den generischen Typ bestimmen, sodass wir den Typ nicht explizit angeben müssen. Der Rückgabetyp der Process
Methode ist ebenfalls bekannt.
Es liegt nun in der Verantwortung der Umsetzung des IQueryProcessor
, das Richtige zu finden IQueryHandler
. Dies erfordert eine dynamische Typisierung und optional die Verwendung eines Dependency Injection-Frameworks und kann mit nur wenigen Codezeilen durchgeführt werden:
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);
}
}
Die QueryProcessor
Klasse erstellt einen bestimmten IQueryHandler<TQuery, TResult>
Typ basierend auf dem Typ der angegebenen Abfrageinstanz. Dieser Typ wird verwendet, um die angegebene Containerklasse aufzufordern, eine Instanz dieses Typs abzurufen. Leider müssen wir die Handle
Methode mit Reflection aufrufen (in diesem Fall mit dem Schlüsselwort C # 4.0 dymamic), da es zu diesem Zeitpunkt unmöglich ist, die Handlerinstanz zu konvertieren, da das generische TQuery
Argument zur Kompilierungszeit nicht verfügbar ist. Sofern die Handle
Methode nicht umbenannt wird oder andere Argumente erhält, schlägt dieser Aufruf niemals fehl. Wenn Sie möchten, ist es sehr einfach, einen Komponententest für diese Klasse zu schreiben. Die Verwendung von Reflexion führt zu einem leichten Abfall, ist jedoch kein Grund zur Sorge.
Um eines Ihrer Anliegen zu beantworten:
Daher suche ich nach Alternativen, die die gesamte Abfrage kapseln, aber dennoch flexibel genug sind, um nicht nur Spaghetti-Repositorys gegen eine Explosion von Befehlsklassen auszutauschen.
Eine Konsequenz der Verwendung dieses Entwurfs ist, dass es viele kleine Klassen im System gibt, aber es ist eine gute Sache, viele kleine / fokussierte Klassen (mit klaren Namen) zu haben. Dieser Ansatz ist eindeutig viel besser als viele Überladungen mit unterschiedlichen Parametern für dieselbe Methode in einem Repository, da Sie diese in einer Abfrageklasse gruppieren können. Sie erhalten also immer noch viel weniger Abfrageklassen als Methoden in einem Repository.