Tại sao tôi nên sử dụng tiêm phụ thuộc?


95

Tôi đang gặp khó khăn trong việc tìm kiếm tài nguyên về lý do tại sao tôi nên sử dụng tiêm phụ thuộc . Hầu hết các tài nguyên mà tôi thấy giải thích rằng nó chỉ chuyển một thể hiện của một đối tượng sang một thể hiện khác của một đối tượng, nhưng tại sao? Đây có phải chỉ dành cho kiến ​​trúc / mã sạch hơn hay điều này ảnh hưởng đến toàn bộ hiệu suất?

Tại sao tôi nên làm như sau?

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Thay vì sau đây?

class Profile {
    public function deactivateProfile()
    {
        $setting = new Setting();
        $setting->isActive = false;
    }
}

8
Bạn đang giới thiệu một phụ thuộc được mã hóa cứng để hủy kích hoạtProfile () (điều này là xấu). Bạn có nhiều mã tách rời hơn trong mã đầu tiên, điều này giúp dễ dàng thay đổi và kiểm tra hơn.
Aulis Ronkainen

3
Tại sao bạn sẽ làm cái đầu tiên? Bạn đang chuyển qua Cài đặt và sau đó bỏ qua giá trị của nó.
Phil N DeBlanc

57
Tôi không đồng ý với các downvote. Trong khi vấn đề có thể được coi là tầm thường đối với các chuyên gia, câu hỏi có giá trị: nếu nên sử dụng phép đảo ngược phụ thuộc, thì cần phải có sự biện minh cho việc sử dụng nó.
Flater

13
@PhilNDeBlanc: Mã này rõ ràng là quá đơn giản và không thực sự chỉ ra logic của thế giới thực. Tuy nhiên, deactivateProfilegợi ý cho tôi rằng đặt isActivethành sai mà không quan tâm đến trạng thái trước đó là cách tiếp cận chính xác ở đây. Gọi phương thức vốn có nghĩa là bạn muốn đặt nó là không hoạt động, không nhận trạng thái hoạt động (trong) hiện tại của nó.
Flater

2
Mã của bạn không phải là một ví dụ về tiêm phụ thuộc hoặc đảo ngược. Đây là một ví dụ về tham số hóa (thường tốt hơn nhiều so với DI).
jpmc26

Câu trả lời:


104

Ưu điểm là không cần tiêm phụ thuộc, lớp Hồ sơ của bạn

  • cần biết cách tạo đối tượng Cài đặt (vi phạm Nguyên tắc Trách nhiệm Đơn lẻ)
  • Luôn tạo đối tượng Cài đặt của nó theo cùng một cách (tạo khớp nối chặt chẽ giữa hai)

Nhưng với tiêm phụ thuộc

  • Logic để tạo các đối tượng Cài đặt là ở một nơi khác
  • Thật dễ dàng để sử dụng các loại đối tượng Cài đặt khác nhau

Điều này có vẻ (hoặc thậm chí) không liên quan trong trường hợp cụ thể này, nhưng hãy tưởng tượng nếu chúng ta không nói về một đối tượng Cài đặt, mà là một đối tượng DataStore, có thể có các triển khai khác nhau, một lưu trữ dữ liệu trong tệp và một đối tượng khác lưu trữ dữ liệu đó Một cơ sở dữ liệu. Và đối với các bài kiểm tra tự động, bạn cũng muốn thực hiện giả. Bây giờ bạn thực sự không muốn lớp Hồ sơ mã hóa cái mà nó sử dụng - và thậm chí quan trọng hơn, bạn thực sự, thực sự không muốn lớp Hồ sơ biết về đường dẫn hệ thống tệp, kết nối DB và mật khẩu, vì vậy việc tạo các đối tượng DataStore thể xảy ra ở một nơi khác.


23
This may seem (or even be) irrelevant in this particular caseTôi nghĩ rằng nó có liên quan rất nhiều, trên thực tế. Làm thế nào bạn sẽ nhận được các cài đặt? Rất nhiều hệ thống tôi đã thấy sẽ có một bộ cài đặt mặc định được mã hóa cứng và cấu hình phải đối mặt công khai, vì vậy bạn cần tải cả hai và ghi đè lên một số giá trị bằng cài đặt công khai. Bạn thậm chí có thể cần nhiều nguồn mặc định. Có lẽ bạn thậm chí có thể nhận được một số từ đĩa, những người khác từ DB. Vì vậy, toàn bộ logic để thậm chí nhận cài đặt có thể, và thường là, không tầm thường - chắc chắn không phải là thứ gì đó tiêu thụ mã nên hoặc sẽ quan tâm.
VLAZ

Chúng ta cũng có thể đề cập rằng việc khởi tạo đối tượng cho một thành phần không tầm thường, chẳng hạn như một dịch vụ web, sẽ làm cho $setting = new Setting();hiệu quả khủng khiếp. Tiêm và đối tượng khởi tạo xảy ra một lần.
vikingsteve

9
Tôi nghĩ rằng sử dụng giả để thử nghiệm nên có sự nhấn mạnh hơn. Rất có thể nếu bạn chỉ nhìn vào mã, nó sẽ luôn là một đối tượng Cài đặt và sẽ không bao giờ thay đổi, vì vậy việc chuyển nó vào có vẻ như là một nỗ lực lãng phí. Tuy nhiên, lần đầu tiên bạn cố gắng để kiểm tra một đối tượng hồ sơ bằng bản thân mà không có cũng cần một đối tượng Cài đặt (sử dụng một đối tượng giả thay vì như một giải pháp) nhu cầu là rất rõ ràng.
JPhi1618

