Phụ thuộc tiêm; thực hành tốt để giảm mã nồi hơi


25

Tôi có một câu hỏi đơn giản và tôi thậm chí không chắc nó có câu trả lời hay không nhưng hãy thử. Tôi đang mã hóa trong C ++ và sử dụng phương thức tiêm phụ thuộc để tránh trạng thái toàn cầu. Điều này hoạt động khá tốt và tôi không thường xuyên chạy trong các hành vi bất ngờ / không xác định.

Tuy nhiên tôi nhận ra rằng, khi dự án của tôi phát triển, tôi đang viết rất nhiều mã mà tôi xem là bản tóm tắt. Tồi tệ hơn: thực tế có nhiều mã soạn sẵn hơn mã thực tế đôi khi khiến nó khó hiểu.

Không có gì đánh bại một ví dụ tốt vì vậy hãy đi:

Tôi có một lớp gọi là TimeFactory tạo các đối tượng Thời gian.

Để biết thêm chi tiết (không chắc là có liên quan): Các đối tượng thời gian khá phức tạp vì Thời gian có thể có các định dạng khác nhau và việc chuyển đổi giữa chúng không phải là tuyến tính, cũng không đơn giản. Mỗi "Thời gian" chứa Trình đồng bộ hóa để xử lý các chuyển đổi và để đảm bảo chúng có cùng một trình đồng bộ hóa, được khởi tạo đúng cách, tôi sử dụng TimeFactory. TimeFactory chỉ có một phiên bản và có phạm vi ứng dụng rộng rãi, vì vậy nó sẽ đủ điều kiện cho singleton nhưng vì nó có thể thay đổi, tôi không muốn biến nó thành một singleton

Trong ứng dụng của tôi, rất nhiều lớp cần tạo các đối tượng Time. Đôi khi những lớp học được lồng sâu.

Giả sử tôi có một lớp A chứa các thể hiện của lớp B, và cứ thế lên đến lớp D. Lớp D cần tạo các đối tượng Thời gian.

Trong triển khai ngây thơ của mình, tôi chuyển TimeFactory cho hàm tạo của lớp A, nó chuyển nó đến hàm tạo của lớp B và cứ thế cho đến lớp D.

Bây giờ, hãy tưởng tượng tôi có một vài lớp như TimeFactory và một vài hệ thống phân cấp lớp như ở trên: Tôi mất tất cả tính linh hoạt và dễ đọc mà tôi cho là có thể sử dụng tiêm phụ thuộc.

Tôi bắt đầu tự hỏi liệu không có một lỗi thiết kế lớn nào trong ứng dụng của mình ... Hay đây có phải là một điều ác cần thiết khi sử dụng thuốc tiêm phụ thuộc?

Bạn nghĩ sao ?


5
Tôi đã đăng câu hỏi này cho các lập trình viên thay vì stackoverflow bởi vì, theo tôi hiểu, các lập trình viên dành nhiều hơn cho các nhiệm vụ liên quan đến thiết kế và stackoverflow dành cho "khi bạn ở trước trình biên dịch của bạn". Xin lỗi trước nếu tôi sai.
Dinaiz

2
lập trình hướng theo khía cạnh, cũng tốt cho nhiệm vụ này - en.wikipedia.org/wiki/Aspect-oriented_programming
EL Yusubov

Là nhà máy hạng A cho các lớp B, C và D? Nếu không tại sao nó tạo ra các trường hợp của họ? Nếu chỉ có lớp D cần các đối tượng Thời gian, tại sao bạn lại tiêm bất kỳ lớp nào khác bằng TimeFactory? Lớp D có thực sự cần TimeFactory không, hay chỉ có thể là một thể hiện của Time được đưa vào nó?
simoraman

D cần tạo các đối tượng Thời gian, với thông tin chỉ có D là phải có. Tôi phải suy nghĩ về phần còn lại của bình luận của bạn. Có thể có một vấn đề trong "ai tạo ra ai" trong mã của tôi
Dinaiz

"Ai tạo ra ai" được giải quyết bằng các bộ chứa IoC, xem chi tiết câu trả lời của tôi .. "ai tạo ra ai" là một trong những câu hỏi hay nhất bạn có thể hỏi khi sử dụng Dependency Injection.
Nhà phát triển Game

