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 TResult
typu 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 User
obiektó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 IQueryHandler
interfejsie:
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 IQueryHandlers
w naszym kodzie. Kiedy zamiast tego zmienimy FindUsersBySearchTextQuery
zwracaną wartość UserInfo[]
(przez implementację IQuery<UserInfo[]>
), UserController
kompilacja nie powiedzie się, ponieważ ograniczenie typu ogólnego na IQueryHandler<TQuery, TResult>
nie będzie mogło zostać zmapowane FindUsersBySearchTextQuery
do User[]
.
Wstrzyknięcie IQueryHandler
interfejsu 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 IQueryHandlers
z 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 IQueryProcessor
to nieogólny interfejs z jedną ogólną metodą. Jak widać w definicji interfejsu, IQueryProcessor
zależ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, UserController
aby 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);
}
}
UserController
Teraz zależy na zasadzie IQueryProcessor
, że może obsługiwać wszystkie nasze pytania. W UserController
„S SearchUsers
sposób wywołuje IQueryProcessor.Process
sposób przechodzi w zainicjowany przedmiotem zapytania. Ponieważ FindUsersBySearchTextQuery
implementuje 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. Process
Znany jest również zwracany typ metody.
Obecnie obowiązkiem wdrożenia prawa IQueryProcessor
jest 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);
}
}
QueryProcessor
Klasa 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ć Handle
metodę 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ż TQuery
argument generyczny nie jest dostępny w czasie kompilacji. Jednak jeśli Handle
nazwa 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.