2
@ JPhi1618 Tôi nghĩ rằng vấn đề với việc nhấn mạnh "DI dành cho Kiểm tra đơn vị" là nó chỉ dẫn đến câu hỏi "tại sao tôi cần Kiểm tra Đơn vị". Câu trả lời có vẻ rõ ràng đối với bạn và lợi ích chắc chắn là có, nhưng với một người mới bắt đầu, nói rằng "bạn cần phải làm điều nghe có vẻ phức tạp này để làm điều này nghe có vẻ phức tạp khác" có xu hướng một chút tắt. Vì vậy, thật tốt khi đề cập đến những lợi thế khác nhau có thể áp dụng nhiều hơn cho những gì họ đang làm ngay bây giờ.
IMSoP

1
@ user986730 - đó là một điểm rất hay và nhấn mạnh rằng nguyên tắc thực sự ở đây là Nghịch đảo phụ thuộc , trong đó Tiêm chỉ là một kỹ thuật. Ví dụ, trong C, bạn thực sự không thể tiêm phụ thuộc bằng thư viện IOC, nhưng bạn có thể đảo ngược chúng tại thời gian biên dịch, bằng cách bao gồm các tệp nguồn khác nhau cho các giả của bạn, v.v., có cùng tác dụng.
Stephen Byrne

64

Dependency Injection làm cho mã của bạn dễ kiểm tra hơn.

Tôi đã học được điều này khi tôi được giao nhiệm vụ sửa một lỗi khó bắt gặp trong tích hợp PayPal của Magento.

Một vấn đề sẽ nảy sinh khi PayPal nói với Magento về khoản thanh toán thất bại: Magento sẽ không đăng ký thất bại đúng cách.

Việc kiểm tra bản sửa lỗi tiềm năng "thủ công" sẽ rất tẻ nhạt: bạn cần bằng cách nào đó kích hoạt thông báo PayPal "Không thành công". Bạn sẽ phải gửi một kiểm tra điện tử, hủy bỏ nó và chờ cho nó lỗi. Điều đó có nghĩa là hơn 3 ngày để kiểm tra thay đổi mã một ký tự!

May mắn thay, có vẻ như các nhà phát triển lõi Magento đã phát triển chức năng này đã thử nghiệm và sử dụng mô hình tiêm phụ thuộc để làm cho nó trở nên tầm thường. Điều này cho phép chúng tôi xác minh công việc của mình với một trường hợp thử nghiệm đơn giản như thế này:

<?php
// This is the dependency we will inject to facilitate our testing
class MockHttpClient extends Varien_Http_Adapter_Curl {
    function read() {
        // Make Magento think that PayPal said "VERIFIED", no matter what they actually said...
        return "HTTP/1.1 200 OK\n\nVERIFIED";
    }
}

// Here, we trick Magento into thinking PayPal actually sent something back.
// Magento will try to verify it against PayPal's API though, and since it's fake data, it'll always fail.
$ipnPayload = array (
  'invoice'        => '100058137',         // Order ID to test against
  'txn_id'         => '04S87540L2309371A', // Test PayPal transaction ID
  'payment_status' => 'Failed'             // New payment status that Magento should ingest
);

// This is what Magento's controller calls during a normal IPN request.
// Instead of letting Magento talk to PayPal, we "inject" our fake HTTP client, which always returns VERIFIED.
Mage::getModel('paypal/ipn')->processIpnRequest($ipnPayload, new MockHttpClient());

Tôi chắc chắn rằng mẫu DI có nhiều lợi thế khác, nhưng khả năng kiểm tra tăng lên là lợi ích lớn nhất trong tâm trí tôi .

Nếu bạn tò mò về giải pháp cho vấn đề này, hãy xem repo GitHub tại đây: https://github.com/bubbleupdev/BUCorefix_Paypalstatus


4
Việc tiêm phụ thuộc làm cho mã dễ kiểm tra hơn mã với các phụ thuộc được mã hóa cứng. Loại bỏ sự phụ thuộc khỏi logic kinh doanh hoàn toàn thậm chí còn tốt hơn.
Ant P

1
Và một cách chính để làm những gì @AntP gợi ý là thông qua tham số hóa . Logic để chuyển đổi các kết quả quay lại từ cơ sở dữ liệu thành đối tượng được sử dụng để điền vào mẫu trang (thường được gọi là "mô hình") thậm chí không biết việc tìm nạp đang xảy ra; nó chỉ cần lấy những đối tượng đó làm đầu vào.
jpmc26

4
@ jpmc26 thực sự - Tôi có xu hướng tìm hiểu về lõi chức năng, vỏ mệnh lệnh , đây thực sự chỉ là một cái tên ưa thích để truyền dữ liệu vào miền của bạn thay vì tiêm phụ thuộc vào nó. Tên miền là thuần túy, có thể được kiểm tra đơn vị không có giả và sau đó chỉ được bọc trong một vỏ thích nghi với những thứ như sự kiên trì, nhắn tin, v.v.
Ant P

1
Tôi nghĩ rằng sự tập trung duy nhất vào khả năng kiểm tra là có hại cho việc áp dụng DI. Điều này khiến nó không hấp dẫn đối với những người cảm thấy như họ không cần thử nghiệm nhiều hoặc nghĩ rằng họ đã kiểm tra được. Tôi sẽ tranh luận rằng hầu như không thể viết mã sạch, có thể tái sử dụng mà không có DI. Việc kiểm tra nằm rất xa trong danh sách các lợi ích và thật đáng thất vọng khi thấy câu trả lời này được xếp hạng 1 hoặc 2 trong mỗi câu hỏi về lợi ích của DI.
Carl Leth

26

Tại sao (vấn đề thậm chí là gì)?

Tại sao tôi nên sử dụng tiêm phụ thuộc?