Câu trả lời:


23

Trong ứng dụng của tôi, rất nhiều lớp cần tạo các đối tượng Time

Có vẻ như Timelớp của bạn là một kiểu dữ liệu rất cơ bản thuộc về "cơ sở hạ tầng chung" của ứng dụng của bạn. DI không hoạt động tốt cho các lớp như vậy. Hãy suy nghĩ về ý nghĩa của nó nếu một lớp giống như stringphải được đưa vào mọi phần của mã sử dụng các chuỗi và bạn sẽ cần sử dụng stringFactorynhư là khả năng duy nhất của việc tạo các chuỗi mới - khả năng đọc của chương trình của bạn sẽ giảm theo thứ tự cường độ.

Vì vậy, đề nghị của tôi: không sử dụng DI cho các kiểu dữ liệu chung như Time. Viết các bài kiểm tra đơn vị cho Timechính lớp đó và khi hoàn thành, sử dụng nó ở mọi nơi trong chương trình của bạn, giống như stringlớp hoặc một vectorlớp hoặc bất kỳ lớp nào khác của lib chuẩn. Sử dụng DI cho các thành phần nên thực sự tách rời nhau.


Bạn đã hoàn toàn đúng. "khả năng đọc của chương trình của bạn sẽ giảm theo một mức độ lớn." : đó chính xác là những gì đã xảy ra Với điều kiện tôi vẫn muốn sử dụng một nhà máy, điều đó không tệ để biến nó thành một đơn vị?
Dinaiz

3
@Dinaiz: ý tưởng là chấp nhận sự kết hợp chặt chẽ giữa Timevà các phần khác trong chương trình của bạn. Vì vậy, bạn có thể chấp nhận cũng chấp nhận khớp nối chặt chẽ để TimeFactory. Tuy nhiên, điều tôi sẽ tránh là có một TimeFactoryđối tượng toàn cầu duy nhất có trạng thái (ví dụ: thông tin ngôn ngữ hoặc một cái gì đó tương tự) - có thể gây ra tác dụng phụ khó chịu cho chương trình của bạn và làm cho việc kiểm tra và sử dụng lại rất khó khăn. Hoặc làm cho nó không trạng thái, hoặc không sử dụng nó như là một singleton.
Doc Brown

Trên thực tế nó phải có một trạng thái, vì vậy khi bạn nói "Đừng sử dụng nó như một người độc thân", bạn có nghĩa là "tiếp tục sử dụng phép tiêm phụ thuộc, tức là truyền ví dụ cho mọi đối tượng cần nó", phải không?
Dinaiz

1
@Dinaiz: thành thật mà nói, điều đó phụ thuộc - vào loại trạng thái, loại và số phần của chương trình của bạn sử dụng TimeTimeFactory, ở mức độ "khả năng phát triển" mà bạn cần cho các tiện ích mở rộng trong tương lai TimeFactory, v.v.
Doc Brown

OK Tôi sẽ cố gắng tiếp tục tiêm phụ thuộc nhưng có một thiết kế thông minh hơn mà không cần quá nhiều nồi hơi sau đó! cảm ơn rất nhiều doc ​​'
Dinaiz

4

Ý bạn là gì khi "Tôi mất tất cả tính linh hoạt và dễ đọc Tôi cho rằng sẽ sử dụng tiêm phụ thuộc" - DI không nói về khả năng đọc. Đó là về việc tách rời sự phụ thuộc giữa các đối tượng.

Có vẻ như bạn có Lớp A tạo Lớp B, Lớp B tạo Lớp C và Lớp C tạo Lớp D.

Những gì bạn nên có là Lớp B được tiêm vào Lớp A. Lớp C được tiêm vào Lớp B. Lớp D được tiêm vào Lớp C.


DI có thể là về khả năng đọc. Nếu bạn có một biến toàn cục "kỳ diệu" xuất hiện ở giữa một phương thức, điều đó không giúp hiểu được mã (từ quan điểm bảo trì). Biến này đến từ đâu? Ai sở hữu nó ? Ai khởi tạo nó? Và cứ thế ... Đối với quan điểm thứ hai của bạn, có lẽ bạn đúng nhưng tôi không nghĩ nó áp dụng trong trường hợp của tôi. Cảm ơn bạn đã dành thời gian trả lời
Dinaiz

