Chỉ trích và nhược điểm của tiêm phụ thuộc


118

Phụ thuộc tiêm (DI) là một mô hình nổi tiếng và thời trang. Hầu hết các kỹ sư biết lợi thế của nó, như:

  • Làm cho sự cô lập trong kiểm tra đơn vị có thể / dễ dàng
  • Xác định rõ ràng sự phụ thuộc của một lớp
  • Tạo điều kiện thiết kế tốt ( ví dụ nguyên tắc trách nhiệm (SRP))
  • Cho phép thực hiện chuyển đổi nhanh chóng ( DbLoggerthay vì ConsoleLoggerví dụ)

Tôi cho rằng có sự đồng thuận rộng rãi trong ngành rằng DI là một mô hình tốt, hữu ích. Không có quá nhiều chỉ trích vào lúc này. Nhược điểm được đề cập trong cộng đồng thường là nhỏ. Vài người trong số họ:

  • Số lượng lớp học tăng lên
  • Tạo các giao diện không cần thiết

Hiện tại chúng tôi thảo luận về thiết kế kiến ​​trúc với đồng nghiệp của tôi. Anh ấy khá bảo thủ, nhưng cởi mở. Anh ấy thích đặt câu hỏi, điều mà tôi cho là tốt, bởi vì nhiều người trong ngành CNTT chỉ sao chép xu hướng mới nhất, lặp lại những lợi thế và nói chung đừng suy nghĩ quá nhiều - đừng phân tích quá sâu.

Những điều tôi muốn hỏi là:

  • Chúng ta có nên sử dụng tiêm phụ thuộc khi chúng ta chỉ có một thực hiện?
  • Chúng ta có nên cấm tạo các đối tượng mới ngoại trừ các đối tượng ngôn ngữ / khung không?
  • Có phải việc đưa ra một ý tưởng thực hiện xấu (giả sử chúng ta chỉ có một triển khai nên chúng ta không muốn tạo giao diện "trống") nếu chúng ta không có kế hoạch kiểm tra đơn vị một lớp cụ thể?

33
Bạn có thực sự hỏi về việc tiêm depenency như một mô hình, hoặc bạn đang hỏi về việc sử dụng các khung DI? Đây là những điều thực sự khác biệt, bạn nên làm rõ phần nào của vấn đề bạn quan tâm, hoặc hỏi rõ ràng về cả hai.
Frax

10
@Frax về mẫu, không phải khung
Landeeyo

10
Bạn đang nhầm lẫn nghịch đảo phụ thuộc với tiêm phụ thuộc. Các cựu là một nguyên tắc thiết kế. Cái sau là một kỹ thuật (thường được thực hiện với một công cụ hiện có) để xây dựng hệ thống phân cấp của các đối tượng.
jpmc26

3
Tôi thường viết các bài kiểm tra bằng cơ sở dữ liệu thực và không có đối tượng giả nào cả. Hoạt động thực sự tốt trong nhiều trường hợp. Và sau đó bạn không cần giao diện hầu hết thời gian. Nếu bạn có một UserServicelớp đó chỉ là một người nắm giữ logic. Nó được tiêm một kết nối cơ sở dữ liệu và các kiểm tra chạy bên trong một giao dịch được khôi phục. Nhiều người sẽ gọi đây là thực hành tồi nhưng tôi thấy rằng nó hoạt động rất tốt. Không cần phải sửa mã của bạn chỉ để thử nghiệm và bạn sẽ nhận được lỗi tìm kiếm sức mạnh của các thử nghiệm tích hợp.
usr

3
DI gần như luôn luôn tốt. Điều tồi tệ với nó là rất nhiều người nghĩ rằng họ biết DI nhưng tất cả những gì họ biết là làm thế nào để sử dụng một khuôn khổ kỳ lạ nào đó mà không chắc chắn về những gì họ đang làm. DI hiện nay chịu đựng rất nhiều từ lập trình Cargo Cult.
T. Sar

Câu trả lời:


160

Đầu tiên, tôi muốn tách biệt phương pháp thiết kế khỏi khái niệm khung. Tiêm phụ thuộc ở cấp độ đơn giản và cơ bản nhất của nó chỉ đơn giản là:

Một đối tượng cha cung cấp tất cả các phụ thuộc cần thiết cho đối tượng con.

Đó là nó. Lưu ý rằng không có gì trong đó yêu cầu giao diện, khung, bất kỳ kiểu tiêm nào, v.v. Để công bằng, lần đầu tiên tôi đã tìm hiểu về mẫu này 20 năm trước. Nó không phải là mới.

Do có hơn 2 người nhầm lẫn giữa thuật ngữ cha mẹ và con cái, trong bối cảnh tiêm phụ thuộc:

  • Các mẹ là đối tượng mà instantiates và cấu hình các đối tượng trẻ em nó sử dụng
  • Các con là thành phần được thiết kế để được khởi tạo một cách thụ động. Tức là nó được thiết kế để sử dụng bất kỳ phụ thuộc nào được cung cấp bởi cha mẹ và không khởi tạo các phụ thuộc của chính nó.

Phụ thuộc tiêm là một mô hình cho thành phần đối tượng .

Tại sao giao diện?

