Sự phụ thuộc đảo ngược mở rộng API, dẫn đến các thử nghiệm không cần thiết


8

Câu hỏi này đã làm phiền tôi trong vài ngày và cảm giác như một vài thực tiễn mâu thuẫn với nhau.

Thí dụ

Lặp lại 1

public class FooDao : IFooDao
{
    private IFooConnection fooConnection;
    private IBarConnection barConnection;

    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooConnection = fooConnection;
        this.barConnection = barConnection;
    }

    public Foo GetFoo(int id)
    {
        Foo foo = fooConection.get(id);
        Bar bar = barConnection.get(foo);
        foo.bar = bar;
        return foo;
    }
}

Bây giờ, khi kiểm tra điều này, tôi sẽ giả mạo IFooConnection và IBarConnection và sử dụng Dependency Injection (DI) khi khởi tạo FooDao.

Tôi có thể thay đổi việc thực hiện, mà không thay đổi chức năng.

Lặp lại 2

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Bây giờ, tôi sẽ không viết trình xây dựng này, nhưng hãy tưởng tượng nó làm điều tương tự mà FooDao đã làm trước đây. Đây chỉ là một cấu trúc lại, vì vậy rõ ràng, điều này không thay đổi chức năng, và vì vậy, thử nghiệm của tôi vẫn vượt qua.

IFooBuilder là Internal, vì nó chỉ tồn tại để làm việc cho thư viện, tức là nó không phải là một phần của API.

Vấn đề duy nhất là, tôi không còn tuân thủ Nghịch đảo phụ thuộc. Nếu tôi viết lại cái này để khắc phục vấn đề đó, nó có thể trông như thế này.

Lặp lại 3

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    public FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Điều này nên làm điều đó. Tôi đã thay đổi hàm tạo, vì vậy thử nghiệm của tôi cần thay đổi để hỗ trợ điều này (hoặc cách khác), đây không phải là vấn đề của tôi.

Để làm việc với DI, FooBuilder và IFooBuilder cần được công khai. Điều này có nghĩa là tôi nên viết một bài kiểm tra cho FooBuilder, vì nó đột nhiên trở thành một phần của API thư viện của tôi. Vấn đề của tôi là, các máy khách của thư viện chỉ nên sử dụng nó thông qua thiết kế API dự định của tôi, IFooDao và các thử nghiệm của tôi hoạt động như các máy khách. Nếu tôi không tuân theo Phụ thuộc đảo ngược, các thử nghiệm và API của tôi sẽ sạch hơn.

Nói cách khác, tất cả những gì tôi quan tâm với tư cách là khách hàng, hoặc là thử nghiệm, là để có được Foo chính xác, chứ không phải cách nó được xây dựng.

Các giải pháp

  • Tôi có nên đơn giản là không quan tâm, viết các bài kiểm tra cho FooBuilder mặc dù nó chỉ công khai để làm hài lòng DI? - Hỗ trợ lặp 3

  • Tôi có nên nhận ra rằng việc mở rộng API là một nhược điểm của Nghịch đảo phụ thuộc và rất rõ ràng về lý do tại sao tôi chọn không tuân thủ nó ở đây? - Hỗ trợ lặp 2

  • Tôi có quá chú trọng đến việc có API sạch không? - Hỗ trợ lặp 3

EDIT: Tôi muốn làm rõ, rằng vấn đề của tôi không phải là "Làm thế nào để kiểm tra nội bộ?", Thay vào đó là một câu như "Tôi có thể giữ nội bộ và vẫn tuân thủ DIP, và tôi có nên không?".


Tôi có thể chỉnh sửa câu hỏi của mình để thêm các bài kiểm tra nếu bạn muốn.
Chris Wohlert

1
"FooBuilder và IFooBuilder cần được công khai". Chắc chỉ IFooBuildercần công khai? FooBuilderchỉ cần được tiếp xúc với hệ thống DI thông qua một số phương thức "thực hiện mặc định" trả về một IFooBuildervà do đó có thể duy trì nội bộ.
David Arno

Tôi đoán đó có thể là một giải pháp.
Chris Wohlert