DI tăng khả năng đọc chủ yếu vì "mới / xóa" sẽ bị loại bỏ khỏi mã của bạn một cách kỳ diệu. Nếu bạn không phải cảnh báo về mã phân bổ động thì dễ đọc hơn.
Nhà phát triển Game

Cũng quên nói rằng điều này làm cho mã mạnh hơn một chút vì không thể đưa ra các giả định về "cách" một phụ thuộc được tạo ra, nó "chỉ cần nó". Và điều đó làm cho lớp dễ dàng hơn để duy trì .. xem câu trả lời của tôi
Nhà phát triển GameD

3

Tôi không chắc tại sao bạn không muốn biến nhà máy thời gian của mình thành một đơn. Nếu chỉ có một phiên bản của nó trong toàn bộ ứng dụng của bạn, thì thực tế nó là một đơn.

Điều đó đang được nói, rất nguy hiểm khi chia sẻ một đối tượng có thể thay đổi, ngoại trừ nếu nó được bảo vệ đúng cách bởi các khối đồng bộ hóa, trong trường hợp đó, không có lý do gì để nó không phải là một đơn vị.

Nếu bạn muốn thực hiện tiêm phụ thuộc, bạn có thể muốn xem xét mùa xuân hoặc các khung tiêm phụ thuộc khác, điều này sẽ cho phép bạn tự động gán tham số bằng cách sử dụng chú thích


Spring là dành cho java và tôi sử dụng C ++. Nhưng tại sao 2 người lại đánh giá thấp bạn? Có những điểm trong bài viết của bạn tôi không đồng ý nhưng vẫn ...
Dinaiz

Tôi sẽ giả định rằng downvote là do không trả lời câu hỏi, có lẽ cũng như đề cập đến Spring. Tuy nhiên, gợi ý về việc sử dụng Singleton là một điều hợp lệ trong tâm trí của tôi và có thể không trả lời câu hỏi - nhưng gợi ý một giải pháp thay thế hợp lệ và đưa ra một vấn đề thiết kế thú vị. Vì lý do này, tôi đã +1.
Fergus Tại London

1

Điều này nhằm mục đích trở thành một câu trả lời bổ sung cho Doc Brown, và cũng để trả lời những bình luận chưa được trả lời của Dinaiz vẫn còn liên quan đến Câu hỏi.

Những gì bạn có thể cần là một khuôn khổ để làm DI. Có cấu trúc phân cấp phức tạp không nhất thiết có nghĩa là thiết kế tồi, nhưng nếu bạn phải tiêm từ dưới lên TimeFactory (từ A đến D) thay vì tiêm trực tiếp vào D thì có lẽ có gì đó không đúng với cách bạn đang thực hiện Dependency Injection.

Một người độc thân? Không, cám ơn. Nếu bạn chỉ cần một istance, hãy chia sẻ nó trong bối cảnh ứng dụng của bạn (Sử dụng bộ chứa IoC cho DI như Infector ++ chỉ cần liên kết TimeFactory dưới dạng một istance), đây là ví dụ (C ++ 11, nhưng C ++. đến C ++ 11 chưa? Bạn nhận được ứng dụng Leak-Free miễn phí):

Infector::Container ioc; //your app's context

ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor 

// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();

Bây giờ, điểm hay của bộ chứa IoC là bạn không cần phải chuyển nhà máy thời gian lên D. nếu lớp "D" của bạn cần nhà máy thời gian, chỉ cần đặt nhà máy thời gian làm tham số hàm tạo cho lớp D.

ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D

//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A

như bạn thấy bạn tiêm TimeFactory chỉ một lần. Làm thế nào để sử dụng "A"? Rất đơn giản, mỗi lớp được tiêm, xây dựng chính hoặc được xây dựng với một nhà máy.

auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time

mỗi khi bạn tạo lớp A, nó sẽ tự động (istantiation lười biếng) được tiêm tất cả các phụ thuộc lên đến D và D sẽ được tiêm TimeFactory, do đó, chỉ bằng cách gọi 1 phương thức, bạn đã sẵn sàng phân cấp hoàn chỉnh (và thậm chí cả các thuật ngữ phức tạp được giải quyết theo cách này loại bỏ RẤT NHIỀU mã tấm nồi hơi): Bạn không phải gọi "mới / xóa" và điều đó rất quan trọng vì bạn có thể tách logic ứng dụng khỏi mã keo.