Giao diện là một hợp đồng. Chúng tồn tại để hạn chế mức độ kết hợp chặt chẽ giữa hai đối tượng. Không phải mọi phụ thuộc đều cần một giao diện, nhưng chúng giúp viết mã mô-đun.

Khi bạn thêm vào khái niệm kiểm thử đơn vị, bạn có thể có hai triển khai khái niệm cho bất kỳ giao diện cụ thể nào: đối tượng thực bạn muốn sử dụng trong ứng dụng của mình và đối tượng bị nhạo báng hoặc sơ khai mà bạn sử dụng để kiểm tra mã phụ thuộc vào đối tượng. Điều đó một mình có thể đủ biện minh cho giao diện.

Tại sao khung?

Về cơ bản, việc khởi tạo và cung cấp các phụ thuộc cho các đối tượng con có thể gây nản chí khi có một số lượng lớn chúng. Các khung cung cấp các lợi ích sau:

  • Tự động phụ thuộc vào các thành phần
  • Cấu hình các thành phần với các thiết lập của một số loại
  • Tự động hóa mã tấm nồi hơi để bạn không phải nhìn thấy nó được viết ở nhiều vị trí.

Họ cũng có những nhược điểm sau:

  • Đối tượng cha là một "thùng chứa" và không có gì trong mã của bạn
  • Nó làm cho việc kiểm tra trở nên phức tạp hơn nếu bạn không thể cung cấp các phụ thuộc trực tiếp trong mã kiểm tra của mình
  • Nó có thể làm chậm quá trình khởi tạo vì nó giải quyết tất cả các phụ thuộc bằng cách sử dụng sự phản chiếu và nhiều thủ thuật khác
  • Việc gỡ lỗi thời gian chạy có thể khó khăn hơn, đặc biệt nếu container chứa proxy giữa giao diện và thành phần thực tế thực hiện giao diện (lập trình hướng theo khía cạnh được tích hợp trong Spring). Hộp chứa là một hộp đen và chúng không phải luôn được chế tạo với bất kỳ khái niệm nào tạo thuận lợi cho quá trình gỡ lỗi.

Tất cả những gì đã nói, có sự đánh đổi. Đối với các dự án nhỏ, nơi không có nhiều bộ phận chuyển động và có ít lý do để sử dụng khung DI. Tuy nhiên, đối với các dự án phức tạp hơn, nơi đã có một số thành phần nhất định được thực hiện cho bạn, khung có thể được biện minh.

Còn [bài viết ngẫu nhiên trên Internet] thì sao?

Còn nó thì sao? Nhiều lần mọi người có thể trở nên quá nhiệt tình và thêm một loạt các hạn chế và đánh cược bạn nếu bạn không làm mọi thứ theo "một cách thực sự". Không có một cách đúng. Xem nếu bạn có thể trích xuất bất cứ điều gì hữu ích từ bài viết và bỏ qua những thứ bạn không đồng ý.

Tóm lại, hãy suy nghĩ cho bản thân và thử mọi thứ.

Làm việc với "những cái đầu cũ"

Tìm hiểu càng nhiều càng tốt. Những gì bạn sẽ tìm thấy với rất nhiều nhà phát triển đang làm việc ở độ tuổi 70 là họ đã học được cách không giáo điều về rất nhiều thứ. Họ có những phương pháp mà họ đã làm việc trong nhiều thập kỷ để tạo ra kết quả chính xác.

Tôi đã có đặc quyền làm việc với một vài trong số này, và họ có thể cung cấp một số phản hồi trung thực một cách tàn nhẫn có nhiều ý nghĩa. Và nơi họ thấy giá trị, họ thêm những công cụ đó vào tiết mục của mình.


6
@CarlLeth, tôi đã làm việc với một số khung từ các biến thể của Spring đến .net. Spring sẽ cho phép bạn đưa các triển khai ngay vào các trường riêng bằng cách sử dụng một số phép thuật đen phản chiếu / trình nạp lớp. Cách duy nhất để kiểm tra các thành phần được xây dựng như thế là sử dụng container. Spring không có người chạy JUnit để định cấu hình môi trường thử nghiệm, nhưng nó phức tạp hơn việc tự thiết lập mọi thứ. Vì vậy, có, tôi chỉ đưa ra một ví dụ thực tế .
Berin Loritsch

17
Có một nhược điểm nữa mà tôi thấy là trở ngại với DI thông qua các khung khi tôi đội chiếc mũ bảo trì / khắc phục sự cố của mình: hành động ma quái ở khoảng cách mà chúng cung cấp khiến việc gỡ lỗi ngoại tuyến trở nên khó khăn hơn. Trong trường hợp xấu nhất, tôi phải chạy mã để xem cách phụ thuộc được khởi tạo và chuyển vào. Bạn đề cập đến điều này trong ngữ cảnh "thử nghiệm", nhưng thực sự tồi tệ hơn nhiều nếu bạn chỉ bắt đầu nhìn vào nguồn, không bao giờ cố gắng để làm cho nó chạy (có thể liên quan đến một tấn thiết lập). Áp dụng khả năng của tôi để nói mã nào làm được bằng cách chỉ nhìn vào nó là một điều xấu.
Jeroen Mostert

