47

nhanh Chuyển tiếp:Hiểu IoC Container và Dependency Injection

Tôi viết thư này với mục đích nhận được một sự hiểu biết tốt hơn về dependency injection và container IoC, nhưng cũng vì thế mà sau đó tôi có thể sửa chữa những sai lầm trong nó và sử dụng nó để giúp dạy cho một vài người bạn của tôi về họ.

Hiện tại, tôi đã đọc qua tài liệu cho nhiều khung công tác khác nhau (laravel, fuel, codeigniter, symfony) và tôi thấy rằng có quá nhiều khía cạnh khác nhau của khung công tác mà tôi cần phải cảm thấy thoải mái khi sử dụng quyết định cố gắng tìm hiểu từng phần chính một cách riêng lẻ trước khi cố gắng sử dụng chúng trong chính các khung công tác.

Tôi đã dành hàng giờ để tìm hiểu các ý nghĩa khác nhau, xem qua phản hồi ngăn xếp và đọc các bài viết khác nhau để hiểu IoC là gì và cách sử dụng nó để quản lý phụ thuộc một cách chính xác và tôi tin rằng tôi hiểu ý nghĩa của nó. Tôi vẫn còn màu xám về cách thực hiện nó một cách chính xác. Tôi nghĩ cách tốt nhất để mọi người đọc điều này để giúp tôi là đưa ra những hiểu biết hiện tại về container IoC và tiêm phụ thuộc, và sau đó để những người có hiểu biết tốt hơn tôi chỉ ra nơi hiểu biết của tôi thiếu.

hiểu biết của tôi:

  • Một phụ thuộc là khi một thể hiện của ClassA đòi hỏi một thể hiện của ClassB để nhanh chóng một thể hiện mới của ClassA.
  • Tiêm phụ thuộc là khi ClassA được chuyển một thể hiện của ClassB, hoặc thông qua một tham số trong hàm tạo của ClassA hoặc thông qua một hàm ~ DependencyNameHere ~ (~ DependencyNameHere ~ $ param). (Đây là một trong những khu vực tôi không hoàn toàn chắc chắn).
  • Vùng chứa IoC là một lớp đơn (chỉ có thể có 1 thể hiện tại bất kỳ thời điểm nào), nơi cách thức cụ thể của các đối tượng instantiating của lớp đó cho dự án này có thể được đăng ký. Here's a link to an example of what I'm trying to describe along with the class definition for the IoC container I've been using

Vì vậy, tại thời điểm này, tôi bắt đầu thử sử dụng vùng chứa IoC cho các tình huống phức tạp hơn. Bây giờ có vẻ như để sử dụng container IoC, tôi bị giới hạn bởi một mối quan hệ có-một cho khá nhiều lớp mà tôi muốn tạo ra có các phụ thuộc mà nó muốn định nghĩa trong thùng chứa IoC. Điều gì xảy ra nếu tôi muốn tạo một lớp thừa kế một lớp, nhưng chỉ khi lớp cha đã được tạo theo một cách cụ thể mà nó đã được đăng ký trong thùng chứa IoC. Ví dụ: Tôi muốn tạo một lớp con của mysqli, nhưng tôi muốn đăng ký lớp này trong thùng chứa IoC để chỉ khởi tạo với lớp cha được xây dựng theo cách mà trước đây tôi đã đăng ký trong thùng chứa IoC. Tôi không thể nghĩ ra một cách để làm điều này mà không cần sao chép mã (và vì đây là một dự án học tập, tôi đang cố gắng giữ nó như là 'thuần khiết' nhất có thể). Here are some more examples of what I am trying to describe.