D có thể tạo các đối tượng Thời gian với thông tin mà chỉ D có thể có

Điều đó thật dễ dàng, TimeFactory của bạn có phương thức "tạo", sau đó chỉ cần sử dụng một chữ ký khác "tạo (params)" và bạn đã hoàn thành. Các thông số không phụ thuộc thường được giải quyết theo cách này. Điều này cũng loại bỏ nhiệm vụ tiêm những thứ như "chuỗi" hoặc "số nguyên" bởi vì chỉ cần thêm tấm nồi hơi.

Ai tạo ra ai? IoC container tạo ra các istances và nhà máy, các nhà máy tạo ra phần còn lại (các nhà máy có thể tạo ra các đối tượng khác nhau với các tham số tùy ý, vì vậy bạn không thực sự cần một trạng thái cho các nhà máy). Bạn vẫn có thể sử dụng các nhà máy làm trình bao bọc cho IoC Container: nói chung, việc tiêm chích vào IoC Container rất tệ và giống như sử dụng công cụ định vị dịch vụ. Một số người đã giải quyết vấn đề bằng cách bọc IoC Container với một nhà máy (điều này không thực sự cần thiết, nhưng có lợi thế là hệ thống phân cấp được giải quyết bởi Container và tất cả các nhà máy của bạn thậm chí còn dễ bảo trì hơn).

//factory method
std::unique_ptr<myType> create(params){
    auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
    istance->setParams(params); //the customization you needed
    return std::move(istance); 
}

Cũng không lạm dụng tiêm phụ thuộc, các loại đơn giản chỉ có thể là thành viên của lớp hoặc biến trong phạm vi cục bộ. Điều này có vẻ hiển nhiên nhưng tôi thấy mọi người tiêm "std :: vector" chỉ vì có một khung DI cho phép điều đó. Luôn nhớ luật của Demeter: "Chỉ tiêm những gì bạn thực sự cần tiêm"


Wow tôi đã không nhìn thấy câu trả lời của bạn trước đây, xin lỗi! Rất nhiều thông tin thưa ông! Nâng cấp
Dinaiz

0

Các lớp A, B và C của bạn cũng cần tạo các thể hiện Thời gian hay chỉ lớp D? Nếu đó chỉ là lớp D, thì A và B sẽ không biết gì về TimeFactory. Tạo một thể hiện của TimeFactory bên trong lớp C và chuyển nó đến lớp D. Lưu ý rằng bằng cách "tạo một thể hiện" Tôi không nhất thiết có nghĩa là lớp C phải chịu trách nhiệm khởi tạo TimeFactory. Nó có thể nhận DClassFactory từ lớp B và DClassFactory biết cách tạo cá thể Time.

Một kỹ thuật mà tôi cũng thường sử dụng khi tôi không có bất kỳ khung DI nào là cung cấp hai hàm tạo, một hàm chấp nhận một nhà máy và một kỹ thuật khác tạo ra một nhà máy mặc định. Cái thứ hai thường có quyền truy cập được bảo vệ / gói và được sử dụng chủ yếu cho các bài kiểm tra đơn vị.


-1

Tôi đã triển khai một khung công tác tiêm phụ thuộc C ++ khác, gần đây đã được đề xuất để tăng cường - https://github.com/krzysztof-jusiak/di - thư viện là macro ít hơn (miễn phí), chỉ tiêu đề, C ++ 03 / C ++ 11 / Thư viện C ++ 14 cung cấp loại an toàn, thời gian biên dịch, tiêm phụ thuộc hàm tạo miễn phí macro.


3
Trả lời các câu hỏi cũ trên P.SE là một cách kém để quảng cáo dự án của bạn. Hãy xem xét rằng có hàng chục ngàn dự án ngoài kia có thể có một số câu hỏi liên quan. Nếu tất cả các tác giả của họ đăng như vậy ở đây, chất lượng câu trả lời của trang web sẽ giảm mạnh.
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.