1
Các giao diện không phải là hợp đồng, chúng chỉ đơn giản là các API. Hợp đồng ngụ ý ngữ nghĩa. Câu trả lời này là sử dụng thuật ngữ cụ thể về ngôn ngữ và các quy ước cụ thể về Java / C #.
Frank Hileman

2
@BerinLoritsch Điểm chính của câu trả lời của bạn là nguyên tắc DI! = Bất kỳ khung DI nào. Việc Spring có thể làm những điều khủng khiếp, không thể tha thứ là một bất lợi của Spring, không phải của các khuôn khổ DI nói chung. Một khung DI tốt giúp bạn tuân theo nguyên tắc DI mà không cần các thủ thuật khó chịu.
Carl Leth

1
@CarlLeth: tất cả các khung DI được thiết kế để loại bỏ hoặc tự động hóa một số thứ mà lập trình viên không muốn đánh vần, chúng chỉ khác nhau về cách thức. Theo hiểu biết tốt nhất của tôi, tất cả họ đều loại bỏ khả năng biết cách (hoặc nếu ) lớp A và B tương tác bằng cách chỉ nhìn vào A và B - ít nhất, bạn cũng cần xem xét thiết lập / cấu hình DI / quy ước. Không phải là vấn đề đối với lập trình viên (thực ra chính xác là những gì họ muốn), nhưng thực sự là một vấn đề tiềm ẩn đối với người bảo trì / gỡ lỗi (có thể là cùng một lập trình viên, sau này). Đây là một sự đánh đổi mà bạn thực hiện ngay cả khi khung DI của bạn là "hoàn hảo".
Jeroen Mostert

88

Tiêm phụ thuộc, giống như hầu hết các mẫu, là một giải pháp cho các vấn đề . Vì vậy, bắt đầu bằng cách hỏi nếu bạn thậm chí có vấn đề ở nơi đầu tiên. Nếu không, sau đó sử dụng mẫu rất có thể sẽ làm cho mã tồi tệ hơn .

Hãy xem xét đầu tiên nếu bạn có thể giảm hoặc loại bỏ sự phụ thuộc. Tất cả những thứ khác đều bằng nhau, chúng tôi muốn mỗi thành phần trong một hệ thống có càng ít phụ thuộc càng tốt. Và nếu sự phụ thuộc không còn nữa, câu hỏi tiêm hay không trở thành tranh luận!

Xem xét một mô-đun tải xuống một số dữ liệu từ một dịch vụ bên ngoài, phân tích cú pháp và thực hiện một số phân tích phức tạp và ghi vào kết quả vào một tệp.

Bây giờ, nếu sự phụ thuộc vào dịch vụ bên ngoài được mã hóa cứng, thì sẽ rất khó để kiểm tra đơn vị xử lý nội bộ của mô-đun này. Vì vậy, bạn có thể quyết định tiêm dịch vụ bên ngoài và hệ thống tệp dưới dạng phụ thuộc giao diện, điều này sẽ cho phép bạn tiêm giả thay vào đó, điều này giúp kiểm tra đơn vị logic bên trong có thể.

Nhưng một giải pháp tốt hơn nhiều chỉ đơn giản là tách phân tích khỏi đầu vào / đầu ra. Nếu phân tích được trích xuất thành một mô-đun mà không có tác dụng phụ, nó sẽ dễ dàng hơn để kiểm tra. Lưu ý rằng chế nhạo là một mùi mã - không phải lúc nào cũng có thể tránh được, nhưng nói chung, sẽ tốt hơn nếu bạn có thể kiểm tra mà không cần dựa vào chế độ chế nhạo. Vì vậy, bằng cách loại bỏ các phụ thuộc, bạn tránh được các vấn đề mà DI được cho là sẽ giảm bớt. Lưu ý rằng thiết kế như vậy cũng tuân thủ SRP tốt hơn nhiều.

Tôi muốn nhấn mạnh rằng DI không nhất thiết tạo điều kiện thuận lợi cho SRP hoặc các nguyên tắc thiết kế tốt khác như tách biệt mối quan tâm, độ gắn kết cao / khớp nối thấp, v.v. Nó cũng có thể có tác dụng ngược lại. Hãy xem xét một lớp A sử dụng một lớp B khác trong nội bộ. B chỉ được sử dụng bởi A và do đó được đóng gói đầy đủ và có thể được coi là một chi tiết thực hiện. Nếu bạn thay đổi điều này để tiêm B vào hàm tạo của A, thì bạn đã tiết lộ chi tiết triển khai này và bây giờ có kiến ​​thức về sự phụ thuộc này và về cách khởi tạo B, thời gian tồn tại của B và phải tồn tại một vị trí khác trong hệ thống từ A. Vì vậy, bạn có một kiến ​​trúc tổng thể tồi tệ hơn với những lo ngại rò rỉ.

Mặt khác, có một số trường hợp DI thực sự hữu ích. Ví dụ cho các dịch vụ toàn cầu với các tác dụng phụ như logger.

Vấn đề là khi các mẫu và kiến ​​trúc trở thành mục tiêu trong chính chúng chứ không phải là công cụ. Chỉ cần hỏi "Chúng ta có nên sử dụng DI?" là loại đặt xe trước ngựa. Bạn nên hỏi: "Chúng ta có vấn đề à?" và "giải pháp tốt nhất cho vấn đề này là gì?"

