Tình hình hiện nay
Thiết lập hiện tại vi phạm Nguyên tắc phân chia giao diện (I trong RẮN).
Tài liệu tham khảo
Theo Wikipedia, nguyên tắc phân tách giao diện (ISP) nói rằng không khách hàng nào bị buộc phải phụ thuộc vào các phương thức mà nó không sử dụng . Nguyên tắc phân tách giao diện được xây dựng bởi Robert Martin vào giữa những năm 1990.
Nói cách khác, nếu đây là giao diện của bạn:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
Sau đó, mỗi lớp thực hiện giao diện này phải sử dụng mọi phương thức được liệt kê của giao diện. Không ngoại lệ.
Hãy tưởng tượng nếu có một phương pháp tổng quát:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
Nếu bạn thực sự làm điều đó để chỉ một số lớp triển khai thực sự có thể xóa người dùng, thì phương thức này đôi khi sẽ nổ tung trong khuôn mặt của bạn (hoặc không làm gì cả). Đó không phải là thiết kế tốt.
Giải pháp đề xuất của bạn
Tôi đã thấy một giải pháp trong đó IUserInterface có phương thức triển khai thực hiện trả về một số nguyên là kết quả của các bit OR của các hành động theo bit bit ANDed với các hành động được yêu cầu.
Những gì bạn về cơ bản muốn làm là:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
Tôi đang bỏ qua cách chính xác chúng tôi xác định liệu một lớp nhất định có thể xóa người dùng hay không. Cho dù đó là boolean, cờ bit, ... không thành vấn đề. Tất cả tập trung vào một câu trả lời nhị phân: nó có thể xóa người dùng, có hay không?
Điều đó sẽ giải quyết vấn đề, phải không? Vâng, về mặt kỹ thuật, nó làm. Nhưng bây giờ, bạn đang vi phạm Nguyên tắc thay thế Liskov (chữ L trong RẮN).
Để giải thích Wikipedia khá phức tạp, tôi đã tìm thấy một ví dụ điển hình trên StackOverflow . Lưu ý ví dụ "xấu":
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
Tôi giả sử bạn thấy sự tương đồng ở đây. Đây là một phương thức được cho là xử lý một đối tượng trừu tượng ( IDuck, IUserBackend), nhưng do thiết kế lớp bị xâm phạm, trước tiên, nó buộc phải xử lý các triển khai cụ thể ( ElectricDuck, đảm bảo đó không phải là một IUserBackendlớp không thể xóa người dùng).
Điều này đánh bại mục đích phát triển một cách tiếp cận trừu tượng.
Lưu ý: Ví dụ ở đây dễ sửa hơn trường hợp của bạn. Ví dụ, nó cũng đủ để có ElectricDucklượt cứ vào bên trong các Swim()phương pháp. Cả hai con vịt vẫn có thể bơi, vì vậy kết quả chức năng là như nhau.
Bạn có thể muốn làm một cái gì đó tương tự. Đừng . Bạn không thể giả vờ xóa người dùng nhưng thực tế có phần thân phương thức trống. Mặc dù điều này không hoạt động từ góc độ kỹ thuật, nhưng nó không thể biết liệu lớp triển khai của bạn có thực sự làm điều gì đó khi được yêu cầu làm điều gì đó hay không. Đó là một nơi sinh sản cho mã không thể nhầm lẫn.
Giải pháp đề xuất của tôi
Nhưng bạn đã nói rằng một lớp triển khai chỉ có thể xử lý một số phương thức này.
Vì lợi ích của ví dụ, giả sử rằng đối với mọi sự kết hợp có thể có của các phương thức này, có một lớp sẽ thực hiện nó. Nó bao gồm tất cả các căn cứ của chúng tôi.
Giải pháp ở đây là phân chia giao diện .
public interface IGetUserService
{
User getUser(int uid);
}
public interface ICreateUserService
{
User createUser(int uid);
}
public interface IDeleteUserService
{
void deleteUser(int uid);
}
public interface ISetPasswordService
{
void setPassword(int uid, string password);
}
Lưu ý rằng bạn có thể thấy điều này đến khi bắt đầu câu trả lời của tôi. Các giao diện Tách riêng Nguyên tắc tên đã tiết lộ rằng nguyên tắc này được thiết kế để làm cho bạn cách ly các giao diện đến một mức độ đủ.
Điều này cho phép bạn kết hợp các giao diện kết hợp theo ý muốn:
public class UserRetrievalService
: IGetUserService, ICreateUserService
{
//getUser and createUser methods implemented here
}
public class UserDeleteService
: IDeleteUserService
{
//deleteUser method implemented here
}
public class DoesEverythingService
: IGetUserService, ICreateUserService, IDeleteUserService, ISetPasswordService
{
//All methods implemented here
}
Mỗi lớp có thể quyết định những gì họ muốn làm, mà không cần phá vỡ hợp đồng giao diện của họ.
Điều này cũng có nghĩa là chúng ta không cần kiểm tra xem một lớp nhất định có thể xóa người dùng hay không. Mỗi lớp thực hiện IDeleteUserServicegiao diện sẽ có thể xóa người dùng = Không vi phạm Nguyên tắc thay thế Liskov .
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
Nếu bất cứ ai cố gắng vượt qua một đối tượng không thực hiện IDeleteUserService, chương trình sẽ từ chối biên dịch. Đây là lý do tại sao chúng tôi thích có loại an toàn.
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
Chú thích
Tôi lấy ví dụ đến mức cực đoan, tách giao diện thành các phần nhỏ nhất có thể. Tuy nhiên, nếu tình huống của bạn khác đi, bạn có thể thoát khỏi những khối lớn hơn.
Ví dụ: nếu mọi dịch vụ có thể tạo người dùng luôn có khả năng xóa người dùng (và ngược lại), bạn có thể giữ các phương thức này như một phần của một giao diện:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
Không có lợi ích kỹ thuật nào khi làm điều này thay vì tách ra thành các phần nhỏ hơn; nhưng nó sẽ làm cho sự phát triển dễ dàng hơn một chút vì nó đòi hỏi ít lò hơi hơn.
IUserBackendnên chứadeleteUserphương thức nào cả. Đó phải là một phần củaIUserDeleteBackend(hoặc bất cứ điều gì bạn muốn gọi nó). Mã cần xóa người dùng sẽ có các đối sốIUserDeleteBackend, mã không cần chức năng đó sẽ sử dụngIUserBackendvà sẽ không có bất kỳ rắc rối nào với các phương thức chưa được thực hiện.