Vì vậy, đây là một số câu hỏi của tôi:

  • phải là những gì tôi đang cố gắng để làm trên có thể mà không vi phạm một số nguyên tắc OOP? Tôi biết trong c + + Tôi có thể sử dụng bộ nhớ động và một nhà xây dựng bản sao để thực hiện nó, nhưng tôi đã không thể tìm thấy loại chức năng trong php.(Tôi sẽ thừa nhận rằng tôi có rất ít kinh nghiệm sử dụng bất kỳ phương pháp ma thuật nào khác ngoài __construct, nhưng từ đọc và __clone nếu tôi hiểu đúng, tôi không thể sử dụng trong hàm tạo nó để làm cho lớp con được khởi tạo một bản sao của một ví dụ của lớp cha).
  • Tất cả các định nghĩa lớp phụ thuộc của tôi nên liên quan đến IoC ở đâu? (IoC.php của tôi có nên có một loạt các require_once ('dependencyClassDefinition.php') ở đầu? Phản ứng ruột của tôi là có một cách tốt hơn, nhưng tôi đã không đưa ra một chưa)
  • Tệp gì Tôi có nên đăng ký đối tượng của mình không? Hiện đang thực hiện tất cả các cuộc gọi tới IoC :: register() trong tệp IoC.php sau khi định nghĩa lớp.
  • Tôi có cần đăng ký phụ thuộc vào IoC trước khi đăng ký một lớp cần sự phụ thuộc đó không? Vì tôi không gọi hàm ẩn danh cho đến khi tôi thực sự khởi tạo một đối tượng được đăng ký trong IoC, tôi đoán là không, nhưng nó vẫn là một mối quan tâm.
  • Có điều gì khác mà tôi đang xem mà tôi nên làm hoặc sử dụng không? Tôi đang cố gắng thực hiện từng bước một, nhưng tôi cũng không muốn biết rằng mã của tôi sẽ được tái sử dụng và quan trọng nhất là ai đó không biết gì về dự án của tôi có thể đọc và hiểu nó.

Tôi biết điều này rất dài, và chỉ muốn cảm ơn trước bất kỳ ai dành thời gian đọc nó, và thậm chí nhiều hơn để mọi người chia sẻ kiến ​​thức của họ.

Trả lời

108

Đặt đơn giản (vì nó không phải là vấn đề chỉ giới hạn ở thế giới OOP), một phụ thuộc phụ thuộc là tình huống mà thành phần A cần (phụ thuộc) thành phần B để thực hiện công việc cần làm. Từ này cũng được sử dụng để mô tả thành phần phụ thuộc vào trong kịch bản này. Để đặt này trong OOP ngữ/PHP, xem xét ví dụ sau đây với sự tương tự xe bắt buộc:

class Car { 

    public function start() { 
     $engine = new Engine(); 
     $engine->vroom(); 
    } 

} 

Carphụ thuộc trên Engine. EngineCar 's phụ thuộc. Tuy nhiên, đoạn mã này khá tệ, bởi vì:

  • phụ thuộc ngầm; bạn không biết nó ở đó cho đến khi bạn kiểm tra các mã của Car
  • các lớp được kết hợp chặt chẽ; bạn không thể thay thế Engine bằng MockEngine cho mục đích thử nghiệm hoặc TurboEngine mở rộng bản gốc mà không sửa đổi Car.
  • Có vẻ ngớ ngẩn khi một chiếc xe có thể tự chế tạo động cơ, phải không?

Dependency Injection là một cách để giải quyết tất cả những vấn đề này bằng cách làm cho một thực tế rằng Car cần Engine rõ ràng và dứt khoát cung cấp nó với một:

class Car { 

    protected $engine; 

    public function __construct(Engine $engine) { 
     $this->engine = $engine; 
    } 

    public function start() { 
     $this->engine->vroom(); 
    } 

} 

$engine = new SuperDuperTurboEnginePlus(); // a subclass of Engine 
$car = new Car($engine); 

Trên đây là một ví dụ về constructor injection, trong đó phụ thuộc (đối tượng phụ thuộc) được cung cấp cho người phụ thuộc (người tiêu dùng) thông qua hàm tạo lớp. Một cách khác sẽ hiển thị phương thức setEngine trong lớp Car và sử dụng nó để tiêm một phiên bản Engine.Điều này được gọi là tiêm setter và hữu ích chủ yếu cho các phụ thuộc được cho là được hoán đổi vào thời gian chạy.

Bất kỳ dự án không tầm thường nào bao gồm một loạt các thành phần phụ thuộc lẫn nhau và dễ dàng mất dấu theo dõi những gì được tiêm ở nơi khá nhanh. Một container phụ thuộc tiêm là một đối tượng biết làm thế nào để nhanh chóng và cấu hình các đối tượng khác, biết mối quan hệ của họ với các đối tượng khác trong dự án và tiêm phụ thuộc cho bạn. Điều này cho phép bạn tập trung quản lý tất cả các phụ thuộc (liên) của dự án và, quan trọng hơn, làm cho nó có thể thay đổi/mô phỏng một hoặc nhiều trong số chúng mà không cần phải chỉnh sửa một loạt các vị trí trong mã của bạn.