Cách ghi nhớ tốt nhất tôi tìm thấy cho điều này là " mới là keo ": Mỗi khi bạn sử dụng newmã của mình, mã đó được gắn với triển khai cụ thể đó . Nếu bạn liên tục sử dụng mới trong các hàm tạo, bạn sẽ tạo ra một chuỗi các triển khai cụ thể . Và bởi vì bạn không thể "có" một thể hiện của một lớp mà không xây dựng nó, bạn không thể tách chuỗi đó.

Ví dụ, hãy tưởng tượng bạn đang viết một trò chơi video về xe đua. Bạn đã bắt đầu với một lớp Game, tạo một RaceTrack, tạo 8 Cars, mỗi lớp tạo một Motor. Bây giờ nếu bạn muốn thêm 4 Carsvới gia tốc khác nhau, bạn sẽ phải thay đổi mọi lớp được đề cập , ngoại trừ có thể Game.

Mã sạch hơn

Đây có phải chỉ dành cho kiến ​​trúc / mã sạch hơn

.

Tuy nhiên, nó có vẻ rất rõ ràng trong tình huống này, bởi vì đó là một ví dụ về cách thực hiện nó . Lợi thế thực tế chỉ thể hiện khi một số lớp có liên quan và khó chứng minh hơn, nhưng hãy tưởng tượng bạn sẽ sử dụng DI trong ví dụ trước. Mã tạo ra tất cả những thứ đó có thể trông giống như thế này:

