2012-04-25 31 views
24

Tôi đã phạm tội có mối quan hệ 1-1 giữa giao diện của tôi và các lớp cụ thể khi sử dụng tiêm phụ thuộc. Khi tôi cần thêm một phương thức vào một giao diện, tôi sẽ phá vỡ tất cả các lớp thực hiện giao diện.Tiêm phụ thuộc với giao diện hoặc lớp học

Đây là một ví dụ đơn giản, nhưng giả sử rằng tôi cần phải tiêm một ILogger vào một trong các lớp học của tôi.

public interface ILogger 
{ 
    void Info(string message); 
} 

public class Logger : ILogger 
{ 
    public void Info(string message) { } 
} 

Có mối quan hệ 1-1 tương tự như mùi này. Vì tôi chỉ có một triển khai duy nhất, có bất kỳ vấn đề nào có khả năng xảy ra nếu tôi tạo một lớp và đánh dấu phương thức Info là ảo để ghi đè trong các thử nghiệm của tôi thay vì phải tạo giao diện cho một lớp đơn lẻ không?

public class Logger 
{ 
    public virtual void Info(string message) 
    { 
     // Log to file 
    } 
} 

Nếu tôi cần thực hiện khác, tôi có thể ghi đè lên Info phương pháp:

public class SqlLogger : Logger 
{ 
    public override void Info(string message) 
    { 
     // Log to SQL 
    } 
} 

Nếu mỗi người trong số các lớp này có các thuộc tính hoặc các phương pháp cụ thể mà sẽ tạo ra một sự trừu tượng rò rỉ, tôi có thể trích xuất ra khỏi cơ sở lớp học:

public class Logger 
{ 
    public virtual void Info(string message) 
    { 
     throw new NotImplementedException(); 
    } 
} 

public class SqlLogger : Logger 
{ 
    public override void Info(string message) { } 
} 

public class FileLogger : Logger 
{ 
    public override void Info(string message) { } 
} 

Lý do tôi không đánh dấu lớp cơ sở là trừu tượng là vì nếu tôi muốn thêm phương pháp khác, tôi sẽ không phá vỡ exis ting triển khai. Ví dụ: nếu số FileLogger của tôi cần phương thức Debug, tôi có thể cập nhật lớp cơ sở Logger mà không vi phạm SqlLogger hiện tại.

public class Logger 
{ 
    public virtual void Info(string message) 
    { 
     throw new NotImplementedException(); 
    } 

    public virtual void Debug(string message) 
    { 
     throw new NotImplementedException(); 
    } 
} 

public class SqlLogger : Logger 
{ 
    public override void Info(string message) { } 
} 

public class FileLogger : Logger 
{ 
    public override void Info(string message) { } 
    public override void Debug(string message) { } 
} 

Một lần nữa, đây là ví dụ đơn giản, nhưng khi nào tôi nên sử dụng giao diện?

+1

_ Lý do tại sao tôi không đánh dấu lớp cơ sở là trừu tượng là vì nếu tôi muốn thêm phương thức khác_ Hm, lớp trừu tượng có thể chứa triển khai. Bạn có thể thêm phương thức Debug vào lớp Logger trừu tượng của bạn. –

+0

Việc phá vỡ các triển khai hiện tại chỉ là vấn đề nếu bạn đang viết một thư viện có thể tái sử dụng. Bạn có phải? Hay bạn chỉ đơn giản là viết một dòng ứng dụng kinh doanh? – Steven

+0

Đó không phải là phạm vi của câu hỏi, nhưng thừa kế được đánh giá cao. Một 'SqlLogger' chỉ là một' Logger' cụ thể với một 'SqlLogPersistenceStrategy'. Thành phần tốt hơn nhiều so với thừa kế trong hầu hết các trường hợp. Cũng cho vấn đề của bạn, những gì về các ISP? 'ILogInfo',' ILogError', vv – plalx

Trả lời

27

Các "Quick" Trả lời

tôi sẽ gắn bó với giao diện. Chúng được thiết kế là hợp đồng tiêu thụ cho các thực thể bên ngoài.

@JakubKonecki đã đề cập đến nhiều lần kế thừa. Tôi nghĩ rằng đây là lý do lớn nhất để gắn bó với các giao diện vì nó sẽ trở nên rất rõ ràng ở phía người tiêu dùng nếu bạn buộc họ phải lấy một lớp cơ sở ... không ai thích các lớp cơ sở bị đẩy vào chúng.

Các cập nhật "Quick" Trả lời

Bạn đã tuyên bố vấn đề với việc triển khai giao diện ngoài tầm kiểm soát của bạn. Cách tiếp cận tốt là tạo một giao diện mới kế thừa từ giao diện cũ và sửa lỗi triển khai của riêng bạn.Sau đó, bạn có thể thông báo cho các nhóm khác rằng một giao diện mới có sẵn. Theo thời gian, bạn có thể ngừng sử dụng các giao diện cũ hơn.