2
Điều này đã được hỏi ở đây trên các lập trình viên ít nhất một chục lần, bằng các thuật ngữ khác nhau: "Tôi muốn tạo sự khác biệt giữa publicAPI thư viện và publicđể thử nghiệm". Hoặc ở dạng khác "Tôi muốn giữ các phương thức riêng tư để tránh xuất hiện chúng trong API, làm cách nào để kiểm tra các phương thức riêng tư đó?". Các câu trả lời luôn là "sống với API công khai rộng hơn", "sống với các thử nghiệm thông qua API công khai" hoặc "tạo các phương thức internalvà làm cho chúng hiển thị với mã kiểm tra".
Doc Brown

Tôi có thể sẽ không thay thế ctor, nhưng giới thiệu một cái mới và xâu chuỗi chúng lại với nhau.
RubberDuck

Câu trả lời:


3

Tôi sẽ đi với:

Tôi có nên nhận ra rằng việc mở rộng API là một nhược điểm của Nghịch đảo phụ thuộc và rất rõ ràng về lý do tại sao tôi chọn không tuân thủ nó ở đây? - Hỗ trợ lặp 2

Tuy nhiên, trong Nguyên tắc RẮN của OO và Thiết kế Agile (a lúc 1:08:55) chú Bob nói rằng quy tắc của ông về tiêm phụ thuộc là không tiêm mọi thứ, bạn chỉ tiêm tại các địa điểm chiến lược. (Ông cũng đề cập rằng các chủ đề đảo ngược phụ thuộc và tiêm phụ thuộc là như nhau).

Đó là, đảo ngược phụ thuộc không có nghĩa là được áp dụng cho tất cả các phụ thuộc lớp trong một chương trình. Bạn nên làm điều đó tại các địa điểm chiến lược (khi nó trả tiền (hoặc có thể trả tiền)).


2
Nhận xét này từ Bob là hoàn hảo, cảm ơn bạn rất nhiều vì đã chia sẻ điều này.
Chris Wohlert

5

Tôi sẽ chia sẻ một số kinh nghiệm / quan sát về các cơ sở mã khác nhau mà tôi đã thấy. NB Tôi không ủng hộ bất kỳ cách tiếp cận cụ thể nào, chỉ chia sẻ với bạn nơi tôi thấy mã đó sẽ diễn ra:

Tôi có nên đơn giản là không quan tâm, hãy viết các bài kiểm tra cho FooBuilder mặc dù nó chỉ công khai để làm hài lòng DI

Trước khi DI và kiểm tra tự động, sự khôn ngoan được chấp nhận là một cái gì đó nên được giới hạn trong phạm vi cần thiết. Tuy nhiên, ngày nay, không có gì lạ khi thấy các phương thức có thể được công khai nội bộ để dễ kiểm tra (vì điều này thường xảy ra trong một hội đồng khác). NB có một chỉ thị để đưa ra các phương thức nội bộ nhưng điều này ít nhiều cho cùng một điều.

Tôi có nên nhận ra rằng việc mở rộng API là một nhược điểm của Nghịch đảo phụ thuộc và rất rõ ràng về lý do tại sao tôi chọn không tuân thủ nó ở đây?

DI chắc chắn phải chịu một chi phí chung với mã bổ sung.

Tôi có quá chú trọng đến việc có một API sạch không

Kể từ DI khuôn khổ làm giới thiệu mã bổ sung, tôi đã nhận thấy một sự thay đổi đối với mã mà khi viết theo phong cách DI không sử dụng DI cho mỗi gia nhập tức là D từ RẮN.

Tóm tắt:

  1. Công cụ sửa đổi truy cập đôi khi được nới lỏng để đơn giản hóa việc kiểm tra

  2. DI không giới thiệu mã bổ sung

Trong ánh sáng của 1 & 2, một số nhà phát triển cho rằng điều này là quá nhiều hy sinh và vì vậy chỉ cần chọn phụ thuộc vào trừu tượng ban đầu với tùy chọn để phù hợp với DI sau này.


4
Đó thực sự là lý do tại sao một số nhà phát triển lựa chọn thuộc tính InternalsVisibleTo cho các phương thức nội bộ thay vì hiển thị chúng dưới dạng công khai.
Robbie Dee

1
"Tuy nhiên, ngày nay, không có gì lạ khi thấy các phương thức có thể được công khai nội bộ để dễ kiểm tra (vì điều này thường xảy ra trong một hội đồng khác)." - thực sự?
Brian Agnew

