Zastrzeżenie: Ponieważ nie ma jeszcze żadnych świetnych odpowiedzi, zdecydowałem się opublikować fragment świetnego posta na blogu, który przeczytałem jakiś czas temu, skopiowany prawie dosłownie. Możesz znaleźć pełny post na blogu tutaj . Więc oto jest:
Możemy zdefiniować dwa następujące interfejsy:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
W IQuery<TResult>Określa komunikat, który definiuje konkretne zapytanie z danymi to powraca użyciu TResulttypu rodzajowego. Za pomocą wcześniej zdefiniowanego interfejsu możemy zdefiniować komunikat zapytania w następujący sposób:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
Ta klasa definiuje operację zapytania z dwoma parametrami, której wynikiem będzie tablica Userobiektów. Klasę obsługującą tę wiadomość można zdefiniować w następujący sposób:
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();
}
}
Teraz możemy pozwolić konsumentom polegać na ogólnym IQueryHandlerinterfejsie:
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);
}
}
Od razu ten model daje nam dużą elastyczność, ponieważ możemy teraz zdecydować, co wstrzyknąć do UserController. Możemy wstrzyknąć zupełnie inną implementację lub taką, która otacza rzeczywistą implementację, bez konieczności wprowadzania zmian w UserController(i wszystkich innych użytkownikach tego interfejsu).
IQuery<TResult>Interfejs daje nam czas kompilacji wsparcia przy określaniu lub wstrzyknięcie IQueryHandlersw naszym kodzie. Kiedy zamiast tego zmienimy FindUsersBySearchTextQueryzwracaną wartość UserInfo[](przez implementację IQuery<UserInfo[]>), UserControllerkompilacja nie powiedzie się, ponieważ ograniczenie typu ogólnego na IQueryHandler<TQuery, TResult>nie będzie mogło zostać zmapowane FindUsersBySearchTextQuerydo User[].
Wstrzyknięcie IQueryHandlerinterfejsu do konsumenta wiąże się jednak z pewnymi mniej oczywistymi problemami, które nadal wymagają rozwiązania. Liczba zależności naszych konsumentów może być zbyt duża i może prowadzić do przeciążenia konstruktora - gdy konstruktor przyjmuje zbyt wiele argumentów. Liczba zapytań wykonywanych przez klasę może się często zmieniać, co wymagałoby ciągłych zmian w liczbie argumentów konstruktora.
Możemy rozwiązać problem polegający na tym, że zbyt wiele IQueryHandlersz nich musimy wprowadzić dodatkową warstwę abstrakcji. Tworzymy mediatora, który znajduje się między konsumentami a obsługą zapytań:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
Jest IQueryProcessorto nieogólny interfejs z jedną ogólną metodą. Jak widać w definicji interfejsu, IQueryProcessorzależy to od IQuery<TResult>interfejsu. Dzięki temu możemy mieć obsługę czasu kompilacji u naszych konsumentów, która jest zależna od IQueryProcessor. Przepiszmy, UserControlleraby używał nowego 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
};
User[] users = this.queryProcessor.Process(query);
return this.View(users);
}
}
UserControllerTeraz zależy na zasadzie IQueryProcessor, że może obsługiwać wszystkie nasze pytania. W UserController„S SearchUserssposób wywołuje IQueryProcessor.Processsposób przechodzi w zainicjowany przedmiotem zapytania. Ponieważ FindUsersBySearchTextQueryimplementuje IQuery<User[]>interfejs, możemy przekazać go do Execute<TResult>(IQuery<TResult> query)metody generycznej . Dzięki wnioskowaniu o typie C # kompilator jest w stanie określić typ ogólny, co oszczędza nam konieczności jawnego określania typu. ProcessZnany jest również zwracany typ metody.
Obecnie obowiązkiem wdrożenia prawa IQueryProcessorjest znalezienie odpowiedniego IQueryHandler. Wymaga to pewnego dynamicznego pisania i opcjonalnie użycia struktury Dependency Injection, a wszystko to można zrobić za pomocą zaledwie kilku wierszy kodu:
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);
}
}
QueryProcessorKlasa tworzy specyficzny IQueryHandler<TQuery, TResult>rodzaj na podstawie typu dostarczonego przykład zapytania. Ten typ służy do żądania od podanej klasy kontenera pobrania wystąpienia tego typu. Niestety musimy wywołać Handlemetodę z użyciem refleksji (w tym przypadku używając słowa kluczowego dymamic C # 4.0), ponieważ w tym momencie nie jest możliwe rzutowanie instancji handlera, ponieważ TQueryargument generyczny nie jest dostępny w czasie kompilacji. Jednak jeśli Handlenazwa metody nie zostanie zmieniona lub nie otrzyma innych argumentów, to wywołanie nigdy się nie powiedzie, a jeśli chcesz, bardzo łatwo jest napisać test jednostkowy dla tej klasy. Korzystanie z odbicia może nieco spaść, ale nie ma się czym martwić.
Aby odpowiedzieć na jedno z twoich obaw:
Dlatego szukam alternatyw, które obejmują całe zapytanie, ale są na tyle elastyczne, że nie wystarczy zamienić repozytoria spaghetti na eksplozję klas poleceń.
Konsekwencją korzystania z tego projektu jest to, że w systemie będzie wiele małych klas, ale posiadanie wielu małych / skupionych klas (z wyraźnymi nazwami) jest dobrą rzeczą. Takie podejście jest zdecydowanie lepsze niż posiadanie wielu przeciążeń z różnymi parametrami dla tej samej metody w repozytorium, ponieważ można je pogrupować w jednej klasie zapytania. Więc nadal masz dużo mniej klas zapytań niż metod w repozytorium.