Đừng quên bạn có thể sử dụng sự hỗ trợ của explicit interface implementations để giúp duy trì một khoảng cách tốt đẹp giữa các giao diện hợp lý giống nhau, nhưng các phiên bản khác nhau.

Nếu bạn muốn tất cả điều này phù hợp với DI, sau đó cố gắng không xác định giao diện mới và thay vào đó ưu tiên bổ sung. Ngoài ra, để hạn chế các thay đổi mã máy khách, hãy thử kế thừa các giao diện mới từ các giao diện cũ.

thực hiện so với tiêu thụ

Có sự khác biệt giữa thực hiện giao diện và tốn nó. Thêm một phương thức phá vỡ (các) việc triển khai thực hiện, nhưng không phá vỡ người tiêu dùng.

Xóa một phương pháp rõ ràng là phá vỡ người tiêu dùng, nhưng không phá vỡ việc triển khai - tuy nhiên bạn sẽ không làm điều này nếu bạn có khả năng tương thích ngược cho người tiêu dùng của mình.

Kinh nghiệm của tôi

Chúng tôi thường xuyên có một 1-to-1 mối quan hệ với các giao diện. Nó phần lớn là một hình thức nhưng đôi khi bạn có được các trường hợp đẹp, nơi các giao diện hữu ích vì chúng tôi triển khai thử nghiệm/thử nghiệm hoặc chúng tôi thực sự cung cấp các triển khai cụ thể cho từng khách hàng. Thực tế là điều này thường xuyên phá vỡ một thực hiện đó nếu chúng ta xảy ra để thay đổi giao diện không phải là một mùi mã, theo ý kiến ​​của tôi, nó chỉ đơn giản là cách bạn làm việc với các giao diện.

Cách tiếp cận dựa trên giao diện của chúng tôi hiện đang giúp chúng tôi sử dụng các kỹ thuật như mẫu nhà máy và các yếu tố của DI để cải thiện cơ sở mã cũ. Thử nghiệm đã có thể nhanh chóng tận dụng lợi thế của thực tế là các giao diện tồn tại trong cơ sở mã trong nhiều năm trước khi tìm thấy một "dứt khoát" sử dụng (tức là, không chỉ 1-1 ánh xạ với các lớp cụ thể).

Base Class Nhược điểm lớp

cơ sở là để chia sẻ chi tiết thực hiện cho các tổ chức phổ biến, thực tế họ có thể làm điều gì đó tương tự với chia sẻ một API công khai là một sản phẩm phụ trong quan điểm của tôi. Giao diện được thiết kế để chia sẻ công khai API, vì vậy hãy sử dụng chúng.

Với các lớp cơ sở, bạn cũng có thể có khả năng bị rò rỉ các chi tiết triển khai, ví dụ: nếu bạn cần đặt một thứ gì đó công khai cho một phần khác của quá trình triển khai để sử dụng. Đây không phải là lợi ích để duy trì một API công cộng sạch sẽ.

Breaking/Hỗ trợ Triển khai

Nếu bạn đi xuống các tuyến đường giao diện bạn có thể gặp khó khăn trong việc thay đổi ngay cả những giao diện do hợp đồng vi phạm. Ngoài ra, như bạn đề cập, bạn có thể phá vỡ các triển khai ngoài tầm kiểm soát của bạn. Có hai cách để giải quyết vấn đề này:

  1. Nêu bạn không phá vỡ người tiêu dùng, nhưng bạn sẽ không hỗ trợ triển khai.
  2. Trạng thái khi giao diện được xuất bản, nó sẽ không bao giờ bị thay đổi.

tôi đã chứng kiến ​​sau này, tôi thấy nó có hai vỏ ngoài:

  1. giao diện hoàn toàn riêng biệt cho bất kỳ công cụ mới: MyInterfaceV1, MyInterfaceV2.
  2. Thừa kế giao diện: MyInterfaceV2 : MyInterfaceV1.

Cá nhân tôi sẽ không chọn để đi tuyến đường này, tôi sẽ chọn không hỗ trợ triển khai phá vỡ các thay đổi. Nhưng đôi khi chúng ta không có lựa chọn này.

Một số Mã

public interface IGetNames 
{ 
    List<string> GetNames(); 
} 

// One option is to redefine the entire interface and use 
// explicit interface implementations in your concrete classes. 
public interface IGetMoreNames 
{ 
    List<string> GetNames(); 
    List<string> GetMoreNames(); 
} 

// Another option is to inherit. 
public interface IGetMoreNames : IGetNames 
{ 
    List<string> GetMoreNames(); 
} 

