면책 조항 : 아직 훌륭한 답변이 없기 때문에 얼마 전에 읽은 훌륭한 블로그 게시물의 일부를 거의 그대로 복사하기로 결정했습니다. 여기 에서 전체 블로그 게시물을 찾을 수 있습니다 . 그래서 여기 있습니다 :
다음 두 가지 인터페이스를 정의 할 수 있습니다.
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
IQuery<TResult>
지정는 사용하여 반환하는 데이터를 특정 쿼리를 정의하는 메시지 TResult
일반적인 유형입니다. 이전에 정의 된 인터페이스를 사용하여 다음과 같은 쿼리 메시지를 정의 할 수 있습니다.
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
이 클래스는 두 개의 매개 변수를 사용하여 쿼리 작업을 정의하며 결과적으로 User
개체 배열이 생성 됩니다. 이 메시지를 처리하는 클래스는 다음과 같이 정의 할 수 있습니다.
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();
}
}
이제 소비자가 일반 IQueryHandler
인터페이스에 의존하도록 할 수 있습니다 .
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);
}
}
즉시이 모델은 우리에게 많은 유연성을 제공합니다 UserController
.. 완전히 다른 구현 또는 실제 구현을 래핑하는 구현을 삽입 할 수 있습니다.UserController
해당 인터페이스의 다른 모든 소비자를 있습니다.
IQuery<TResult>
인터페이스를 지정하거나 주입 할 때 우리가 지원 컴파일시 제공 IQueryHandlers
우리의 코드. 우리는을 변경하면 FindUsersBySearchTextQuery
반환 UserInfo[]
(구현하는 대신 IQuery<UserInfo[]>
)의은 UserController
에 제네릭 형식 제약 조건이 있기 때문에, 컴파일 할 수 없게됩니다 IQueryHandler<TQuery, TResult>
매핑 할 수 없습니다 FindUsersBySearchTextQuery
에User[]
.
주입 IQueryHandler
그러나 소비자에게 인터페이스를 것은 여전히 해결해야 할 몇 가지 덜 분명한 문제를 가지고 있습니다. 소비자의 종속성 수가 너무 커져 생성자가 너무 많은 인수를 사용하는 경우 생성자 과잉 주입으로 이어질 수 있습니다. 클래스가 실행하는 쿼리 수는 자주 변경 될 수 있으므로 생성자 인수 수를 지속적으로 변경해야합니다.
IQueryHandlers
추가 추상화 계층으로 너무 많이 주입해야하는 문제를 해결할 수 있습니다 . 소비자와 쿼리 처리기 사이에있는 중재자를 만듭니다.
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
은 IQueryProcessor
하나 개의 일반적인 방법이 아닌 일반적인 인터페이스입니다. 인터페이스 정의에서 볼 수 있듯이 인터페이스에 IQueryProcessor
따라 다릅니다 IQuery<TResult>
. 이를 통해 .NET Framework에 의존하는 소비자에서 컴파일 시간을 지원할 수 있습니다 IQueryProcessor
. UserController
new를 사용 하도록를 다시 작성해 보겠습니다 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
지금에 따라 IQueryProcessor
우리의 모든 쿼리를 처리 할 수. UserController
의 SearchUsers
방법은 호출 IQueryProcessor.Process
초기화 된 질의 객체를 전달 방법. 인터페이스를 FindUsersBySearchTextQuery
구현 하므로 IQuery<User[]>
일반 Execute<TResult>(IQuery<TResult> query)
메서드에 전달할 수 있습니다 . C # 유형 추론 덕분에 컴파일러는 제네릭 유형을 결정할 수 있으므로 명시 적으로 유형을 명시 할 필요가 없습니다. 반환 유형Process
메서드 도 알려져 있습니다.
이제의 구현 IQueryProcessor
에서 권리를 찾을 책임이 IQueryHandler
있습니다. 이를 위해서는 동적 타이핑과 선택적으로 의존성 주입 프레임 워크의 사용이 필요하며 몇 줄의 코드로 모두 수행 할 수 있습니다.
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
클래스는 특정 구조 IQueryHandler<TQuery, TResult>
제공된 조회 인스턴스의 유형에 따라 유형입니다. 이 유형은 제공된 컨테이너 클래스에 해당 유형의 인스턴스를 가져 오도록 요청하는 데 사용됩니다. 안타깝게도 Handle
리플렉션을 사용하여 메서드 를 호출해야합니다 (이 경우 C # 4.0 동적 키워드 TQuery
사용). 컴파일 타임에 제네릭 인수를 사용할 수 없기 때문에이 시점에서 핸들러 인스턴스를 캐스팅 할 수 없기 때문 입니다. 그러나 Handle
메서드의 이름을 바꾸거나 다른 인수를 가져 오지 않는 한이 호출은 실패하지 않으며 원하는 경우이 클래스에 대한 단위 테스트를 작성하는 것이 매우 쉽습니다. 리플렉션을 사용하면 약간 떨어질 수 있지만 실제로 걱정할 필요는 없습니다.
우려 사항 중 하나에 답변하려면 :
그래서 저는 전체 쿼리를 캡슐화하는 대안을 찾고 있지만, 명령 클래스의 폭발적인 증가를 위해 스파게티 리포지토리를 교체 할 수있을만큼 충분히 유연합니다.
이 디자인을 사용한 결과는 시스템에 작은 클래스가 많이있을 것이지만 작은 / 집중된 클래스 (명확한 이름 포함)를 많이 갖는 것은 좋은 것입니다. 이 접근 방식은 하나의 쿼리 클래스로 그룹화 할 수 있으므로 저장소의 동일한 메서드에 대해 다른 매개 변수가있는 많은 오버로드를 갖는 것보다 훨씬 낫습니다. 따라서 여전히 저장소의 메서드보다 쿼리 클래스가 훨씬 적습니다.