Đặt câu hỏi cho một trong các đối số cho khung tiêm phụ thuộc: Tại sao việc tạo biểu đồ đối tượng khó?


13

Các khung tiêm phụ thuộc như Google Guice cung cấp động lực sau cho việc sử dụng ( nguồn ) của họ:

Để xây dựng một đối tượng, trước tiên bạn xây dựng các phụ thuộc của nó. Nhưng để xây dựng mỗi phụ thuộc, bạn cần phụ thuộc của nó, và như vậy. Vì vậy, khi bạn xây dựng một đối tượng, bạn thực sự cần phải xây dựng một biểu đồ đối tượng.

Xây dựng đồ thị đối tượng bằng tay tốn nhiều công sức (...) và khiến việc kiểm tra trở nên khó khăn.

Nhưng tôi không mua đối số này: Ngay cả khi không có khung tiêm phụ thuộc, tôi có thể viết các lớp vừa dễ khởi tạo vừa thuận tiện để kiểm tra. Ví dụ: ví dụ từ trang động lực Guice có thể được viết lại theo cách sau:

class BillingService
{
    private final CreditCardProcessor processor;
    private final TransactionLog transactionLog;

    // constructor for tests, taking all collaborators as parameters
    BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
    {
        this.processor = processor;
        this.transactionLog = transactionLog;
    }

    // constructor for production, calling the (productive) constructors of the collaborators
    public BillingService()
    {
        this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
    }

    public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
    {
        ...
    }
}

Vì vậy, có thể có các đối số khác cho các khung tiêm phụ thuộc ( nằm ngoài phạm vi của câu hỏi này !), Nhưng việc tạo dễ dàng các biểu đồ đối tượng có thể kiểm tra không phải là một trong số chúng, phải không?


1
Tôi nghĩ rằng một lợi thế khác của tiêm phụ thuộc thường bị bỏ qua là nó buộc bạn phải biết những đối tượng phụ thuộc vào . Không có gì xuất hiện một cách kỳ diệu trong các đối tượng, bạn có thể tiêm các thuộc tính được đánh dấu rõ ràng hoặc thông qua hàm tạo trực tiếp. Không đề cập đến cách nó làm cho mã của bạn dễ kiểm tra hơn.
Benjamin Gruenbaum

Lưu ý rằng tôi không tìm kiếm những lợi thế khác của việc tiêm phụ thuộc - Tôi chỉ quan tâm đến việc hiểu lập luận rằng "khởi tạo đối tượng là khó mà không cần tiêm phụ thuộc"
oberlies

Câu trả lời này dường như cung cấp một ví dụ rất hay về lý do tại sao bạn muốn một số loại thùng chứa cho biểu đồ đối tượng của bạn (tóm lại, bạn không nghĩ đủ lớn, chỉ sử dụng 3 đối tượng).
Izkata

@Izkata Không, đây không phải là vấn đề kích thước. Với cách tiếp cận được mô tả, mã từ bài đăng đó sẽ chỉ là new ShippingService().
oberlies

@oberlies Vẫn là; Sau đó, bạn phải đi đào 9 định nghĩa lớp để tìm ra lớp nào đang được sử dụng cho ShippingService đó, thay vì một địa điểm
Izkata

Câu trả lời:


12

Có một cuộc tranh luận cũ, cũ đang diễn ra về cách tốt nhất để thực hiện tiêm phụ thuộc.

  • Việc cắt lò xo ban đầu khởi tạo một đối tượng đơn giản, và sau đó thêm các phụ thuộc mặc dù các phương thức setter.

  • Nhưng sau đó, một nhóm lớn người đã nhấn mạnh rằng tiêm phụ thuộc thông qua các tham số của hàm tạo là cách chính xác để làm điều đó.

  • Sau đó, gần đây, khi việc sử dụng sự phản chiếu trở nên phổ biến hơn, việc đặt các giá trị của các thành viên tư nhân trực tiếp, mà không cần setters hay constructor, trở thành cơn thịnh nộ.

Vì vậy, nhà xây dựng đầu tiên của bạn phù hợp với cách tiếp cận thứ hai để tiêm phụ thuộc. Nó cho phép bạn làm những điều tốt đẹp như tiêm giả để thử nghiệm.

