Cân bằng tiêm phụ thuộc với thiết kế API công cộng


13

Tôi đã suy nghĩ làm thế nào để cân bằng thiết kế có thể kiểm tra bằng cách sử dụng phép nội xạ phụ thuộc với việc cung cấp API công khai cố định đơn giản. Vấn đề nan giải của tôi là: mọi người sẽ muốn làm một cái gì đó như thế nào var server = new Server(){ ... }và không phải lo lắng về việc tạo ra nhiều phụ thuộc và biểu đồ phụ thuộc mà một người Server(,,,,,,)có thể có. Trong khi phát triển, tôi không quá lo lắng, vì tôi sử dụng khung IoC / DI để xử lý tất cả những điều đó (Tôi không sử dụng các khía cạnh quản lý vòng đời của bất kỳ container nào, điều này sẽ làm phức tạp thêm mọi thứ).

Bây giờ, các phụ thuộc không có khả năng được thực hiện lại. Thành phần hóa trong trường hợp này gần như hoàn toàn cho khả năng kiểm tra (và thiết kế hợp lý!) Thay vì tạo đường nối để mở rộng, v.v. Mọi người sẽ 99,999% thời gian muốn sử dụng cấu hình mặc định. Vì thế. Tôi có thể mã hóa các phụ thuộc. Đừng muốn làm điều đó, chúng tôi mất thử nghiệm của chúng tôi! Tôi có thể cung cấp một hàm tạo mặc định với các phụ thuộc được mã hóa cứng và một hàm phụ thuộc. Điều đó ... lộn xộn, và có thể gây nhầm lẫn, nhưng khả thi. Tôi có thể làm cho phần phụ thuộc nhận hàm tạo bên trong và làm cho đơn vị của tôi kiểm tra một nhóm bạn (giả sử C #), để dọn dẹp API công khai nhưng để lại một cái bẫy ẩn khó chịu ẩn giấu để bảo trì. Có hai nhà xây dựng được kết nối ngầm chứ không rõ ràng sẽ là thiết kế tồi nói chung trong cuốn sách của tôi.

Tại thời điểm đó là về điều xấu xa nhất tôi có thể nghĩ ra. Ý kiến? Sự khôn ngoan?

Câu trả lời:


11

Tiêm phụ thuộc là một mô hình mạnh mẽ khi được sử dụng tốt, nhưng tất cả quá thường xuyên các học viên của nó trở nên phụ thuộc vào một số khuôn khổ bên ngoài. API kết quả khá đau đối với những người trong chúng ta không muốn liên kết lỏng lẻo ứng dụng của mình với băng keo XML. Đừng quên về Plain Old Object (POO).

Trước tiên hãy xem xét cách bạn mong đợi ai đó sử dụng API của mình - không có khung liên quan.

  • Làm thế nào để bạn khởi tạo máy chủ?
  • Làm thế nào để bạn mở rộng Máy chủ?
  • Bạn muốn phơi bày điều gì trong API bên ngoài?
  • Bạn muốn ẩn điều gì trong API bên ngoài?

Tôi thích ý tưởng cung cấp các triển khai mặc định cho các thành phần của bạn và thực tế là bạn có các thành phần đó. Cách tiếp cận sau đây là một cách thực hành OO hoàn toàn hợp lệ và hợp lý:

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

Miễn là bạn ghi lại những gì triển khai mặc định cho các thành phần trong tài liệu API và cung cấp một hàm tạo thực hiện tất cả các thiết lập, bạn sẽ ổn. Các hàm tạo tầng không dễ vỡ miễn là mã thiết lập xảy ra trong một hàm tạo chính.

Về cơ bản, tất cả các nhà xây dựng phải đối mặt công khai sẽ có các kết hợp khác nhau của các thành phần bạn muốn trưng ra. Trong nội bộ, họ sẽ đưa các mặc định và trì hoãn vào một nhà xây dựng riêng hoàn thành việc thiết lập. Về bản chất, điều này cung cấp cho bạn một số DRY và khá dễ kiểm tra.

Nếu bạn cần cung cấp friendquyền truy cập vào các thử nghiệm của mình để thiết lập đối tượng Máy chủ để cô lập nó để thử nghiệm, hãy truy cập nó. Nó sẽ không phải là một phần của API công khai, đó là những gì bạn muốn cẩn thận.

Chỉ cần tử tế với người tiêu dùng API của bạn và không yêu cầu khung IoC / DI để sử dụng thư viện của bạn. Để cảm nhận được nỗi đau mà bạn đang gây ra, hãy đảm bảo các bài kiểm tra đơn vị của bạn không dựa vào khung IoC / DI. (Đó là một tiểu thú cưng cá nhân, nhưng ngay khi bạn giới thiệu khung thì nó không còn là thử nghiệm đơn vị nữa - nó trở thành thử nghiệm tích hợp).


Tôi đồng ý và tôi chắc chắn không phải là người hâm mộ súp súp cấu hình IoC - Cấu hình XML không phải là thứ tôi sử dụng ở tất cả những gì IoC quan tâm. Chắc chắn yêu cầu sử dụng IoC chính xác là những gì tôi đã tránh - đó là loại giả định mà một nhà thiết kế API công cộng không bao giờ có thể thực hiện được. Cảm ơn vì nhận xét, nó dọc theo dòng mà tôi đã suy nghĩ và thật yên tâm khi kiểm tra sự tỉnh táo ngay bây giờ và một lần nữa!
kolektiv

-1 Câu trả lời này về cơ bản nói "không sử dụng tiêm phụ thuộc" và chứa các giả định không chính xác. Trong ví dụ của bạn nếu hàm HttpListenertạo thay đổi thì Serverlớp cũng phải thay đổi. Chúng được ghép nối. Một giải pháp tốt hơn là những gì @Winston Ewert nói bên dưới, đó là cung cấp một lớp mặc định với các phụ thuộc được chỉ định trước, bên ngoài API của thư viện. Sau đó, lập trình viên không bị buộc phải thiết lập tất cả các phụ thuộc kể từ khi bạn đưa ra quyết định cho anh ta, nhưng anh ta vẫn có thể linh hoạt để thay đổi chúng sau này. Đây là một vấn đề lớn khi bạn có sự phụ thuộc của bên thứ ba.
M. Dudley

FWIW ví dụ được cung cấp được gọi là "tiêm khốn". stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
M. Dudley

1
@MichaelDudley, bạn đã đọc câu hỏi của OP chưa. Tất cả các "giả định" đều dựa trên thông tin do OP cung cấp. Trong một thời gian, "tiêm khốn" như bạn gọi, là định dạng tiêm phụ thuộc ưa thích - đặc biệt đối với các hệ thống có thành phần không thay đổi trong thời gian chạy. Setters và getters cũng là một hình thức tiêm phụ thuộc. Tôi chỉ đơn giản sử dụng các ví dụ dựa trên những gì OP cung cấp.
Berin Loritsch

4

Trong Java trong những trường hợp như vậy, thông thường sẽ có cấu hình "mặc định" được xây dựng bởi một nhà máy, được giữ độc lập với máy chủ hoạt động "đúng" thực tế. Vì vậy, người dùng của bạn viết:

var server = DefaultServer.create();

trong khi phương Serverthức khởi tạo vẫn chấp nhận tất cả các phụ thuộc của nó và có thể được sử dụng để tùy chỉnh sâu.


+1, SRP. Trách nhiệm của văn phòng nha sĩ là không xây dựng văn phòng nha sĩ. Trách nhiệm của một máy chủ không phải là xây dựng một máy chủ.
R. Schmitz

2

Làm thế nào về việc cung cấp một lớp con cung cấp "api công cộng"

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

Người dùng có thể new StandardServer()và đang trên đường của họ. Họ cũng có thể sử dụng lớp cơ sở Máy chủ nếu họ muốn kiểm soát nhiều hơn về cách thức hoạt động của máy chủ. Đây ít nhiều là cách tiếp cận tôi sử dụng. (Tôi không sử dụng khung như tôi chưa thấy điểm.)

Điều này vẫn phơi bày api nội bộ, nhưng tôi nghĩ bạn nên. Bạn đã chia các đối tượng của mình thành các thành phần hữu ích khác nhau để hoạt động độc lập. Không có thông báo khi nào bên thứ ba có thể muốn sử dụng một trong các thành phần riêng lẻ.


1

Tôi có thể làm cho phần phụ thuộc nhận hàm tạo bên trong và làm cho đơn vị của tôi kiểm tra một nhóm bạn (giả sử C #), để dọn dẹp API công khai nhưng để lại một cái bẫy ẩn khó chịu ẩn giấu để bảo trì.

Điều đó dường như không phải là một "cái bẫy khó chịu" đối với tôi. Các nhà xây dựng công cộng chỉ nên gọi nội bộ với các phụ thuộc "mặc định". Miễn là rõ ràng rằng các nhà xây dựng công cộng không nên sửa đổi, mọi thứ sẽ ổn.


0

Tôi hoàn toàn đồng ý với ý kiến ​​của bạn. Chúng tôi không muốn gây ô nhiễm ranh giới sử dụng thành phần chỉ nhằm mục đích thử nghiệm đơn vị. Đây là toàn bộ vấn đề của giải pháp thử nghiệm dựa trên DI.

Tôi sẽ đề nghị sử dụng InjectableFactory / InjectableInstance mẫu. InjectionInstance là một lớp tiện ích có thể tái sử dụng đơn giản, là giá trị có thể thay đổi . Giao diện thành phần chứa tham chiếu đến giá trị này được khởi tạo với cài đặt mặc định. Giao diện sẽ cung cấp một phương thức singleton get () ủy nhiệm cho người giữ giá trị. Máy khách thành phần sẽ gọi phương thức get () thay vì mới để lấy một thể hiện thực hiện để tránh sự phụ thuộc trực tiếp. Trong thời gian thử nghiệm, chúng tôi có thể tùy chọn thay thế việc thực hiện mặc định bằng một giả. Sau đây tôi sẽ sử dụng một ví dụ TimeProviderlớp là sự trừu tượng của Date () vì vậy nó cho phép thử nghiệm đơn vị tiêm và mô phỏng để mô phỏng thời điểm khác nhau. Xin lỗi tôi sẽ sử dụng java ở đây vì tôi quen thuộc hơn với java. C # nên tương tự.

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

Công cụ tiêm thuốc khá dễ thực hiện, tôi có một triển khai tham chiếu trong java. Để biết chi tiết, xin vui lòng tham khảo bài viết trên blog của tôi Phụ thuộc vào cảm ứng với Nhà máy thuốc tiêm


Vì vậy, ... bạn thay thế tiêm phụ thuộc bằng một singleton đột biến? Nghe có vẻ kinh khủng, làm thế nào bạn sẽ chạy thử nghiệm độc lập? Bạn có mở một quy trình mới cho mỗi bài kiểm tra hoặc buộc chúng vào một trình tự cụ thể không? Java không phải là phù hợp mạnh mẽ của tôi, tôi đã hiểu sai điều gì?
nvoigt

Trong dự án maven Java, cá thể chia sẻ không phải là vấn đề vì công cụ Junit sẽ chạy từng thử nghiệm trong trình nạp lớp khác nhau, tuy nhiên tôi gặp phải vấn đề này trong một số IDE vì nó có thể sử dụng lại trình tải lớp tương tự. Để giải quyết vấn đề này, lớp cung cấp một phương thức thiết lập lại được gọi để thiết lập lại trạng thái ban đầu sau mỗi bài kiểm tra. Trong thực tế, đó là một tình huống hiếm hoi mà các thử nghiệm khác nhau muốn sử dụng giả khác nhau. Chúng tôi đã áp dụng mô hình này cho các dự án quy mô lớn trong nhiều năm, Mọi thứ chỉ hoạt động trơn tru, chúng tôi rất thích đọc mã, sạch mà không phải hy sinh tính di động và khả năng kiểm tra.
Jianwu Chen
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.