List<Car> cars = new List<Car>();
for(int i=0; i<8; i++){
    float acceleration = 0.3f;
    float maxSpeed = 200.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
RaceTrack raceTrack = new RaceTrack(cars);
Game game = new Game(raceTrack);

Thêm 4 chiếc xe khác nhau bây giờ có thể được thực hiện bằng cách thêm các dòng sau:

for(int i=0; i<4; i++){
    float acceleration = 0.5f;
    float maxSpeed = 100.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
  • Không có thay đổi trong RaceTrack, Game, Car, hoặc Motorlà cần thiết - có nghĩa là chúng ta có thể chắc chắn 100% rằng chúng tôi không giới thiệu bất kỳ lỗi mới đó!
  • Thay vì phải nhảy giữa một số tệp, bạn sẽ có thể thấy sự thay đổi hoàn toàn trên màn hình của mình cùng một lúc. Đây là kết quả của việc xem sáng tạo / thiết lập / cấu hình là trách nhiệm của riêng mình - đó không phải là công việc của một chiếc xe hơi để chế tạo một động cơ.

Cân nhắc hiệu suất

hoặc điều này ảnh hưởng đến hiệu suất nói chung?

Không . Nhưng để hoàn toàn trung thực với bạn, nó có thể.

Tuy nhiên, ngay cả trong trường hợp đó, đó là một số tiền nhỏ đến mức nực cười mà bạn không cần phải quan tâm. Nếu tại một thời điểm nào đó trong tương lai, bạn phải viết mã cho một tamagotchi với tương đương CPU 5Mhz và RAM 2 MB, thì có lẽ bạn có thể phải quan tâm đến điều này.

Trong 99,999% * các trường hợp, nó sẽ có hiệu suất tốt hơn, vì bạn đã dành ít thời gian hơn để sửa lỗi và có nhiều thời gian hơn để cải thiện các thuật toán nặng tài nguyên của bạn.

* hoàn toàn tạo thành số

Đã thêm thông tin: "mã hóa cứng"

Đừng nhầm lẫn, đây vẫn rất nhiều "mã hóa cứng" - các số được viết trực tiếp trong mã. Không được mã hóa cứng có nghĩa là một cái gì đó như lưu trữ các giá trị đó trong tệp văn bản - ví dụ: ở định dạng JSON - và sau đó đọc chúng từ tệp đó.

Để làm điều đó, bạn phải thêm mã để đọc một tệp và sau đó phân tích cú pháp JSON. Nếu bạn xem xét ví dụ một lần nữa; trong phiên bản không phải DI, giờ đây Carhoặc Motorphải đọc tệp. Điều đó không có vẻ như nó quá nhiều ý nghĩa.

Trong phiên bản DI, bạn sẽ thêm nó vào mã thiết lập trò chơi.


3
Quảng cáo được mã hóa cứng, thực sự không có quá nhiều sự khác biệt giữa mã và tệp cấu hình. Một tệp đi kèm với ứng dụng là một nguồn ngay cả khi bạn đọc động. Việc kéo các giá trị từ mã sang các tệp dữ liệu ở định dạng json hoặc bất kỳ định dạng 'config' nào sẽ không giúp được gì trừ khi các giá trị sẽ bị người dùng ghi đè hoặc phụ thuộc vào môi trường.
Jan Hudec

3
Tôi thực sự đã tạo ra một Tamagotchi một lần trên Arduino (16 MHz, 2KB)
Jungkook 14/11/18

@JanHudec Đúng. Tôi thực sự đã có một lời giải thích dài hơn ở đó, nhưng đã quyết định loại bỏ nó để giữ cho nó ngắn hơn và tập trung vào cách nó liên quan đến DI. Có nhiều thứ không đúng 100%; Nhìn chung, câu trả lời được tối ưu hóa nhiều hơn để đẩy "điểm" của DI mà không quá lâu. Hoặc nói khác đi, đây là những gì tôi muốn nghe khi tôi bắt đầu với DI.
R. Schmitz

17

Tôi luôn luôn bị cản trở bởi tiêm phụ thuộc. Nó dường như chỉ tồn tại trong các quả cầu Java, nhưng những quả cầu đó đã nói về nó với sự tôn kính lớn. Đó là một trong những Mẫu tuyệt vời, bạn thấy, được cho là mang lại trật tự cho sự hỗn loạn. Nhưng các ví dụ luôn luôn phức tạp và giả tạo, thiết lập một vấn đề và sau đó đặt ra để giải quyết nó bằng cách làm cho mã phức tạp hơn.

Nó có ý nghĩa hơn khi một đồng nghiệp Python phát triển cho tôi sự khôn ngoan này: nó chỉ truyền các đối số cho các hàm. Nó hầu như không phải là một mô hình; giống như một lời nhắc nhở rằng bạn có thể yêu cầu một điều gì đó như là một đối số, ngay cả khi bạn có thể tự mình cung cấp một giá trị hợp lý.

Vì vậy, câu hỏi của bạn gần tương đương với "tại sao chức năng của tôi nên lấy đối số?" và có nhiều câu trả lời giống nhau, cụ thể là: để người gọi đưa ra quyết định .

Tất nhiên, điều này đi kèm với một chi phí, bởi vì bây giờ bạn buộc người gọi phải đưa ra một số quyết định (trừ khi bạn đưa ra đối số tùy chọn) và giao diện có phần phức tạp hơn. Đổi lại, bạn có được sự linh hoạt.

Vì vậy: Có lý do chính đáng nào bạn cần sử dụng Settingloại / giá trị cụ thể này không? Có một lý do chính đáng để gọi mã có thể muốn một Settingloại / giá trị khác nhau ? (Hãy nhớ rằng, các bài kiểm tra là mã !)


2
Vâng. IoC cuối cùng đã nhấp vào tôi khi tôi nhận ra rằng đó chỉ đơn giản là "để người gọi đưa ra quyết định". IoC đó có nghĩa là quyền kiểm soát thành phần được chuyển từ tác giả của thành phần sang người dùng của thành phần. Và vì tại thời điểm đó tôi đã có đủ khả năng với phần mềm nghĩ rằng nó thông minh hơn tôi, tôi đã được bán ngay lập tức theo phương pháp DI.
Joker_vD

1
"nó chỉ truyền các đối số cho các hàm. Nó hoàn toàn không phải là một mô hình" Đây là lời giải thích tốt hoặc thậm chí dễ hiểu của DI tôi đã nghe (hầu hết mọi người thậm chí không nói về nó là gì , mà là lợi ích của nó). Và sau đó, họ sử dụng các thuật ngữ phức tạp như "đảo ngược điều khiển" và "khớp nối lỏng lẻo" chỉ đòi hỏi nhiều câu hỏi hơn để hiểu khi đó là một ý tưởng thực sự đơn giản.
addison

7

Ví dụ bạn đưa ra không phải là tiêm phụ thuộc theo nghĩa cổ điển. Việc tiêm phụ thuộc thường đề cập đến việc truyền các đối tượng trong một hàm tạo hoặc bằng cách sử dụng "setter tiêm" ngay sau khi đối tượng được tạo, để đặt giá trị trên một trường trong một đối tượng mới được tạo.

Ví dụ của bạn chuyển một đối tượng làm đối số cho một phương thức thể hiện. Phương thức cá thể này sau đó sửa đổi một trường trên đối tượng đó. Phụ thuộc tiêm? Không phá vỡ đóng gói và ẩn dữ liệu? Chắc chắn rồi!

Bây giờ, nếu mã là như thế này:

class Profile {
    private $settings;

    public function __construct(Settings $settings) {
        $this->settings = $settings;
    }

    public function deactive() {
        $this->settings->isActive = false;
    }
}

Sau đó tôi sẽ nói bạn đang sử dụng tiêm phụ thuộc. Sự khác biệt đáng chú ý là một Settingsđối tượng được truyền vào hàm tạo hoặc Profileđối tượng.

Điều này hữu ích nếu đối tượng Cài đặt tốn kém hoặc phức tạp để xây dựng hoặc Cài đặt là giao diện hoặc lớp trừu tượng nơi có nhiều triển khai cụ thể tồn tại để thay đổi hành vi thời gian chạy.

Vì bạn đang truy cập trực tiếp vào một trường trên đối tượng Cài đặt thay vì gọi một phương thức, bạn không thể tận dụng Đa hình, đây là một trong những lợi ích của việc tiêm phụ thuộc.

Có vẻ như Cài đặt cho Cấu hình dành riêng cho cấu hình đó. Trong trường hợp này tôi sẽ làm một trong những điều sau đây:

  1. Khởi tạo đối tượng Cài đặt bên trong hàm tạo Cấu hình

  2. Truyền đối tượng Cài đặt trong hàm tạo và sao chép qua các trường riêng lẻ áp dụng cho Cấu hình

Thành thật mà nói, bằng cách chuyển đối tượng Cài đặt vào deactivateProfilevà sau đó sửa đổi trường bên trong của đối tượng Cài đặt là mùi mã. Đối tượng Cài đặt phải là người duy nhất sửa đổi các trường bên trong của nó.


5
The example you give is not dependency injection in the classical sense.- Không sao đâu. Người OO có đồ vật trong não, nhưng bạn vẫn đang phụ thuộc vào thứ gì đó.
Robert Harvey

1
Khi bạn nói về người Viking theo nghĩa cổ điển , bạn, như @RobertHarvey nói, nói hoàn toàn theo thuật ngữ OO. Ví dụ, trong lập trình chức năng, việc tiêm một hàm này vào một hàm khác (các hàm bậc cao hơn) là ví dụ cổ điển của mô hình tiêm phụ thuộc.
David Arno

3
@RobertHarvey: Tôi đoán rằng tôi đã dùng quá nhiều quyền tự do với "tiêm phụ thuộc". Nơi mọi người thường nghĩ về thuật ngữ và sử dụng thuật ngữ này là tham chiếu đến các trường trên một đối tượng được "tiêm" tại thời điểm xây dựng đối tượng hoặc ngay sau đó trong trường hợp tiêm setter.
Greg Burghardt

@DavidArno: Vâng, bạn đã đúng. OP dường như có một đoạn mã PHP hướng đối tượng trong câu hỏi, vì vậy tôi chỉ trả lời về vấn đề đó và không giải quyết lập trình chức năng --- tất cả mặc dù với PHP bạn có thể hỏi cùng một câu hỏi từ quan điểm của lập trình chức năng.
Greg Burghardt

7

Tôi biết tôi sẽ đến bữa tiệc muộn nhưng tôi cảm thấy một điểm quan trọng đang bị bỏ lỡ.

Tại sao tôi nên làm điều này:

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Bạn không nên. Nhưng không phải vì Dependency Injection là một ý tưởng tồi. Đó là bởi vì điều này đang làm sai.

Hãy nhìn vào những thứ này bằng cách sử dụng mã. Chúng tôi sẽ làm điều này:

$profile = new Profile();
$profile->deactivateProfile($setting);

khi chúng ta nhận được điều tương tự từ điều này:

$setting->isActive = false; // Deactivate profile

Vì vậy, tất nhiên nó có vẻ như một sự lãng phí thời gian. Đó là khi bạn làm theo cách này. Đây không phải là cách sử dụng tốt nhất của Dependency Injection. Nó thậm chí không phải là cách sử dụng tốt nhất của một lớp.

Bây giờ nếu như thay vào đó chúng ta đã có điều này:

$profile = new Profile($setting);

$application = new Application($profile);

$application.start();

Và bây giờ, applicationcó thể tự do kích hoạt và hủy kích hoạt profilemà không cần phải biết bất cứ điều gì cụ thể về việc settingnó thực sự thay đổi. Tại sao cái đó lại tốt? Trong trường hợp bạn cần thay đổi cài đặt. Điều applicationnày được ngăn chặn từ những thay đổi đó để bạn có thể thoải mái đi vào một không gian an toàn mà không phải xem mọi thứ bị hỏng ngay khi bạn chạm vào thứ gì đó.

Điều này tuân theo việc xây dựng riêng biệt từ nguyên tắc hành vi . Mẫu DI ở đây là một mẫu đơn giản. Xây dựng mọi thứ bạn cần ở mức thấp nhất có thể, nối chúng lại với nhau, sau đó bắt đầu tất cả các hành vi tích tắc bằng một cuộc gọi.

Kết quả là bạn có một nơi riêng biệt để quyết định kết nối với cái gì và một nơi khác để quản lý những gì nói với cái gì.

Hãy thử điều đó trên một cái gì đó bạn phải duy trì theo thời gian và xem nếu nó không giúp đỡ.


6

Là một khách hàng, khi bạn thuê một thợ máy để làm một cái gì đó cho chiếc xe của bạn, bạn có mong đợi rằng người thợ đó sẽ chế tạo một chiếc xe từ đầu chỉ để làm việc với nó không? Không, bạn cung cấp cho thợ máy chiếc xe bạn muốn họ làm việc .

Là chủ sở hữu nhà để xe, khi bạn hướng dẫn một thợ máy làm một cái gì đó cho một chiếc xe hơi, bạn có mong đợi các thợ máy tạo ra tuốc nơ vít / cờ lê / phụ tùng xe hơi của riêng mình? Không, bạn cung cấp cho thợ máy các bộ phận / công cụ anh ta cần sử dụng

Tại sao chúng ta làm việc này? Vâng, hãy nghĩ về nó. Bạn là chủ nhà xe muốn thuê ai đó trở thành thợ cơ khí của bạn. Bạn sẽ dạy họ trở thành một thợ cơ khí (= bạn sẽ viết mã).

Điều gì sẽ dễ dàng hơn:

  • Dạy một thợ máy làm thế nào để gắn một cánh lướt gió vào xe bằng tuốc nơ vít.
  • Dạy một thợ máy tạo ra một chiếc xe hơi, tạo ra một spoiler, tạo ra một tuốc nơ vít và sau đó gắn spoiler mới được tạo ra cho chiếc xe mới được tạo ra với tuốc nơ vít mới được tạo ra.

Có những lợi ích to lớn khi không có thợ máy của bạn tạo ra mọi thứ từ đầu:

  • Rõ ràng, đào tạo (= phát triển) được rút ngắn đáng kể nếu bạn chỉ cung cấp cho thợ máy của mình các công cụ và bộ phận hiện có.
  • Nếu cùng một thợ máy phải thực hiện cùng một công việc nhiều lần, bạn có thể chắc chắn rằng anh ta sử dụng lại tuốc nơ vít thay vì luôn vứt cái cũ ra và tạo ra một công việc mới.
  • Ngoài ra, một thợ máy đã học cách tạo ra mọi thứ sẽ cần nhiều hơn một chuyên gia, và do đó sẽ mong đợi một mức lương cao hơn. Điểm tương đồng về mã hóa ở đây là một lớp có nhiều trách nhiệm khó bảo trì hơn nhiều so với một lớp có một trách nhiệm được xác định nghiêm ngặt.
  • Ngoài ra, khi các phát minh mới được tung ra thị trường và các chiến lợi phẩm hiện đang được chế tạo từ carbon thay vì nhựa; bạn sẽ phải đào tạo lại (= tái phát triển) thợ máy chuyên gia của bạn. Nhưng thợ máy "đơn giản" của bạn sẽ không phải đào tạo lại miễn là spoiler vẫn có thể được gắn theo cùng một cách.
  • Có một thợ máy không dựa vào chiếc xe mà họ tự chế tạo có nghĩa là bạn có một thợ máy có thể điều khiển bất kỳ chiếc xe nào họ có thể nhận. Bao gồm cả những chiếc xe thậm chí còn chưa tồn tại vào thời điểm đào tạo thợ máy. Tuy nhiên, thợ máy chuyên gia của bạn sẽ không thể chế tạo những chiếc xe mới hơn đã được tạo ra sau khi đào tạo.

Nếu bạn thuê và đào tạo thợ cơ khí chuyên nghiệp, bạn sẽ kết thúc với một nhân viên tốn nhiều tiền hơn, mất nhiều thời gian hơn để thực hiện công việc đơn giản và sẽ cần phải được đào tạo lại bất cứ khi nào một trong số nhiều trách nhiệm của họ cần để được cập nhật.

Điểm tương đồng về phát triển là nếu bạn sử dụng các lớp có phần phụ thuộc được mã hóa cứng, thì bạn sẽ khó có thể duy trì các lớp sẽ cần tái phát triển / thay đổi liên tục bất cứ khi nào một phiên bản mới của đối tượng ( Settingstrong trường hợp của bạn) được tạo ra, và bạn 'sẽ phải phát triển logic bên trong cho lớp để có khả năng tạo các loại Settingsđối tượng khác nhau .

Hơn nữa, bất cứ ai tiêu thụ lớp của bạn bây giờ cũng sẽ phải yêu cầu lớp tạo Settingsđối tượng chính xác , trái ngược với việc đơn giản là có thể vượt qua lớp bất kỳ Settingsđối tượng nào mà nó muốn vượt qua. Điều này có nghĩa là sự phát triển bổ sung cho người tiêu dùng để tìm ra cách yêu cầu lớp tạo ra công cụ phù hợp.


Có, đảo ngược phụ thuộc phải mất một chút nỗ lực để viết thay vì mã hóa phụ thuộc. Vâng, thật khó chịu khi phải gõ nhiều hơn.

Nhưng đó là lý lẽ tương tự như việc chọn mã hóa các giá trị bằng chữ bởi vì "khai báo biến mất nhiều nỗ lực hơn". Về mặt kỹ thuật chính xác, nhưng sự chuyên nghiệp vượt trội hơn nhiều so với nhược điểm .

Lợi ích của việc đảo ngược phụ thuộc không có kinh nghiệm khi bạn tạo phiên bản đầu tiên của ứng dụng. Lợi ích của việc đảo ngược phụ thuộc được trải nghiệm khi bạn cần thay đổi hoặc mở rộng phiên bản ban đầu đó. Và đừng tự lừa mình rằng bạn sẽ hiểu đúng ngay từ lần đầu tiên và sẽ không cần gia hạn / thay đổi mã. Bạn sẽ phải thay đổi mọi thứ.


điều này có ảnh hưởng đến hiệu suất nói chung không?

Điều này không ảnh hưởng đến hiệu suất thời gian chạy của ứng dụng. Nhưng nó ồ ạt tác động đến thời gian phát triển (và do đó là hiệu suất) của nhà phát triển .


2
Nếu bạn xóa bình luận của mình, " Như một bình luận nhỏ, câu hỏi của bạn tập trung vào nghịch đảo phụ thuộc, không phải tiêm phụ thuộc. Tiêm là một cách để đảo ngược, nhưng đó không phải là cách duy nhất. ", Đây sẽ là một câu trả lời tuyệt vời. Nghịch đảo phụ thuộc có thể đạt được thông qua tiêm hoặc định vị / toàn cầu. Các ví dụ của câu hỏi liên quan đến tiêm. Vì vậy, câu hỏi về tiêm phụ thuộc (cũng như đảo ngược phụ thuộc).
David Arno

12
Tôi nghĩ rằng toàn bộ chiếc xe hơi hơi khó hiểu và khó hiểu
Ewan

4
@Flater, một phần của vấn đề là không ai thực sự đồng ý về sự khác biệt giữa tiêm phụ thuộc, đảo ngược phụ thuộc và đảo ngược kiểm soát là gì. Mặc dù vậy, có một điều chắc chắn là "container" chắc chắn là không cần thiết để đưa một phụ thuộc vào một phương thức hoặc hàm tạo. DI thuần túy (hoặc người nghèo) DI mô tả cụ thể việc tiêm phụ thuộc thủ công. Đó là mũi tiêm phụ thuộc duy nhất mà cá nhân tôi sử dụng khi tôi không thích "ma thuật" liên quan đến container.
David Arno

1
@Flater Vấn đề với ví dụ là bạn gần như chắc chắn sẽ không mô hình hóa bất kỳ vấn đề nào trong mã theo cách này. Do đó, nó không tự nhiên, gượng ép và thêm nhiều câu hỏi hơn câu trả lời. Điều đó làm cho nó dễ bị đánh lừa và nhầm lẫn hơn là để chứng minh.
jpmc26

2
@gbjbaanb: Không có câu trả lời nào của tôi thậm chí đi sâu vào đa hình. Không có sự kế thừa, thực hiện giao diện hoặc bất cứ thứ gì tương tự như lên / xuống của các loại. Bạn đang hoàn toàn đọc sai câu trả lời.
Flater

0

Giống như tất cả các mẫu, rất hợp lệ để hỏi "tại sao" để tránh các thiết kế cồng kềnh.

Đối với tiêm phụ thuộc, điều này có thể dễ dàng nhận thấy bằng cách nghĩ về hai khía cạnh quan trọng nhất của thiết kế OOP ...

Khớp nối thấp

Khớp nối trong lập trình máy tính :

Trong công nghệ phần mềm, khớp nối là mức độ phụ thuộc lẫn nhau giữa các mô-đun phần mềm; một thước đo về mức độ kết nối chặt chẽ giữa hai thói quen hoặc mô-đun; sức mạnh của các mối quan hệ giữa các mô-đun.

Bạn muốn đạt được khớp nối thấp . Hai thứ được kết hợp mạnh mẽ có nghĩa là nếu bạn thay đổi một thứ, bạn rất có thể phải thay đổi thứ khác. Lỗi hoặc hạn chế trong một khả năng sẽ gây ra lỗi / hạn chế ở bên kia; vân vân

Một đối tượng khởi tạo lớp của các đối tượng khác là một khớp nối rất mạnh, bởi vì người này cần biết về sự tồn tại của lớp kia; nó cần biết làm thế nào để khởi tạo nó (đối số mà hàm tạo cần) và các đối số đó cần có sẵn khi gọi hàm tạo. Ngoài ra, tùy thuộc vào việc ngôn ngữ có cần giải cấu trúc rõ ràng (C ++) hay không, điều này sẽ giới thiệu thêm các biến chứng. Nếu bạn giới thiệu các lớp mới (nghĩa là a NextSettingshoặc bất cứ thứ gì), bạn phải quay lại lớp ban đầu và thêm nhiều cuộc gọi đến các hàm tạo của chúng.

Độ kết dính cao

Sự gắn kết :

Trong lập trình máy tính, sự gắn kết liên quan đến mức độ mà các yếu tố bên trong một mô-đun thuộc về nhau.

Đây là mặt khác của đồng tiền. Nếu bạn nhìn vào một đơn vị mã (một phương thức, một lớp, một gói, v.v.), bạn muốn có tất cả mã bên trong đơn vị đó để có càng ít trách nhiệm càng tốt.

Một ví dụ cơ bản cho điều này sẽ là mẫu MVC: bạn tách biệt rõ ràng mô hình miền khỏi chế độ xem (GUI) và một lớp điều khiển gắn kết chúng lại với nhau.

Điều này tránh sự phình to mã nơi bạn có được khối lớn làm nhiều việc khác nhau; nếu bạn muốn thay đổi một phần của nó, bạn cũng phải theo dõi tất cả các tính năng khác, cố gắng tránh các lỗi, v.v.; và bạn nhanh chóng lập trình cho mình vào một cái lỗ nơi rất khó thoát ra.

Với phép nội xạ phụ thuộc, bạn ủy quyền việc tạo hoặc theo dõi, phụ thuộc vào bất kỳ lớp nào (hoặc tệp cấu hình) nào thực hiện DI của bạn. Các lớp khác sẽ không quan tâm nhiều đến chính xác những gì đang diễn ra - họ sẽ làm việc với một số giao diện chung và không biết triển khai thực tế là gì, điều đó có nghĩa là họ không chịu trách nhiệm cho những thứ khác.


-1

Bạn nên sử dụng các kỹ thuật để giải quyết vấn đề mà họ giỏi trong việc giải quyết khi bạn gặp phải những vấn đề đó. Phụ thuộc đảo ngược và tiêm không khác nhau.

Nghịch đảo phụ thuộc hoặc tiêm là một kỹ thuật cho phép mã của bạn quyết định việc triển khai phương thức nào được gọi trong thời gian chạy. Điều này tối đa hóa lợi ích của ràng buộc muộn. Kỹ thuật này là cần thiết khi ngôn ngữ không hỗ trợ thay thế thời gian chạy của các chức năng phi thể hiện. Ví dụ, Java thiếu một cơ chế để thay thế các cuộc gọi đến một phương thức tĩnh bằng các cuộc gọi đến một triển khai khác; tương phản với Python, trong đó tất cả những gì cần thiết để thay thế lời gọi hàm là liên kết tên với một hàm khác (gán lại biến giữ chức năng).

Tại sao chúng ta muốn thay đổi việc thực hiện chức năng? Có hai lý do chính:

  • Chúng tôi muốn sử dụng hàng giả cho mục đích thử nghiệm. Điều này cho phép chúng tôi kiểm tra một lớp phụ thuộc vào tìm nạp cơ sở dữ liệu mà không thực sự kết nối với cơ sở dữ liệu.
  • Chúng tôi cần hỗ trợ nhiều triển khai. Ví dụ: chúng ta có thể cần thiết lập một hệ thống hỗ trợ cả cơ sở dữ liệu MySQL và PostgreQuery.

Bạn cũng có thể muốn lưu ý đảo ngược các container điều khiển. Đây là một kỹ thuật nhằm giúp bạn tránh những cây xây dựng khổng lồ, rối rắm trông giống như mã giả này:

thing5 =  new MyThing5();
thing3 = new MyThing3(thing5, new MyThing10());

myApp = new MyApp(
    new MyAppDependency1(thing5, thing3),
    new MyAppDependency2(
        new Thing1(),
        new Thing2(new Thing3(thing5, new Thing4(thing5)))
    ),
    ...
    new MyAppDependency15(thing5)
);

Nó cho phép bạn đăng ký các lớp học và sau đó thực hiện việc xây dựng cho bạn:

injector.register(Thing1); // Yes, you'd need some kind of actual class reference.
injector.register(Thing2);
...
injector.register(MyAppDepdency15);
injector.register(MyApp);

myApp = injector.create(MyApp); // The injector fills in all the construction parameters.

Lưu ý rằng nó đơn giản nhất nếu các lớp được đăng ký có thể là các singletons không trạng thái .

Lời cảnh báo

Lưu ý rằng đảo ngược phụ thuộc không nên là câu trả lời của bạn cho logic tách rời. Tìm kiếm cơ hội để sử dụng tham số hóa thay thế. Hãy xem xét phương pháp mã giả này chẳng hạn:

myAverageAboveMin()
{
    dbConn = new DbConnection("my connection string");
    dbQuery = dbConn.makeQuery();
    dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
    dbQuery.setParam("min", 5);
    dbQuery.Execute();
    myData = dbQuery.getAll();
    count = 0;
    total = 0;
    foreach (row in myData)
    {
        count++;
        total += row.x;
    }

    return total / count;
}

Chúng ta có thể sử dụng nghịch đảo phụ thuộc cho một số phần của phương pháp này:

class MyQuerier
{
    private _dbConn;

    MyQueries(dbConn) { this._dbConn = dbConn; }

    fetchAboveMin(min)
    {
        dbQuery = this._dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    private _querier;

    Averager(querier) { this._querier = querier; }

    myAverageAboveMin(min)
    {
        myData = this._querier.fetchAboveMin(min);
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

Nhưng chúng ta không nên, ít nhất là không hoàn toàn. Lưu ý rằng chúng tôi đã tạo ra một lớp trạng thái với Querier. Bây giờ nó giữ một tham chiếu đến một số đối tượng kết nối toàn cầu về cơ bản. Điều này tạo ra các vấn đề như khó khăn trong việc hiểu trạng thái chung của chương trình và cách các lớp khác nhau phối hợp với nhau. Cũng lưu ý rằng chúng tôi buộc phải giả mạo bộ kiểm tra hoặc kết nối nếu chúng tôi muốn kiểm tra logic trung bình. Hơn nữa Một cách tiếp cận tốt hơn sẽ là tăng tham số hóa :

class MyQuerier
{
    fetchAboveMin(dbConn, min)
    {
        dbQuery = dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    averageData(myData)
    {
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

class StuffDoer
{
    private _querier;
    private _averager;

    StuffDoer(querier, averager)
    {
        this._querier = querier;
        this._averager = averager;
    }

    myAverageAboveMin(dbConn, min)
    {
        myData = this._querier.fetchAboveMin(dbConn, min);
        return this._averager.averageData(myData);
    }
}

Và kết nối sẽ được quản lý ở cấp độ cao hơn, chịu trách nhiệm cho toàn bộ hoạt động và biết phải làm gì với đầu ra này.

Bây giờ chúng ta có thể kiểm tra logic trung bình hoàn toàn độc lập với truy vấn và chúng ta có thể sử dụng nó nhiều hơn trong nhiều tình huống khác nhau. Chúng tôi có thể đặt câu hỏi liệu chúng tôi thậm chí có cần MyQuerierAveragercác đối tượng hay không, và có thể câu trả lời là chúng tôi không nếu chúng tôi không có ý định kiểm tra đơn vị StuffDoer, và không kiểm tra đơn vị StuffDoersẽ hoàn toàn hợp lý vì nó được kết hợp chặt chẽ với cơ sở dữ liệu. Nó có thể có ý nghĩa hơn khi chỉ để cho các bài kiểm tra tích hợp bao gồm nó. Trong trường hợp đó, chúng ta có thể thực hiện tốt fetchAboveMinaverageDatathành các phương thức tĩnh.


2
" Tiêm phụ thuộc là một kỹ thuật nhằm giúp bạn tránh những cây xây dựng khổng lồ, rối rắm ... ". Ví dụ đầu tiên của bạn sau tuyên bố này là một ví dụ về tiêm phụ thuộc thuần túy hoặc nghèo của người đàn ông. Thứ hai là một ví dụ về việc sử dụng bộ chứa IoC để "tự động hóa" việc tiêm các phụ thuộc đó. Cả hai đều là ví dụ của tiêm phụ thuộc trong hành động.
David Arno

@DavidArno Vâng, bạn nói đúng. Tôi đã điều chỉnh thuật ngữ.
jpmc26

Có một lý do chính thứ ba để thay đổi cách triển khai hoặc ít nhất là thiết kế mã giả định rằng việc triển khai có thể khác nhau: nó khuyến khích nhà phát triển có khớp nối lỏng lẻo và tránh viết mã sẽ khó thay đổi khi triển khai mới nên được triển khai vào lúc nào đó tương lai. Và mặc dù điều đó có thể không phải là ưu tiên trong một số dự án (ví dụ: biết rằng ứng dụng sẽ không bao giờ được xem xét lại sau khi phát hành lần đầu), nó sẽ ở các dự án khác (ví dụ như mô hình kinh doanh của công ty đặc biệt cố gắng cung cấp hỗ trợ / mở rộng các ứng dụng của họ ).
Flater

@Flater tiêm phụ thuộc vẫn dẫn đến khớp nối mạnh mẽ. Nó liên kết logic với một giao diện cụ thể và yêu cầu mã được đề cập để biết về bất cứ giao diện nào. Xem xét mã biến đổi kết quả được tìm nạp từ DB. Nếu tôi sử dụng DI để phân tách chúng, thì mã vẫn cần biết rằng việc tìm nạp DB xảy ra và gọi nó. Giải pháp tốt hơn là nếu mã chuyển đổi thậm chí không biết việc tìm nạp đang xảy ra . Cách duy nhất bạn có thể làm là nếu kết quả tìm nạp được chuyển qua bởi một người gọi, thay vì tiêm trình nạp.
jpmc26

1
@ jpmc26 Nhưng trong ví dụ (bình luận) của bạn, cơ sở dữ liệu thậm chí không cần phải là một phụ thuộc để bắt đầu. Tất nhiên tránh phụ thuộc là tốt hơn cho khớp nối lỏng lẻo, nhưng DI tập trung vào việc thực hiện các phụ thuộc cần thiết.
Flater
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.