Một số vấn đề rất rõ ràng với ý tưởng của một generic IRepository<T>
:
Nó giả định rằng tất cả các thực thể sử dụng cùng một loại chìa khóa, đó là không đúng sự thật trong hầu hết các hệ thống không tầm thường. Một số thực thể sẽ sử dụng GUID, những người khác có thể có một số loại khóa tự nhiên và/hoặc tổng hợp. NHibernate có thể hỗ trợ điều này khá tốt nhưng LINQ to SQL là khá xấu ở đó - bạn phải viết một thỏa thuận tốt về mã hackish để làm ánh xạ khóa tự động.
Điều này có nghĩa là mỗi kho lưu trữ chỉ có thể xử lý chính xác một loại thực thể và chỉ hỗ trợ các hoạt động tầm thường nhất. Khi một kho lưu trữ được chuyển xuống một trình bao bọc CRUD đơn giản, nó có rất ít sử dụng. Bạn cũng có thể giao khách hàng theo số IQueryable<T>
hoặc Table<T>
.
Giả định rằng bạn thực hiện chính xác các thao tác giống nhau trên mọi thực thể. Trong thực tế, điều này sẽ rất xa sự thật. Chắc chắn, có thể là bạn muốn nhận được rằng Order
theo ID của nó, nhưng nhiều khả năng bạn muốn nhận được danh sách các đối tượng Order
cho một khách hàng cụ thể và trong một số phạm vi ngày. Khái niệm về một hoàn toàn chung chung IRepository<T>
không cho phép thực tế rằng bạn gần như chắc chắn muốn thực hiện các loại truy vấn khác nhau trên các loại thực thể khác nhau.
Toàn bộ vấn đề của mô hình kho là để tạo ra một trừu tượng trên mô hình truy cập dữ liệu thông thường. Tôi nghĩ một số lập trình viên cảm thấy buồn chán với việc tạo kho để họ nói "Này, tôi biết, tôi sẽ tạo một kho lưu trữ über có thể xử lý bất kỳ loại thực thể nào!" Mà là tuyệt vời ngoại trừ việc kho lưu trữ là khá nhiều useless cho 80% những gì bạn đang cố gắng để làm. Nó là tốt như là một lớp cơ sở/giao diện, nhưng nếu đó là mức độ đầy đủ của công việc mà bạn làm thì bạn chỉ là lười biếng (và đảm bảo đau đầu trong tương lai).
Lý tưởng nhất là tôi có thể bắt đầu với một kho lưu trữ chung mà trông giống như sau:
public interface IRepository<TKey, TEntity>
{
TEntity Get(TKey id);
void Save(TEntity entity);
}
Bạn sẽ nhận thấy rằng này không có một List
hoặc GetAll
chức năng - đó là bởi vì nó vô lý để nghĩ rằng có thể chấp nhận dữ liệu từ toàn bộ bảng cùng một lúc ở bất kỳ đâu trong mã. Đây là thời điểm bạn cần bắt đầu đi vào các kho lưu trữ cụ thể:
public interface IOrderRepository : IRepository<int, Order>
{
IEnumerable<Order> GetOrdersByCustomer(Guid customerID);
IPager<Order> GetOrdersByDate(DateTime fromDate, DateTime toDate);
IPager<Order> GetOrdersByProduct(int productID);
}
Và cứ như vậy - bạn sẽ có ý tưởng. Bằng cách này, chúng ta có kho lưu trữ "chung chung" nếu chúng ta thực sự cần các ngữ nghĩa truy xuất đơn giản vô cùng đơn giản, nhưng nói chung chúng ta không bao giờ thực sự truyền nó, chắc chắn không phải là một lớp điều khiển.
Còn với bộ điều khiển, bạn phải làm điều này đúng, nếu không bạn đã phủ nhận tất cả công việc bạn vừa làm khi gộp tất cả các kho.
Bộ điều khiển cần phải lấy kho lưu trữ từ thế giới bên ngoài. Lý do bạn tạo các kho lưu trữ này là do đó bạn có thể thực hiện một số loại Inversion of Control. Mục tiêu cuối cùng của bạn ở đây là có thể hoán đổi một kho lưu trữ cho một kho lưu trữ khác - ví dụ, để kiểm tra đơn vị, hoặc nếu bạn quyết định chuyển từ Linq sang SQL sang Entity Framework tại một thời điểm nào đó trong tương lai.
Một ví dụ về nguyên tắc này là:
public class OrderController : Controller
{
public OrderController(IOrderRepository orderRepository)
{
if (orderRepository == null)
throw new ArgumentNullException("orderRepository");
this.OrderRepository = orderRepository;
}
public ActionResult List(DateTime fromDate, DateTime toDate) { ... }
// More actions
public IOrderRepository OrderRepository { get; set; }
}
Nói cách khác Controller có không biết làm thế nào để tạo ra một kho lưu trữ, và cũng không nên nó. Nếu bạn có bất kỳ kho lưu trữ-xây dựng đang diễn ra trong đó, nó tạo ra khớp nối mà bạn thực sự không muốn. Lý do mà các bộ điều khiển mẫu ASP.NET MVC có các hàm tạo tham số tạo ra các kho lưu trữ cụ thể là các trang web cần có khả năng biên dịch và chạy mà không buộc bạn phải thiết lập toàn bộ khuôn khổ Dependency Injection. Tuy nhiên, trong một trang web sản xuất, nếu bạn không đi qua sự phụ thuộc kho thông qua một nhà xây dựng hoặc tài sản công cộng, thì bạn lãng phí thời gian của bạn có kho, vì các bộ điều khiển vẫn được kết hợp chặt chẽ với tầng cơ sở dữ liệu . Bạn cần để có thể viết mã kiểm tra như thế này:
[TestMethod]
public void Can_add_order()
{
OrderController controller = new OrderController();
FakeOrderRepository fakeRepository = new FakeOrderRepository();
controller.OrderRepository = fakeRepository; //<-- Important!
controller.SubmitOrder(...);
Assert.That(fakeRepository.ContainsOrder(...));
}
Bạn không thể làm điều này nếu bạn OrderController
đang xảy ra và tạo kho lưu trữ riêng của mình. Phương pháp kiểm tra này không được phép thực hiện bất kỳ truy cập dữ liệu nào, nó chỉ đảm bảo rằng trình điều khiển đang gọi phương thức kho lưu trữ đúng dựa trên hành động.
Đây không phải là DI, nhưng bạn hãy nhớ rằng đây chỉ là giả mạo/chế nhạo. Trường hợp DI đi vào hình ảnh là khi bạn quyết định rằng LINQ to SQL không làm đủ cho bạn và bạn thực sự muốn HQL trong NHibernate, nhưng nó sẽ đưa bạn 3 tháng để cổng tất cả mọi thứ trên, và bạn muốn để có thể làm một kho lưu trữ này tại một thời điểm. Vì vậy, ví dụ, sử dụng một khung DI như Ninject, tất cả các bạn phải làm là thay đổi điều này:
Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<LinqToSqlOrderRepository>();
Bind<IProductRepository>().To<LinqToSqlProductRepository>();
Để:
Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<NHibernateOrderRepository>();
Bind<IProductRepository>().To<NHibernateProductRepository>();
Và có bạn, bây giờ tất cả mọi thứ mà phụ thuộc vào IOrderRepository
đang sử dụng phiên bản NHibernate, bạn chỉ phải thay đổi một dòng mã như trái ngược với hàng trăm dòng. Và chúng tôi đang chạy các phiên bản LINQ to SQL và NHibernate cạnh nhau, chức năng chuyển đổi từng mảnh một mà không bao giờ phá vỡ bất cứ điều gì ở giữa.
Vì vậy, để tóm tắt tất cả những điểm tôi đã thực hiện:
Đừng dựa chặt chẽ trên một giao diện chung IRepository<T>
. Hầu hết chức năng bạn muốn từ một kho lưu trữ là cụ thể, không phải chung. Nếu bạn muốn bao gồm một số IRepository<T>
ở cấp trên của phân cấp lớp/giao diện, điều đó là tốt, nhưng bộ điều khiển phải phụ thuộc vào kho lưu trữ cụ thể để bạn không phải thay đổi mã của mình ở 5 địa điểm khác nhau khi bạn tìm thấy kho lưu trữ chung thiếu các phương thức quan trọng.
Bộ điều khiển nên chấp nhận kho lưu trữ từ bên ngoài, không được tạo kho của riêng mình. Đây là một bước quan trọng trong việc loại bỏ khả năng kiểm tra ghép nối và cải thiện.
Thông thường bạn sẽ muốn kết nối bộ điều khiển bằng cách sử dụng khuôn khổ Dependency Injection, và nhiều trong số chúng có thể được tích hợp liền mạch với ASP.NET MVC.Nếu đó là quá nhiều cho bạn để có trong, sau đó ít nhất bạn nên sử dụng một số loại nhà cung cấp dịch vụ tĩnh để bạn có thể tập trung tất cả các logic tạo kho lưu trữ. (Về lâu dài, có thể bạn sẽ dễ dàng tìm hiểu và sử dụng khung DI).
Kho lưu trữ chung chung! YAY! SingletonSquared, chỉ là những gì chúng ta cần! ;-) –
@Sky, người ta không bao giờ có đủ. Cá nhân tôi đang nghĩ đến kho lưu trữ chung một phần tĩnh. –