Nhưng các nhà xây dựng không có đối số có vấn đề này. Vì nó khởi tạo các lớp triển khai cho PaypalCreditCardProcessorDatabaseTransactionLog, nó tạo ra sự phụ thuộc cứng, thời gian biên dịch vào PayPal và Cơ sở dữ liệu. Nó chịu trách nhiệm xây dựng và cấu hình toàn bộ cây phụ thuộc một cách chính xác.

  • Hãy tưởng tượng rằng bộ xử lý PayPay là một hệ thống con thực sự phức tạp và ngoài ra còn có rất nhiều thư viện hỗ trợ. Bằng cách tạo một phụ thuộc thời gian biên dịch trên lớp triển khai đó, bạn đang tạo một liên kết không thể phá vỡ đến toàn bộ cây phụ thuộc đó. Độ phức tạp của biểu đồ đối tượng của bạn vừa tăng lên theo một độ lớn, có thể là hai.

  • Rất nhiều vật phẩm trong cây phụ thuộc sẽ trong suốt, nhưng rất nhiều trong số chúng cũng cần phải được khởi tạo. Vấn đề là, bạn sẽ không thể khởi tạo a PaypalCreditCardProcessor.

  • Ngoài khởi tạo, mỗi đối tượng sẽ cần các thuộc tính được áp dụng từ cấu hình.

Nếu bạn chỉ có một phụ thuộc vào giao diện và cho phép một nhà máy bên ngoài xây dựng và tiêm phụ thuộc, họ sẽ cắt toàn bộ cây phụ thuộc PayPal và sự phức tạp của mã của bạn dừng lại ở giao diện.

Có các lợi ích khác, như là chỉ định các lớp triển khai trong cấu hình (nghĩa là tại thời gian chạy thay vì thời gian biên dịch) hoặc có đặc tả phụ thuộc động hơn thay đổi, theo môi trường (kiểm tra, tích hợp, sản xuất).

Ví dụ: giả sử PayPalProcessor có 3 đối tượng phụ thuộc và mỗi đối tượng phụ thuộc đó có thêm hai đối tượng. Và tất cả những đối tượng đó phải lấy các thuộc tính từ cấu hình. Mã như sẽ đảm nhận trách nhiệm xây dựng tất cả những thứ đó, thiết lập các thuộc tính từ cấu hình, v.v. - tất cả các mối quan tâm mà khung DI sẽ quan tâm.

Ban đầu có vẻ không rõ ràng về những gì bạn che chắn bản thân bằng cách sử dụng khung DI, nhưng nó sẽ tăng lên và trở nên rõ ràng theo thời gian. (lol tôi nói từ kinh nghiệm đã cố gắng làm điều đó một cách khó khăn)

...

Trong thực tế, ngay cả đối với một chương trình thực sự nhỏ bé, tôi thấy tôi kết thúc việc viết theo kiểu DI và chia các lớp thành các cặp thực hiện / nhà máy. Đó là, nếu tôi không sử dụng khung DI như Spring, tôi chỉ cần kết hợp một số lớp học đơn giản.

Điều đó cung cấp sự phân tách các mối quan tâm để lớp của tôi có thể thực hiện điều đó và lớp nhà máy chịu trách nhiệm xây dựng và định cấu hình công cụ.

Không phải là một cách tiếp cận bắt buộc, nhưng FWIW

...

Tổng quát hơn, mẫu giao diện DI / làm giảm độ phức tạp của mã của bạn bằng cách thực hiện hai điều:

  • trừu tượng hóa phụ thuộc hạ lưu vào giao diện

  • "nâng" các phụ thuộc ngược dòng ra khỏi mã của bạn và vào một số loại container

Trên hết, vì khởi tạo và cấu hình đối tượng là một nhiệm vụ khá quen thuộc, khung DI có thể đạt được rất nhiều tính kinh tế theo quy mô thông qua ký hiệu chuẩn & sử dụng các thủ thuật như phản xạ. Phân tán những mối quan tâm tương tự xung quanh các lớp học kết thúc thêm nhiều lộn xộn hơn người ta nghĩ.


Mã sẽ đảm nhận trách nhiệm xây dựng tất cả những điều đó - Một lớp chỉ chịu trách nhiệm biết những gì cộng tác viên của nó triển khai. Nó không cần phải biết những gì các cộng tác viên cần lần lượt. Vì vậy, đây không phải là nhiều.
oberlies

1
Nhưng nếu hàm tạo PayPalProcessor có 2 đối số thì chúng sẽ đến từ đâu?
Rob