Hãy bỏ qua tương tự xe và xem những gì OP đang cố gắng đạt được làm ví dụ. Giả sử chúng ta có đối tượng Database tùy thuộc vào đối tượng mysqli. Giả sử chúng ta muốn sử dụng một lớp container indection phụ thuộc thực sự là DIC để lộ hai phương thức: register($name, $callback) để đăng ký một cách tạo đối tượng dưới tên đã cho và resolve($name) để lấy đối tượng từ tên đó. thiết lập container của chúng tôi sẽ giống như thế này:

$dic = new DIC(); 
$dic->register('mysqli', function() { 
    return new mysqli('somehost','username','password'); 
}); 
$dic->register('database', function() use($dic) { 
    return new Database($dic->resolve('mysqli')); 
}); 

Chú ý chúng ta đang nói container của chúng tôi để lấy một thể hiện của mysqlitừ bản thân để lắp ráp một thể hiện của Database. Sau đó, để có được một trường hợp Database với sự phụ thuộc của nó tự động tiêm, chúng tôi sẽ đơn giản:

$database = $dic->resolve('database'); 

Đó là ý chính của nó. Một chút phức tạp hơn nhưng vẫn tương đối đơn giản và dễ dàng để nắm bắt container PHP DI/IoC là Pimple. Kiểm tra tài liệu của nó để biết thêm ví dụ.


Về mã và câu hỏi OP của:

  • Không sử dụng lớp tĩnh hoặc một singleton cho container của bạn (hoặc bất cứ điều gì khác cho rằng vấn đề); they're both evil. Kiểm tra Pimple để thay thế.
  • Quyết định xem bạn có muốn mysqliWrapper lớp mở rộngmysql hoặc phụ thuộc trên đó.
  • Bằng cách gọi IoC từ trong vòng mysqliWrapper bạn đang trao đổi một phụ thuộc cho người khác. Đối tượng của bạn không nên biết hoặc sử dụng vùng chứa; nếu không nó không phải là DIC nữa, đó là Service Locator (anti) pattern.
  • Bạn không cần phải require một tệp lớp trước khi đăng ký nó trong vùng chứa vì bạn không biết mình có sử dụng đối tượng của lớp đó hay không. Làm tất cả thiết lập vùng chứa của bạn ở một nơi. Nếu bạn không sử dụng trình tải tự động, bạn có thể require bên trong chức năng ẩn danh mà bạn đăng ký với vùng chứa.

Tài nguyên bổ sung:

+4

Với ví dụ này, nó trông giống như những gì bạn đang tạo ra là các nhà máy như một mảng kết hợp được tôn vinh (biến '$ dic' này). Pimple là một mảng kết hợp được tôn vinh. Gọi '$ dic-> resolve()' là một trình định vị dịch vụ và tạo ra một kẻ nói dối về API đối tượng của bạn. Bạn đọc Fowler và các cuộc đàm phán mã sạch - chưa cung cấp mụn và chống mẫu như là giải pháp. Tôi nghĩ rằng bạn cần phải nghĩ lại phần trên '$ database =' ... nếu không thì sẽ rất tuyệt khi thấy ai đó nói rằng không sử dụng statics :-) – Jimbo

+0

@Jimbo Nó chỉ là một dịch vụ định vị nếu bạn sử dụng nó như một chuyển nó xung quanh cho các vật thể của bạn, đó là điều tôi cảnh báo rõ ràng trong câu trả lời của tôi. Không có gì "chống khuôn mẫu" trong việc sử dụng một container đơn giản tương tự để xây dựng đồ thị đối tượng của bạn ở một nơi duy nhất ở đâu đó gần điểm vào của ứng dụng, mặc dù. BTW, có rất nhiều thứ có thể phát triển thành "mảng kết hợp tôn vinh" bằng ngôn ngữ có mảng liên kết/hashmaps là một trong rất ít các loại lõi không vô hướng ;-) – lafor

+1

Lol, điểm công bằng. Tuy nhiên, '$ database = $ dic-> resolve ('database');' và cảnh báo của định vị dịch vụ dường như không được liên kết. Tôi đã không nhận được rằng có một cảnh báo cho việc sử dụng này. Nếu bạn quan tâm đến loại công cụ này, hãy để tôi thổi tâm trí của bạn: [link] (https://github.com/rdlowrey/Auryn). – Jimbo

Các vấn đề liên quan