// A final option is to only define new stuff. 
public interface IGetMoreNames 
{ 
    List<string> GetMoreNames(); 
} 
+0

Một trong những khó khăn mà tôi gặp phải là khi giao diện này được chia sẻ trên toàn doanh nghiệp. Nếu tôi cập nhật một giao diện, nó sẽ phá vỡ việc triển khai cho tất cả các nhóm khác sử dụng giao diện này. Tại thời điểm này, chúng tôi đang buộc những thay đổi này trên chúng. Trong một thế giới hoàn hảo, mọi người sẽ hạnh phúc và sẵn sàng cập nhật các triển khai của họ, nhưng đó không phải lúc nào cũng như vậy. – nivlam

+0

Nếu bạn không thể thay đổi giao diện - bạn có thể tạo một giao diện khác: 'inteface IDebuger {void Debug (chuỗi tin nhắn);}' và triển khai nó trong FileLogger. Vì vậy, nếu các nhóm khác không cần nó, họ sẽ không sử dụng và thực hiện nó. –

+0

@nivlam Cách thay thế cho điều này là khi giao diện được tạo, nó bị khóa để thay đổi. Tạo các giao diện mới kế thừa các giao diện cũ, sau đó các triển khai có thể tùy chọn cũng triển khai các giao diện mới ... có nghĩa là chỉ thực hiện nội bộ của bạn. Tôi đã cập nhật câu trả lời cho phù hợp. –

2

Bạn nên luôn luôn thích giao diện.

Có, trong một số trường hợp, bạn sẽ có cùng phương pháp trên lớp và giao diện, nhưng trong các trường hợp phức tạp hơn, bạn sẽ không làm như vậy. Cũng nên nhớ rằng không có nhiều thừa kế trong .NET.

Bạn nên giữ các giao diện của mình trong một hội đồng riêng biệt và các lớp của bạn phải ở bên trong.

Một lợi ích khác của việc viết mã chống lại giao diện là khả năng dễ dàng giả lập chúng trong các bài kiểm tra đơn vị.

+0

Tại sao nó là một điều tốt để giữ các giao diện trong một hội đồng riêng biệt? – thedev

+4

@thedev Bạn có thể xuất bản các giao diện mà không cần xuất bản triển khai.Đây cũng có thể được chia sẻ mà không bị rò rỉ một triển khai không mong muốn mà chỉ cần thêm các vấn đề trên không hoặc bảo trì. –

0

Tôi thích giao diện. Cho stub và mocks cũng được triển khai (loại), tôi luôn có ít nhất hai triển khai của bất kỳ giao diện nào. Ngoài ra, giao diện có thể được stubbed và chế nhạo để kiểm tra.

Hơn nữa, góc hợp đồng mà Adam Houldsworth đề cập là rất mang tính xây dựng. IMHO nó làm cho mã sạch hơn so với việc thực hiện 1-1 các giao diện làm cho nó có mùi.

9

giao diện ILogger của bạn là phá vỡ interface segregation principle khi bạn bắt đầu thêm Debug, Error, và Critical phương pháp ngoài Info. Hãy xem horrible Log4Net ILog interface và bạn sẽ biết những gì tôi đang nói về.

Thay vì tạo ra một phương pháp mỗi log mức độ nghiêm trọng, tạo ra một phương pháp duy nhất mà phải mất một đối tượng đăng nhập:

void Log(LogEntry entry); 

này hoàn toàn giải quyết tất cả các vấn đề của bạn, bởi vì:

  1. LogEntry sẽ là một DTO đơn giản và bạn có thể thêm các thuộc tính mới vào nó, mà không vi phạm bất kỳ ứng dụng khách nào.
  2. Bạn có thể tạo một tập hợp các phương thức mở rộng cho giao diện ILogger của bạn để ánh xạ tới phương thức Log đơn lẻ đó.

Dưới đây là một ví dụ về phương pháp khuyến nông như:

public static class LoggerExtensions 
{ 
    public static void Debug(this ILogger logger, string message) 
    { 
     logger.Log(new LogEntry(message) 
     { 
      Severity = LoggingSeverity.Debug, 
     }); 
    } 

    public static void Info(this ILogger logger, string message) 
    { 
     logger.Log(new LogEntry(message) 
     { 
      Severity = LoggingSeverity.Information, 
     }); 
    } 
} 

Đối với một cuộc thảo luận chi tiết hơn về thiết kế này, vui lòng đọc this.

+0

Vấn đề bây giờ là hợp đồng không xác định mức độ nghiêm trọng nào hoặc không được hỗ trợ. Với ISP trong tâm trí, những gì về 'ILogInfo',' ILogError', 'ILogDebug', vv? – plalx

Các vấn đề liên quan