Nó không. Giống như BillingService, PayPalProcessor có một hàm tạo zero-args tạo ra các cộng tác viên mà nó cần.
oberlies

trừu tượng hóa các phụ thuộc xuôi dòng vào các giao diện - Mã ví dụ cũng thực hiện điều này. Trường bộ xử lý thuộc loại CreditCardProcessor và không thuộc loại triển khai.
oberlies

2
@oberlies: Ví dụ mã của bạn nằm giữa hai kiểu, kiểu đối số của hàm tạo và kiểu có thể xây dựng zero-args. Sai lầm là zero-args có thể xây dựng không phải lúc nào cũng có thể. Cái hay của DI hoặc Factory là bằng cách nào đó chúng cho phép các đối tượng được xây dựng với những gì trông giống như một hàm tạo zero-args.
rwong

4
  1. Khi bạn bơi ở đầu nông của bể bơi, mọi thứ đều "dễ dàng và thuận tiện". Một khi bạn vượt qua hàng tá đồ vật, nó không còn tiện lợi nữa.
  2. Trong ví dụ của bạn, bạn đã ràng buộc quy trình thanh toán của mình mãi mãi và một ngày với PayPal. Giả sử bạn muốn sử dụng một bộ xử lý thẻ tín dụng khác nhau? Giả sử bạn muốn tạo một bộ xử lý thẻ tín dụng đặc biệt bị hạn chế trên mạng? Hoặc bạn cần kiểm tra xử lý số thẻ tín dụng? Bạn đã tạo mã không di động: "viết một lần, chỉ sử dụng một lần vì nó phụ thuộc vào biểu đồ đối tượng cụ thể mà nó được thiết kế."

Bằng cách ràng buộc biểu đồ đối tượng của bạn sớm trong quá trình, nghĩa là, gắn nó vào mã, bạn yêu cầu cả hợp đồng và việc triển khai phải có mặt. Nếu ai đó (thậm chí là bạn) muốn sử dụng mã đó cho mục đích hơi khác, họ phải tính toán lại toàn bộ biểu đồ đối tượng và thực hiện lại mã đó.

Khung DI cho phép bạn lấy một loạt các thành phần và nối chúng lại với nhau khi chạy. Điều này làm cho hệ thống "mô-đun", bao gồm một số mô-đun hoạt động với giao diện của nhau thay vì triển khai của nhau.


Khi tôi làm theo lập luận của bạn, lý do cho DI nên là "tạo một biểu đồ đối tượng bằng tay rất dễ, nhưng dẫn đến mã mà nó khó duy trì". Tuy nhiên, yêu cầu là "tạo một biểu đồ đối tượng bằng tay là khó" và tôi chỉ tìm kiếm các đối số hỗ trợ cho yêu cầu đó. Vì vậy, bài viết của bạn không trả lời câu hỏi của tôi.
oberlies

1
Tôi đoán nếu bạn giảm câu hỏi thành "tạo một biểu đồ đối tượng ...", thì nó rất đơn giản. Quan điểm của tôi là DI giải quyết vấn đề mà bạn không bao giờ giải quyết chỉ với một biểu đồ đối tượng; bạn đối phó với một gia đình của họ.
BobDalgleish

Trên thực tế, chỉ cần tạo một biểu đồ đối tượng đã có thể khó khăn nếu cộng tác viên được chia sẻ .
oberlies

4

Phương pháp "khởi tạo cộng tác viên của riêng tôi" có thể hoạt động đối với các cây phụ thuộc , nhưng chắc chắn nó sẽ không hoạt động tốt đối với các biểu đồ phụ thuộc là các biểu đồ chu kỳ theo hướng chung (DAG). Trong một DAG phụ thuộc, nhiều nút có thể trỏ đến cùng một nút - có nghĩa là hai đối tượng sử dụng cùng một đối tượng làm cộng tác viên. Trường hợp này trong thực tế có thể không được xây dựng với cách tiếp cận được mô tả trong câu hỏi.

Nếu một số cộng tác viên của tôi (hoặc cộng tác viên của cộng tác viên) nên chia sẻ một đối tượng nhất định, tôi cần phải khởi tạo đối tượng này và chuyển nó cho cộng tác viên của mình. Vì vậy, trên thực tế tôi sẽ cần biết nhiều hơn các cộng tác viên trực tiếp của mình và điều này rõ ràng là không có quy mô.