Một phần câu hỏi của bạn tập trung vào: "Chúng ta có nên tạo giao diện thừa để đáp ứng nhu cầu của mẫu không?" Bạn có thể đã nhận ra câu trả lời cho điều này - hoàn toàn không ! Bất cứ ai nói với bạn nếu không đang cố gắng bán cho bạn một cái gì đó - rất có thể là giờ tư vấn đắt tiền. Một giao diện chỉ có giá trị nếu nó đại diện cho một sự trừu tượng. Một giao diện chỉ bắt chước bề mặt của một lớp duy nhất được gọi là "giao diện tiêu đề" và đây là một phản mẫu đã biết.


15
Tôi không thể đồng ý nhiều hơn! Cũng lưu ý rằng chế giễu mọi thứ vì lợi ích của nó có nghĩa là chúng tôi không thực sự thử nghiệm các triển khai thực sự. Nếu Asử dụng Btrong sản xuất, nhưng chỉ được thử nghiệm MockB, các thử nghiệm của chúng tôi không cho chúng tôi biết nếu nó sẽ hoạt động trong sản xuất. Khi các thành phần thuần túy (không có tác dụng phụ) của mô hình miền đang tiêm và chế nhạo lẫn nhau, kết quả là sự lãng phí rất lớn thời gian của mọi người, một cơ sở mã hóa mỏng manh và dễ vỡ và hệ thống kết quả thấp. Giả định tại các ranh giới của hệ thống, không phải giữa các phần tùy ý của cùng một hệ thống.
Warbo

17
@CarlLeth Tại sao bạn nghĩ DI làm cho mã "có thể kiểm tra và duy trì được" và mã mà không có ít hơn? JacquesB đã đúng rằng các tác dụng phụ là điều gây hại cho khả năng kiểm tra / duy trì. Nếu mã không có tác dụng phụ, chúng tôi không quan tâm cái gì / ở đâu / khi nào / cách nó gọi mã khác; chúng ta có thể giữ nó đơn giản và trực tiếp. Nếu mã có tác dụng phụ chúng ta phải quan tâm. DI có thể kéo các hiệu ứng phụ ra khỏi các chức năng và đưa nó vào các tham số, làm cho các chức năng đó dễ kiểm tra hơn nhưng chương trình phức tạp hơn. Đôi khi điều đó không thể tránh khỏi (ví dụ: truy cập DB). Nếu mã không có tác dụng phụ, DI chỉ là sự phức tạp vô dụng.
Warbo

13
@CarlLeth: DI là một giải pháp cho vấn đề làm cho mã có thể kiểm tra được một cách cô lập nếu nó có các phụ thuộc cấm nó. Nhưng nó không làm giảm độ phức tạp tổng thể, cũng như không làm cho mã dễ đọc hơn, điều đó có nghĩa là nó không nhất thiết phải tăng khả năng bảo trì. Tuy nhiên, nếu tất cả những sự phụ thuộc đó có thể được loại bỏ bằng cách phân tách mối quan tâm tốt hơn, thì điều này hoàn toàn "vô hiệu hóa" lợi ích của DI, bởi vì nó không đáp ứng nhu cầu về DI. Đây thường là một giải pháp tốt hơn để làm cho mã dễ kiểm tra hơn có thể duy trì cùng một lúc.
Doc Brown

5
@Warbo Đây là bản gốc và có lẽ vẫn là cách sử dụng giả duy nhất hợp lệ. Ngay cả ở ranh giới hệ thống, nó hiếm khi cần thiết. Mọi người thực sự lãng phí nhiều thời gian để tạo và cập nhật các bài kiểm tra gần như vô giá trị.
Frank Hileman

6
@CarlLeth: ok, bây giờ tôi thấy sự hiểu lầm đến từ đâu. Bạn đang nói về nghịch đảo phụ thuộc . Nhưng, câu hỏi, câu trả lời này và ý kiến ​​của tôi là về DI = depency tiêm .
Doc Brown

36

Theo kinh nghiệm của tôi, có một số nhược điểm đối với việc tiêm phụ thuộc.

Đầu tiên, sử dụng DI không đơn giản hóa việc kiểm tra tự động nhiều như quảng cáo. Đơn vị kiểm tra một lớp với việc thực hiện giao diện giả cho phép bạn xác thực cách lớp đó sẽ tương tác với giao diện. Đó là, nó cho phép bạn kiểm tra đơn vị cách lớp đang kiểm tra sử dụng hợp đồng được cung cấp bởi giao diện. Tuy nhiên, điều này cung cấp sự đảm bảo lớn hơn nhiều rằng đầu vào từ lớp được kiểm tra vào giao diện như mong đợi. Nó cung cấp sự đảm bảo khá kém rằng lớp được kiểm tra đáp ứng như mong đợi để xuất ra từ giao diện vì đó là đầu ra giả gần như toàn cầu, bản thân nó có lỗi, quá đơn giản, v.v. Nói tóm lại, nó KHÔNG cho phép bạn xác nhận rằng lớp sẽ hoạt động như mong đợi với việc triển khai thực sự giao diện.