3
@BrianAgnew than ôi có. Đây là một mô hình chống cùng với "chúng tôi phải có phạm vi kiểm tra đơn vị 100%", bao gồm cả getters và setters :-( Tại sao các lập trình viên ngày nay không suy nghĩ?!
gbjbaanb

2
@ChrisWohlert giải pháp đơn giản. Đừng thay thế ctor. Thêm một cái mới. Xâu chuỗi chúng lại với nhau. Để lại bài kiểm tra hiện tại của bạn. Nó ngầm kiểm tra trình xây dựng của bạn và mã của bạn vẫn được bảo vệ. Chỉ viết bài kiểm tra nếu nó có giá trị để làm như vậy. Có một điểm mà bạn nhận được lợi tức giảm dần từ khoản đầu tư của mình.
RubberDuck

1
@ChrisWohlert hoàn toàn không có gì sai khi cung cấp một hàm tạo tiện lợi. Bạn vẫn đang tiêm phụ thuộc thực sự của lớp . Bạn nghĩ DI đã được thực hiện như thế nào trước khi tất cả các khung công tác chứa IoC "lạ mắt" xuất hiện?
RubberDuck

0

Tôi đặc biệt không mua khái niệm này rằng bạn phải đưa ra các phương pháp riêng tư (bằng mọi cách) để thực hiện thử nghiệm phù hợp. Tôi cũng đề nghị rằng API khách hàng của bạn đối mặt với API không giống với phương thức chung của đối tượng của bạn, ví dụ: đây là giao diện chung của bạn:

interface IFooDao {
   public Foo GetFoo(int id);
}

và không có đề cập đến của bạn FooBuilder

Trong câu hỏi ban đầu của bạn, tôi sẽ đi theo con đường thứ ba, sử dụng của bạn FooBuilder. Tôi có thể sẽ kiểm tra bạn FooDaobằng cách sử dụng một bản giả , do đó tập trung vào FooDaochức năng của bạn (bạn luôn có thể tiêm một trình xây dựng thực sự nếu nó dễ dàng hơn) và tôi sẽ có các bài kiểm tra riêng xung quanh bạn FooBuilder.

(Tôi ngạc nhiên khi chế giễu không được nhắc đến trong câu hỏi / câu trả lời này - nó đi đôi với thử nghiệm các giải pháp theo mô hình DI / IoC)

Re. bình luận của bạn:

Như đã nêu, trình xây dựng không cung cấp chức năng mới, vì vậy tôi không cần phải kiểm tra nó, tuy nhiên, DIP làm cho nó trở thành một phần của API, điều này buộc tôi phải kiểm tra nó.

Tôi không nghĩ rằng điều này mở rộng API bạn trình bày cho khách hàng của mình. Bạn có thể hạn chế API mà khách hàng của bạn sử dụng thông qua các giao diện (nếu bạn muốn). Tôi cũng đề nghị rằng các bài kiểm tra của bạn không nên chỉ bao quanh các thành phần phải đối mặt với công chúng. Họ nên nắm lấy tất cả các lớp (triển khai) hỗ trợ API bên dưới. ví dụ: đây là API REST cho hệ thống tôi hiện đang làm việc:

(bằng mã giả)

void doSomeCalculation(json : CalculationRequestObject);

Tôi có một phương pháp API và 1.000 bài kiểm tra - phần lớn trong số đó là xung quanh các đối tượng hỗ trợ việc này.


Tôi không đặc biệt mua khái niệm này rằng bạn phải đưa ra các phương pháp riêng tư (bằng mọi cách) để thực hiện thử nghiệm phù hợp - Tôi không tin bất cứ ai ủng hộ điều này ...
Robbie Dee

Nhưng bạn đã nói ở trên - 'Công cụ sửa đổi truy cập đôi khi được nới lỏng để đơn giản hóa việc kiểm tra'?
Brian Agnew

Nội bộ có thể được tiếp xúc với hội đồng thử nghiệm và việc thực hiện giao diện ngầm tạo ra các phương thức công khai. Tôi chưa bao giờ ủng hộ việc vạch trần các phương pháp riêng tư ...
Robbie Dee

Từ PoV của tôi, 'Nội bộ có thể được tiếp xúc với hội đồng thử nghiệm' với cùng một điều. Tôi không tin rằng kiểm tra nội bộ (dù được đánh dấu là riêng tư hay nói cách khác) là một bài tập đáng giá, trừ khi là kiểm tra hồi quy trong khi tái cấu trúc mã có cấu trúc kém
Brian Agnew

Xin chào Brian, tôi cũng như bạn, giả mạo IFooBuilder khi được phân tích cú pháp thành FooDao, nhưng điều này không có nghĩa là tôi sẽ cần một thử nghiệm khác để triển khai IFooBuilder? Cuối cùng chúng tôi cũng đang tiếp cận vấn đề thực sự, và liệu tôi có nên phơi bày nội bộ hay không.
Chris Wohlert

0

Tại sao bạn muốn theo DI trong trường hợp này nếu không nhằm mục đích thử nghiệm? Nếu không chỉ API công khai của bạn, mà cả các bài kiểm tra của bạn thực sự sạch hơn mà không có IFooBuildertham số (như bạn đã viết), sẽ không có ý nghĩa gì khi giới thiệu một lớp như vậy. Bản thân DI không phải là kết thúc, nó sẽ giúp bạn làm cho mã của mình dễ kiểm tra hơn. Nếu bạn không muốn viết các bài kiểm tra tự động cho mức độ trừu tượng cụ thể đó, đừng áp dụng DI ở cấp độ đó.

Tuy nhiên, giả sử trong giây lát bạn muốn áp dụng DI để cho phép tạo các thử nghiệm đơn vị cho nhà FooDaoxây dựng mà không cần quá trình xây dựng, nhưng vẫn không muốn một hàm FooDao(IFooBuilder fooBuilder)tạo trong API công khai của bạn, chỉ một hàm tạo FooDao(IFooConnection fooConnection, IBarConnection barConnection). Sau đó cung cấp cả hai, cái trước là "nội bộ", cái sau là "công khai":

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    // only for testing purposes!    
    internal FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    // the public API
    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }    

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Bây giờ bạn có thể giữ FooBuilderIFooBuildernội bộ, và áp dụng InternalsVisibleTođể làm cho chúng có thể truy cập được bằng các bài kiểm tra đơn vị.


