Tuyên bố từ chối trách nhiệm: Vì chưa có bất kỳ câu trả lời tuyệt vời nào, tôi quyết định đăng một phần từ một bài đăng blog tuyệt vời mà tôi đã đọc cách đây một thời gian, được sao chép gần như nguyên văn. Bạn có thể tìm thấy toàn bộ bài đăng trên blog ở đây . Vì vậy, đây là:
Chúng ta có thể xác định hai giao diện sau:
public interface IQuery<TResult>
{
}
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
TResult Handle(TQuery query);
}
Chỉ IQuery<TResult>
định một thông báo xác định một truy vấn cụ thể với dữ liệu mà nó trả về bằng cách sử dụng TResult
kiểu chung. Với giao diện đã xác định trước đó, chúng ta có thể xác định một thông báo truy vấn như sau:
public class FindUsersBySearchTextQuery : IQuery<User[]>
{
public string SearchText { get; set; }
public bool IncludeInactiveUsers { get; set; }
}
Lớp này định nghĩa một thao tác truy vấn với hai tham số, điều này sẽ dẫn đến một mảng các User
đối tượng. Lớp xử lý thông báo này có thể được định nghĩa như sau:
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();
}
}
Giờ đây, chúng tôi có thể để người tiêu dùng phụ thuộc vào IQueryHandler
giao diện chung :
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);
}
}
Ngay lập tức, mô hình này cung cấp cho chúng tôi rất nhiều sự linh hoạt, bởi vì bây giờ chúng tôi có thể quyết định những gì sẽ tiêm vào UserController
. Chúng tôi có thể đưa vào một triển khai hoàn toàn khác hoặc một triển khai kết thúc triển khai thực sự mà không cần phải thực hiện thay đổi đối với UserController
(và tất cả những người tiêu dùng khác của giao diện đó).
Các IQuery<TResult>
giao diện cho chúng ta thời gian biên dịch hỗ trợ khi xác định hoặc tiêm IQueryHandlers
trong mã của chúng tôi. Khi chúng tôi thay đổi FindUsersBySearchTextQuery
thành trả về UserInfo[]
thay thế (bằng cách triển khai IQuery<UserInfo[]>
), UserController
sẽ không biên dịch được, vì ràng buộc kiểu chung trên IQueryHandler<TQuery, TResult>
sẽ không thể ánh xạ FindUsersBySearchTextQuery
tới User[]
.
IQueryHandler
Tuy nhiên, việc đưa giao diện vào người tiêu dùng có một số vấn đề ít rõ ràng hơn vẫn cần được giải quyết. Số lượng phụ thuộc của người tiêu dùng của chúng ta có thể quá lớn và có thể dẫn đến việc chèn quá nhiều hàm tạo - khi một hàm tạo nhận quá nhiều đối số. Số lượng truy vấn mà một lớp thực thi có thể thay đổi thường xuyên, điều này sẽ yêu cầu thay đổi liên tục đối với số lượng đối số của phương thức khởi tạo.
Chúng tôi có thể khắc phục vấn đề phải tiêm quá nhiều IQueryHandlers
với một lớp trừu tượng bổ sung. Chúng tôi tạo một người hòa giải nằm giữa người tiêu dùng và người xử lý truy vấn:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
Đây IQueryProcessor
là một giao diện không chung chung với một phương pháp chung. Như bạn có thể thấy trong định nghĩa giao diện, IQueryProcessor
tùy thuộc vào IQuery<TResult>
giao diện. Điều này cho phép chúng tôi hỗ trợ thời gian biên dịch trong những người tiêu dùng phụ thuộc vào IQueryProcessor
. Hãy viết lại UserController
để sử dụng mới 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);
}
}
Hiện UserController
tại phụ thuộc vào a IQueryProcessor
có thể xử lý tất cả các truy vấn của chúng tôi. Các UserController
's SearchUsers
phương pháp gọi là IQueryProcessor.Process
phương pháp đi qua trong một đối tượng truy vấn khởi tạo. Vì giao diện FindUsersBySearchTextQuery
triển khai IQuery<User[]>
, chúng ta có thể chuyển nó sang Execute<TResult>(IQuery<TResult> query)
phương thức chung . Nhờ suy luận kiểu C #, trình biên dịch có thể xác định kiểu chung và điều này giúp chúng ta tiết kiệm được kiểu phải khai báo rõ ràng. Kiểu trả về của Process
phương thức cũng được biết đến.
Bây giờ nó là trách nhiệm của việc thực hiện IQueryProcessor
để tìm ra quyền IQueryHandler
. Điều này yêu cầu một số thao tác nhập động và tùy chọn sử dụng khung Phụ thuộc Injection và tất cả có thể được thực hiện chỉ với một vài dòng mã:
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);
}
}
Các QueryProcessor
lớp xây dựng một cụ thể IQueryHandler<TQuery, TResult>
loại dựa trên các loại của các ví dụ truy vấn đã cung cấp. Kiểu này được sử dụng để yêu cầu lớp vùng chứa được cung cấp lấy một thể hiện của kiểu đó. Thật không may, chúng ta cần gọi Handle
phương thức bằng cách sử dụng phản chiếu (bằng cách sử dụng từ khóa dymamic C # 4.0 trong trường hợp này), bởi vì tại thời điểm này, không thể ép kiểu trình xử lý, vì TQuery
đối số chung không có sẵn tại thời điểm biên dịch. Tuy nhiên, trừ khi Handle
phương thức được đổi tên hoặc nhận các đối số khác, cuộc gọi này sẽ không bao giờ thất bại và nếu bạn muốn, rất dễ dàng để viết một bài kiểm tra đơn vị cho lớp này. Sử dụng phản chiếu sẽ giảm nhẹ, nhưng không có gì đáng lo ngại.
Để trả lời một trong những mối quan tâm của bạn:
Vì vậy, tôi đang tìm kiếm các giải pháp thay thế gói gọn toàn bộ truy vấn, nhưng vẫn đủ linh hoạt để bạn không chỉ hoán đổi Kho chứa spaghetti cho sự bùng nổ của các lớp lệnh.
Hệ quả của việc sử dụng thiết kế này là sẽ có rất nhiều lớp nhỏ trong hệ thống, nhưng có rất nhiều lớp nhỏ / tập trung (có tên rõ ràng) là một điều tốt. Cách tiếp cận này rõ ràng là tốt hơn nhiều khi có nhiều quá tải với các tham số khác nhau cho cùng một phương thức trong một kho lưu trữ, vì bạn có thể nhóm chúng trong một lớp truy vấn. Vì vậy, bạn vẫn nhận được ít lớp truy vấn hơn nhiều so với các phương thức trong kho lưu trữ.