Thứ hai, DI làm cho việc điều hướng qua mã khó khăn hơn nhiều. Khi cố gắng điều hướng đến định nghĩa của các lớp được sử dụng làm đầu vào cho các hàm, giao diện có thể là bất cứ điều gì từ một sự phiền toái nhỏ (ví dụ khi có một triển khai duy nhất) đến một khoảng thời gian chính (ví dụ: khi sử dụng giao diện quá chung chung như IDis Dùng) khi cố gắng tìm việc thực hiện đang được sử dụng. Điều này có thể biến một bài tập đơn giản như "Tôi cần sửa một ngoại lệ tham chiếu null trong mã xảy ra ngay sau khi câu lệnh ghi nhật ký này được in" thành một nỗ lực dài cả ngày.

Thứ ba, việc sử dụng DI và khung là con dao hai lưỡi. Nó có thể làm giảm đáng kể số lượng mã nồi hơi cần thiết cho các hoạt động chung. Tuy nhiên, điều này phải trả giá khi cần có kiến ​​thức chi tiết về khung DI cụ thể để hiểu làm thế nào các hoạt động chung này thực sự được nối với nhau. Hiểu cách phụ thuộc được tải vào khung và thêm một phụ thuộc mới vào khung để tiêm có thể yêu cầu đọc một số lượng lớn tài liệu nền và làm theo một số hướng dẫn cơ bản về khung. Điều này có thể biến một số nhiệm vụ đơn giản thành những việc khá tốn thời gian.


Tôi cũng sẽ nói thêm rằng bạn càng tiêm nhiều thì thời gian khởi động của bạn sẽ càng kéo dài. Hầu hết các khung DI tạo ra tất cả các trường hợp đơn lẻ có thể tiêm vào lúc khởi động, bất kể chúng được sử dụng ở đâu.
Rodney P. Barbati

7
Nếu bạn muốn kiểm tra một lớp với triển khai thực tế (không phải là giả), bạn có thể viết các bài kiểm tra chức năng - các bài kiểm tra tương tự như bài kiểm tra đơn vị, nhưng không sử dụng giả.
BЈовић

2
Tôi nghĩ rằng đoạn thứ hai của bạn cần phải có nhiều sắc thái hơn: Bản thân DI không làm cho nó khó (er) để điều hướng qua mã. Đơn giản nhất, DI chỉ đơn giản là hệ quả của việc tuân theo RẮN. Điều làm tăng sự phức tạp là việc sử dụng các khung cảm ứngDI không cần thiết . Ngoài ra, câu trả lời này đánh vào đầu đinh.
Konrad Rudolph

4
Tiêm phụ thuộc, bên ngoài các trường hợp thực sự cần thiết, cũng là một dấu hiệu cảnh báo rằng các mã thừa khác có thể có mặt rất nhiều. Các nhà điều hành thường ngạc nhiên khi biết rằng các nhà phát triển thêm sự phức tạp vì sự phức tạp.
Frank Hileman

1
+1. Đây là câu trả lời thực sự cho câu hỏi đã được hỏi và nên là câu trả lời được chấp nhận.
Mason Wheeler

13

Tôi đã làm theo lời khuyên của Mark Seemann từ "Dependency tiêm trong .NET" - để phỏng đoán.

DI nên được sử dụng khi bạn có 'sự phụ thuộc dễ bay hơi', ví dụ như có khả năng hợp lý nó có thể thay đổi.

Vì vậy, nếu bạn nghĩ rằng bạn có thể có nhiều hơn một triển khai trong tương lai hoặc việc triển khai có thể thay đổi, hãy sử dụng DI. Nếu không thì newtốt.


5
Lưu ý anh ấy cũng đưa ra lời khuyên khác nhau cho OO và ngôn ngữ chức năng blog.ploeh.dk/2017/01/27/ trên
jk.

1
Đó là điểm tốt. Nếu chúng ta tạo giao diện cho mọi phụ thuộc theo mặc định thì bằng cách nào đó nó chống lại YAGNI.
Landeeyo

4
Bạn có thể cung cấp một tài liệu tham khảo cho "Dependency tiêm trong .NET" không?
Peter Mortensen

1
Nếu bạn đang thử nghiệm đơn vị, thì rất có khả năng sự phụ thuộc của bạn không ổn định.
Jaquez

11
Điều tốt là, các nhà phát triển luôn có thể dự đoán tương lai với độ chính xác hoàn hảo.
Frank Hileman

5

Thú cưng lớn nhất của tôi về DI đã được đề cập trong một vài câu trả lời theo cách thông qua, nhưng tôi sẽ mở rộng về nó một chút ở đây. DI (vì nó hầu hết được thực hiện ngày hôm nay, với các thùng chứa, v.v.) thực sự, THỰC SỰ làm tổn thương khả năng đọc mã. Và khả năng đọc mã được cho là lý do đằng sau hầu hết các đổi mới lập trình ngày nay. Như ai đó đã nói - viết mã rất dễ dàng. Đọc mã là khó. Nhưng nó cũng cực kỳ quan trọng, trừ khi bạn đang viết một loại tiện ích vứt đi một lần nhỏ xíu.

Vấn đề với DI về vấn đề này là nó mờ đục. Hộp đựng là một hộp đen. Các đối tượng chỉ đơn giản xuất hiện từ đâu đó và bạn không biết - ai đã xây dựng chúng và khi nào? Những gì đã được chuyển đến các nhà xây dựng? Tôi đang chia sẻ trường hợp này với ai? Ai biết...