Phần đầu tiên của câu hỏi của bạn là chính xác những gì tôi đang tìm kiếm. Một câu trả lời cho câu hỏi: "Tôi có nên sử dụng DIP ở đây không". Tôi không muốn nhà xây dựng thứ hai mặc dù. Nếu tôi không tuân thủ DIP, tôi không cần phải công khai IFooBuilder và tôi có thể ngừng giả mạo / kiểm tra nó. Nói tóm lại, bạn đang bảo tôi bám vào vòng lặp 2.
Chris Wohlert

Thành thật mà nói, nếu bạn loại bỏ mọi thứ khỏi "Tuy nhiên", tôi sẽ chấp nhận câu trả lời. Tôi nghĩ phần đầu tiên giải thích rất rõ khi nào và tại sao tôi nên / không nên sử dụng DI.
Chris Wohlert

Tôi chỉ cần thêm lời cảnh báo rằng DI không chỉ là về thử nghiệm - nó chỉ đơn giản hóa việc hoán đổi một sự lắng đọng với nhau. Về việc bạn cần điều này ở đây là một cái gì đó chỉ bạn có thể trả lời. Như 17 trên 26 tuyệt vời đặt nó - một nơi nào đó giữa YAGNI & PYIAC.
Robbie Dee

@ChrisWohlert: Tôi sẽ không xóa phần 2 chỉ vì bạn không muốn áp dụng nó cho tình huống của mình. Bạn không cần các bài kiểm tra ở cấp độ đó - tốt thôi, áp dụng phần 1, không sử dụng DI ở đây. Bạn muốn kiểm tra tránh để có một số phần nhất định trong API công khai - tốt thôi, bạn cần hai điểm nhập trong mã của mình với các công cụ sửa đổi truy cập khác nhau, vì vậy hãy áp dụng phần 2. Everone có thể chọn giải pháp phù hợp nhất với mình.
Doc Brown

Chắc chắn mọi người đều có thể chọn giải pháp họ muốn, nhưng bạn đã hiểu nhầm câu hỏi nếu bạn nghĩ tôi muốn kiểm tra trình xây dựng. Đó là những gì tôi đã cố gắng để thoát khỏi.
Chris Wohlert
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.