1
Nhưng cây phụ thuộc phải là một cây, theo nghĩa lý thuyết đồ thị. Nếu không, bạn đã có một chu kỳ, điều này đơn giản là không thỏa mãn.
Xion

1
@Xion Biểu đồ phụ thuộc phải là biểu đồ chu kỳ có hướng. Không có yêu cầu rằng chỉ có chính xác một đường dẫn giữa hai nút như trong cây.
oberlies

@Xion: Không nhất thiết. Xem xét một UnitOfWorktrong đó có một DbContextkho lưu trữ số ít và nhiều. Tất cả các kho lưu trữ này nên sử dụng cùng một DbContextđối tượng. Với đề xuất "tự khởi tạo" của OP, điều đó trở nên không thể thực hiện được.
Flater 22/03/19

1

Tôi chưa sử dụng Google Guice, nhưng tôi đã mất rất nhiều thời gian để di chuyển các ứng dụng N-tier cũ trong các kiến ​​trúc .Net sang IoC như Onion Layer phụ thuộc vào Dependency Injection để tách rời mọi thứ.

Tại sao phải phụ thuộc tiêm?

Mục đích của Dependency Injection không thực sự cho khả năng kiểm tra, thực ra là để có các ứng dụng được liên kết chặt chẽ và nới lỏng khớp nối càng nhiều càng tốt. (Sản phẩm mong muốn theo cách làm cho mã của bạn dễ dàng thích nghi hơn để kiểm tra đơn vị phù hợp)

Tại sao tôi nên lo lắng về khớp nối?

Khớp nối hoặc phụ thuộc chặt chẽ có thể là một điều rất nguy hiểm. (Đặc biệt là trong các ngôn ngữ được biên dịch) Trong những trường hợp này, bạn có thể có một thư viện, dll, v.v ... rất hiếm khi được sử dụng có vấn đề làm mất hiệu quả toàn bộ ứng dụng ngoại tuyến. (Toàn bộ ứng dụng của bạn chết vì một phần không quan trọng có vấn đề ... điều này thật tệ ... THỰC SỰ xấu) Bây giờ khi bạn tách rời những thứ bạn thực sự có thể thiết lập ứng dụng của mình để nó có thể chạy ngay cả khi DLL hoặc Thư viện bị thiếu hoàn toàn! Chắc chắn rằng một phần cần thư viện hoặc DLL sẽ không hoạt động, nhưng phần còn lại của ứng dụng sẽ vui vẻ như có thể.

Tại sao tôi cần Dependency Injection để thử nghiệm thích hợp

Thực sự bạn chỉ muốn mã được ghép lỏng lẻo, Dependency Injection chỉ cho phép điều đó. Bạn có thể kết nối một cách lỏng lẻo mà không cần IoC, nhưng thông thường, nó hoạt động nhiều hơn và ít thích nghi hơn (tôi chắc chắn có ai đó có ngoại lệ)

Trong trường hợp bạn đã đưa ra, tôi nghĩ sẽ dễ dàng hơn nhiều khi chỉ thiết lập tiêm phụ thuộc để tôi có thể giả định mã Tôi không quan tâm đến việc tính là một phần của thử nghiệm này. Chỉ cần nói với bạn phương pháp "Này tôi biết tôi đã bảo bạn gọi kho lưu trữ, nhưng thay vào đó, đây là dữ liệu" nên "trả lại nháy mắt " vì dữ liệu đó không bao giờ thay đổi mà bạn biết bạn chỉ kiểm tra phần sử dụng dữ liệu đó chứ không phải thực tế lấy dữ liệu.

Về cơ bản khi kiểm tra bạn muốn Tích hợp (chức năng) Kiểm tra kiểm tra một phần chức năng từ đầu đến cuối và kiểm tra đơn vị đầy đủ để kiểm tra độc lập từng đoạn mã (thường ở cấp độ phương thức hoặc chức năng).

Ý tưởng là bạn muốn đảm bảo toàn bộ chức năng đang hoạt động, nếu không bạn muốn biết chính xác đoạn mã không hoạt động.

Điều này thể được thực hiện mà không cần Dependency Injection, nhưng thông thường khi dự án của bạn phát triển, nó sẽ ngày càng trở nên cồng kềnh hơn khi không có Dependency Injection. (LUÔN LUÔN cho rằng dự án của bạn sẽ phát triển! Thà không cần thực hành các kỹ năng hữu ích hơn là tìm một dự án nhanh chóng và đòi hỏi phải tái cấu trúc và tái cấu trúc nghiêm túc sau khi mọi thứ đã diễn ra.)