Và khi bạn làm việc chủ yếu với các giao diện, tất cả các tính năng "đi đến định nghĩa" của IDE của bạn sẽ tan thành mây khói. Thật khó khăn để tìm ra dòng chảy của chương trình mà không chạy nó và chỉ cần bước qua để xem việc triển khai giao diện đã được sử dụng ở vị trí cụ thể NÀY. Và đôi khi có một số trở ngại kỹ thuật ngăn cản bạn bước qua. Và ngay cả khi bạn có thể, nếu nó liên quan đến việc đi qua các cung xoắn của container DI, toàn bộ sự việc nhanh chóng trở thành một bài tập trong sự thất vọng.

Để làm việc hiệu quả với một đoạn mã đã sử dụng DI, bạn phải làm quen với nó và đã biết những gì đi đâu.


3

Kích hoạt triển khai chuyển đổi nhanh chóng (ví dụ DbLogger thay vì ConsoleLogger)

Mặc dù DI nói chung chắc chắn là một điều tốt, tôi khuyên bạn không nên sử dụng nó một cách mù quáng cho mọi thứ. Ví dụ, tôi không bao giờ tiêm loggers. Một trong những lợi thế của DI là làm cho các phụ thuộc rõ ràng và rõ ràng. Không có điểm nào trong danh sách ILoggerlà sự phụ thuộc của gần như mọi lớp - đó chỉ là sự lộn xộn. Trách nhiệm của logger là cung cấp sự linh hoạt mà bạn cần. Tất cả các logger của tôi là thành viên tĩnh cuối cùng, tôi có thể xem xét việc tiêm logger khi tôi cần một logger không tĩnh.

Số lượng lớp học tăng lên

Đây là một bất lợi của khung DI hoặc khung mô phỏng đã cho, không phải của chính DI. Ở hầu hết các nơi, các lớp học của tôi phụ thuộc vào các lớp cụ thể, điều đó có nghĩa là không cần nồi hơi. Guice (khung công tác Java DI) liên kết theo mặc định một lớp với chính nó và tôi chỉ cần ghi đè liên kết trong các bài kiểm tra (hoặc thay thế chúng bằng tay thay thế).

Tạo các giao diện không cần thiết

Tôi chỉ tạo các giao diện khi cần thiết (điều này khá hiếm). Điều này có nghĩa là đôi khi, tôi phải thay thế tất cả các lần xuất hiện của một lớp bằng một giao diện, nhưng IDE có thể làm điều này cho tôi.

Chúng ta có nên sử dụng tiêm phụ thuộc khi chúng ta chỉ có một thực hiện?

Có, nhưng tránh bất kỳ nồi hơi được thêm vào .

Chúng ta có nên cấm tạo các đối tượng mới ngoại trừ các đối tượng ngôn ngữ / khung không?

Không. Sẽ có nhiều lớp giá trị (không thay đổi) và dữ liệu (có thể thay đổi), trong đó các thể hiện vừa được tạo và chuyển qua và không có điểm nào để tiêm chúng - vì chúng không bao giờ được lưu trữ trong một đối tượng khác (hoặc chỉ trong các đối tượng khác những đối tượng như vậy).

Đối với họ, bạn có thể cần phải tiêm một nhà máy thay thế, nhưng hầu hết thời gian nó không có ý nghĩa gì (hãy tưởng tượng, @Value class NamedUrl {private final String name; private final URL url;}bạn thực sự không cần một nhà máy ở đây và không có gì để tiêm).

Có phải việc đưa ra một ý tưởng thực hiện xấu (giả sử chúng ta chỉ có một triển khai nên chúng ta không muốn tạo giao diện "trống") nếu chúng ta không có kế hoạch kiểm tra đơn vị một lớp cụ thể?

IMHO nó ổn miễn là nó không gây ra sự phình to mã. Đừng phụ thuộc nhưng không tạo giao diện (và không có cấu hình XML điên rồ!) Vì bạn có thể làm điều đó sau mà không gặp rắc rối nào.

Trên thực tế, trong dự án hiện tại của tôi, có bốn lớp (trong số hàng trăm), tôi quyết định loại trừ khỏi DI vì chúng là các lớp đơn giản được sử dụng ở quá nhiều nơi, bao gồm các đối tượng dữ liệu.


Một nhược điểm khác của hầu hết các khung DI là chi phí thời gian chạy. Điều này có thể được chuyển sang thời gian biên dịch (đối với Java, có Dagger , không biết gì về các ngôn ngữ khác).

Tuy nhiên, một nhược điểm khác là phép thuật xảy ra ở mọi nơi, có thể được điều chỉnh (ví dụ: tôi đã vô hiệu hóa việc tạo proxy khi sử dụng Guice).


-4

Tôi phải nói rằng theo ý kiến ​​của tôi, toàn bộ khái niệm về Dependency Injection được đánh giá cao.

DI là ngày hiện đại tương đương với các giá trị toàn cầu. Những thứ bạn đang tiêm là các singletons toàn cầu và các đối tượng mã thuần túy, nếu không, bạn không thể tiêm chúng. Hầu hết việc sử dụng DI đều bắt buộc bạn phải sử dụng một thư viện nhất định (JPA, Spring Data, v.v.). Đối với hầu hết các phần, DI cung cấp môi trường hoàn hảo để nuôi dưỡng và nuôi dưỡng spaghetti.

Thành thật mà nói, cách dễ nhất để kiểm tra một lớp là đảm bảo rằng tất cả các phụ thuộc được tạo trong một phương thức có thể được ghi đè. Sau đó tạo một lớp Test xuất phát từ lớp thực tế và ghi đè phương thức đó.

Sau đó, bạn khởi tạo lớp Test và kiểm tra tất cả các phương thức của nó. Điều này sẽ không rõ ràng đối với một số bạn - các phương pháp bạn đang kiểm tra là những phương pháp thuộc về lớp đang thử nghiệm. Và tất cả các thử nghiệm phương thức này xảy ra trong một tệp lớp duy nhất - lớp thử nghiệm đơn vị được liên kết với lớp được thử nghiệm. Không có chi phí nào ở đây - đây là cách thử nghiệm đơn vị hoạt động.

Trong mã, khái niệm này trông như thế này ...

class ClassUnderTest {

   protected final ADependency;
   protected final AnotherDependency;

   // Call from a factory or use an initializer 
   public void initializeDependencies() {
      aDependency = new ADependency();
      anotherDependency = new AnotherDependency();
   }
}

class TestClassUnderTest extends ClassUnderTest {

    @Override
    public void initializeDependencies() {
      aDependency = new MockitoObject();
      anotherDependency = new MockitoObject();
    }

    // Unit tests go here...
    // Unit tests call base class methods
}

Kết quả hoàn toàn tương đương với việc sử dụng DI - đó là ClassUnderTest được cấu hình để thử nghiệm.

Sự khác biệt duy nhất là mã này hoàn toàn ngắn gọn, được đóng gói hoàn toàn, dễ viết mã hơn, dễ hiểu hơn, nhanh hơn, sử dụng ít bộ nhớ hơn, không yêu cầu cấu hình thay thế, không yêu cầu bất kỳ khung nào, sẽ không bao giờ là nguyên nhân của 4 trang (WTF!) Dấu vết ngăn xếp bao gồm chính xác các lớp ZERO (0) mà bạn đã viết và hoàn toàn rõ ràng đối với bất kỳ ai có kiến ​​thức OO nhỏ nhất, từ người mới bắt đầu đến bậc thầy (bạn sẽ nghĩ, nhưng sẽ bị nhầm lẫn).

Điều đó đang được nói, tất nhiên chúng ta không thể sử dụng nó - nó quá rõ ràng và không đủ hợp thời trang.

Tuy nhiên, vào cuối ngày, mối quan tâm lớn nhất của tôi với DI là các dự án tôi đã thấy thất bại thảm hại, tất cả chúng đều là những cơ sở mã lớn trong đó DI là chất keo giữ mọi thứ lại với nhau. DI không phải là một kiến ​​trúc - nó thực sự chỉ có liên quan trong một số tình huống, hầu hết trong số đó buộc bạn phải sử dụng một thư viện khác (JPA, Spring Data, v.v.). Đối với hầu hết các phần, trong một cơ sở mã được thiết kế tốt, hầu hết việc sử dụng DI sẽ xảy ra ở cấp độ bên dưới nơi diễn ra các hoạt động phát triển hàng ngày của bạn.


6
Bạn đã mô tả không tương đương, nhưng ngược lại với tiêm phụ thuộc. Trong mô hình của bạn, mọi đối tượng cần biết triển khai cụ thể tất cả các phụ thuộc của nó, nhưng sử dụng DI trở thành trách nhiệm của thành phần "chính" - để gắn kết các triển khai phù hợp với nhau. Hãy nhớ rằng DI đi đôi với DI khác - đảo ngược phụ thuộc, nơi bạn không muốn các thành phần cấp cao có phụ thuộc cứng vào các thành phần cấp thấp.
Pickup Logan

1
Thật gọn gàng khi bạn chỉ có một cấp độ kế thừa lớp và một cấp độ phụ thuộc. Chắc chắn nó sẽ biến thành địa ngục trần gian khi nó mở rộng?
Ewan

4
nếu bạn sử dụng các giao diện và di chuyển initizeDependencies () vào hàm tạo giống nhau nhưng gọn gàng hơn. Bước tiếp theo, thêm các tham số xây dựng có nghĩa là bạn có thể loại bỏ tất cả TestClass của mình.
Ewan

5
Có quá nhiều sai lầm với điều này. Như những người khác đã nói, ví dụ 'DI tương đương' của bạn hoàn toàn không phải là sự phụ thuộc, đó là phản đề và thể hiện sự thiếu hiểu biết hoàn toàn về khái niệm này và cũng đưa ra những cạm bẫy tiềm tàng khác: các đối tượng được khởi tạo một phần là mùi mã như Ewan gợi ý, Di chuyển khởi tạo đến hàm tạo và chuyển chúng qua các tham số của hàm tạo. Sau đó, bạn có DI ...
Mr.Mindor