1
Viết các bài kiểm tra sử dụng một phần lớn nhưng không phải tất cả các biểu đồ phụ thuộc trên thực tế là một đối số tốt cho DI.
oberlies

1
Cũng đáng chú ý với DI, bạn có thể trao đổi toàn bộ các đoạn mã của mình một cách dễ dàng. Tôi đã thấy các trường hợp trong đó một hệ thống sẽ xóa và từ chối một giao diện với một lớp hoàn toàn khác để phản ứng với sự cố ngừng hoạt động và các vấn đề về hiệu suất của các dịch vụ từ xa. Có thể có một cách tốt hơn để xử lý nó, nhưng nó hoạt động tốt đáng ngạc nhiên.
RualStorge

0

Như tôi đề cập trong câu trả lời khác , vấn đề ở đây là bạn muốn lớp Aphụ thuộc vào một số lớpB mà không cứng mã hóa lớp Bđược sử dụng vào mã nguồn của A. Đây là không thể trong Java và C # vì cách duy nhất để nhập một lớp là để chỉ nó bằng một cái tên độc nhất toàn cầu.

Sử dụng một giao diện, bạn có thể giải quyết vấn đề phụ thuộc lớp được mã hóa cứng, nhưng bạn vẫn cần phải xử lý một thể hiện của giao diện và bạn không thể gọi các nhà xây dựng hoặc bạn quay lại ngay trong ô vuông 1. Vì vậy, bây giờ hãy viết mã điều đó có thể tạo ra sự phụ thuộc của nó đẩy trách nhiệm đó sang người khác. Và sự phụ thuộc của nó đang làm điều tương tự. Vì vậy, bây giờ mỗi khi bạn cần một thể hiện của một lớp, bạn sẽ kết thúc việc xây dựng toàn bộ cây phụ thuộc một cách thủ công, trong khi đó trong trường hợp lớp A phụ thuộc trực tiếp vào B, bạn chỉ có thể gọi new A()và có lệnh gọi hàm tạo đó new B(), v.v.

Khung tiêm phụ thuộc cố gắng khắc phục điều đó bằng cách cho phép bạn chỉ định ánh xạ giữa các lớp và xây dựng cây phụ thuộc cho bạn. Điều hấp dẫn là khi bạn làm hỏng các ánh xạ, bạn sẽ tìm ra trong thời gian chạy chứ không phải vào thời gian biên dịch như trong các ngôn ngữ hỗ trợ các mô-đun ánh xạ như một khái niệm hạng nhất.


0

Tôi nghĩ rằng đây là một sự hiểu lầm lớn ở đây.

Guice là một khung tiêm phụ thuộc . Nó làm cho DI tự động . Điểm họ đưa ra trong đoạn trích bạn trích dẫn là về việc Guice có thể loại bỏ nhu cầu tạo thủ công "trình xây dựng có thể kiểm tra" mà bạn đã trình bày trong ví dụ của mình. Nó hoàn toàn không có gì để làm với chính tiêm phụ thuộc.

Nhà xây dựng này:

BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
    this.processor = processor;
    this.transactionLog = transactionLog;
}

đã sử dụng tiêm phụ thuộc. Về cơ bản bạn chỉ nói rằng sử dụng DI là dễ dàng.

Vấn đề mà Guice giải quyết là để sử dụng hàm tạo đó, bây giờ bạn phải có mã xây dựng biểu đồ đối tượng ở đâu đó , chuyển thủ công các đối tượng đã được khởi tạo làm đối số của hàm tạo đó. Guice cho phép bạn có một nơi duy nhất nơi bạn có thể định cấu hình các lớp triển khai thực sự tương ứng với các lớp CreditCardProcessorTransactionLoggiao diện đó. Sau cấu hình đó, mỗi khi bạn tạoBillingService bằng Guice, các lớp đó sẽ tự động được chuyển đến hàm tạo.

Đây là những gì khuôn khổ tiêm phụ thuộc làm. Nhưng bản thân hàm tạo mà bạn đã trình bày đã là một triển khai của nguyên tắc tiêm khử . Các thùng chứa IoC và khung DI là phương tiện để tự động hóa các nguyên tắc tương ứng nhưng không có gì ngăn cản bạn làm mọi thứ bằng tay, đó là toàn bộ vấn đề.

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.