3
Thêm vào @ Mr.Mindor: có một "khớp nối tiếp theo" chống mẫu tổng quát hơn mà không chỉ áp dụng cho khởi tạo. Nếu các phương thức của một đối tượng (hoặc, nói chung hơn, các lệnh gọi của API) phải được chạy theo một thứ tự cụ thể, ví dụ: barchỉ có thể được gọi sau foo, thì đó là một API xấu. Nó tuyên bố sẽ cung cấp chức năng ( bar), nhưng chúng tôi thực sự không thể sử dụng nó (vì foocó thể không được gọi). Nếu bạn muốn gắn bó với mẫu initializeDependencies(chống?) Của mình, ít nhất bạn nên đặt nó ở chế độ riêng tư / được bảo vệ và gọi nó tự động từ hàm tạo, vì vậy API rất chân thành.
Warbo

-6

Thực sự câu hỏi của bạn sôi sùng sục thành "Thử nghiệm đơn vị có tệ không?"

99% các lớp thay thế của bạn để tiêm sẽ là giả cho phép thử nghiệm đơn vị.

Nếu bạn thực hiện kiểm tra đơn vị mà không có DI, bạn có vấn đề làm thế nào để có được các lớp sử dụng dữ liệu giả định hoặc dịch vụ giả. Hãy nói 'một phần của logic' vì bạn có thể không tách nó ra thành các dịch vụ.

Có nhiều cách khác nhau để làm điều này, nhưng DI là một cách tốt và linh hoạt. Một khi bạn có nó để thử nghiệm, bạn hầu như bị buộc phải sử dụng nó ở mọi nơi, vì bạn cần một số đoạn mã khác, thậm chí nó còn được gọi là 'DI người nghèo', tạo ra các loại cụ thể.

Thật khó để tưởng tượng một nhược điểm tồi tệ đến mức lợi thế của thử nghiệm đơn vị bị áp đảo.


13
Tôi không đồng ý với khẳng định của bạn rằng DI là về thử nghiệm đơn vị. Tạo điều kiện cho thử nghiệm đơn vị chỉ là một trong những lợi ích của DI và được cho là không quan trọng nhất.
Robert Harvey

5
Tôi không đồng ý với tiền đề của bạn rằng thử nghiệm đơn vị và DI rất gần nhau. Bằng cách sử dụng một mock / stub, chúng tôi làm cho bộ thử nghiệm nói dối với chúng tôi nhiều hơn một chút: hệ thống được thử nghiệm sẽ tiến xa hơn từ hệ thống thực. Đó là khách quan xấu. Đôi khi, điều đó vượt trội hơn bởi một mặt trái: các cuộc gọi FS bị chế giễu không yêu cầu dọn dẹp; các yêu cầu HTTP bị chế giễu là nhanh, xác định và hoạt động ngoại tuyến; v.v ... Ngược lại, mỗi khi chúng ta sử dụng một mã hóa cứng newbên trong một phương thức, chúng ta biết rằng cùng một mã đang chạy trong sản xuất đang chạy trong các thử nghiệm.
Warbo

8
Không, đó không phải là đơn vị kiểm tra đơn vị tệ sao?, Đó là việc chế giễu (a) thực sự cần thiết và (b) có đáng để tăng mức độ phức tạp phát sinh không? Đó là một câu hỏi rất khác. Kiểm thử đơn vị không phải là xấu (nghĩa đen là không ai tranh cãi điều này), và nói chung nó hoàn toàn xứng đáng. Nhưng không phải tất cả các thử nghiệm đơn vị đều yêu cầu chế nhạo, và chế độ nhạo báng mang một chi phí đáng kể, nên ít nhất, nó nên được sử dụng một cách thận trọng.
Konrad Rudolph

6
@Ewan Sau bình luận cuối cùng của bạn, tôi không nghĩ chúng ta đồng ý. Tôi đang nói rằng hầu hết các bài kiểm tra đơn vị không cần DI [khung] , vì hầu hết các bài kiểm tra đơn vị không cần chế giễu. Trên thực tế, tôi thậm chí còn sử dụng điều này như một heuristic cho chất lượng mã: nếu hầu hết mã của bạn không thể được kiểm tra đơn vị mà không có các đối tượng DI / giả thì bạn đã viết mã xấu được ghép quá đúng. Hầu hết các mã phải được phân tách cao, trách nhiệm đơn lẻ và mục đích chung và có thể kiểm tra một cách tầm thường trong sự cô lập.
Konrad Rudolph

5
@Ewan Liên kết của bạn cung cấp một định nghĩa tốt về thử nghiệm đơn vị. Theo định nghĩa đó, Orderví dụ của tôi là một bài kiểm tra đơn vị: đó là kiểm tra một phương thức ( totalphương thức Order). Bạn đang phàn nàn rằng đó là mã gọi từ 2 lớp, phản hồi của tôi là ? Chúng tôi không thử nghiệm "2 lớp cùng một lúc", chúng tôi đang thử nghiệm totalphương pháp. Chúng ta không nên quan tâm làm thế nào một phương thức thực hiện công việc của nó: đó là những chi tiết triển khai; kiểm tra chúng gây ra sự mong manh, khớp nối chặt chẽ, v.v. Chúng tôi chỉ quan tâm đến hành vi của phương thức (giá trị trả về và tác dụng phụ), chứ không phải các lớp / mô-đun / thanh ghi CPU / v.v. nó được sử dụng trong quá trình này
Warbo
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.