Sự khác biệt giữa sử dụng tiêm phụ thuộc với container và sử dụng công cụ định vị dịch vụ là gì?


107

Tôi hiểu rằng trực tiếp khởi tạo các phụ thuộc bên trong một lớp được coi là thực hành xấu. Điều này có ý nghĩa như làm rất chặt chẽ mọi thứ mà lần lượt làm cho việc kiểm tra rất khó khăn.

Hầu như tất cả các khung mà tôi đi qua dường như ủng hộ việc tiêm phụ thuộc với một container hơn là sử dụng các trình định vị dịch vụ. Cả hai dường như đạt được điều tương tự bằng cách cho phép lập trình viên chỉ định đối tượng nào sẽ được trả về khi một lớp yêu cầu một phụ thuộc.

Sự khác biệt giữa hai là gì? Tại sao tôi lại chọn cái này?


3
Điều này đã được hỏi (và trả lời) trên StackOverflow: stackoverflow.com/questions/8900710/
mẹo

2
Theo tôi, không có gì lạ khi các nhà phát triển lừa dối người khác nghĩ rằng công cụ định vị dịch vụ của họ là tiêm phụ thuộc. Họ làm điều đó bởi vì tiêm phụ thuộc thường được cho là tiên tiến hơn.
Gherman


1
Tôi ngạc nhiên không ai đề cập rằng với Bộ định vị dịch vụ, khó biết khi nào tài nguyên nhất thời có thể bị phá hủy. Tôi luôn nghĩ rằng đó là lý do chính.
Buh Buh

Câu trả lời:


126

Khi chính đối tượng chịu trách nhiệm yêu cầu các phụ thuộc của nó, trái ngược với việc chấp nhận chúng thông qua một nhà xây dựng, nó sẽ ẩn một số thông tin cần thiết. Nó chỉ tốt hơn một chút so với trường hợp sử dụng rất chặt chẽ newđể khởi tạo các phụ thuộc của nó. Nó giảm khớp nối vì trên thực tế bạn có thể thay đổi các phụ thuộc mà nó nhận được, nhưng nó vẫn có một phụ thuộc mà nó không thể lắc: bộ định vị dịch vụ. Điều đó trở thành điều mà mọi thứ đều phụ thuộc vào.

Một thùng chứa cung cấp các phụ thuộc thông qua các đối số của hàm tạo cho sự rõ ràng nhất. Chúng ta thấy ngay phía trước rằng một đối tượng cần cả an AccountRepositoryvà a PasswordStrengthEvaluator. Khi sử dụng công cụ định vị dịch vụ, thông tin đó ít rõ ràng hơn ngay lập tức. Bạn sẽ thấy ngay một trường hợp mà một đối tượng có, ồ, 17 phụ thuộc và tự nói với mình, "Hmm, có vẻ như rất nhiều. Chuyện gì đang xảy ra ở đó?" Các cuộc gọi đến một bộ định vị dịch vụ có thể được trải rộng xung quanh các phương thức khác nhau và ẩn đằng sau logic có điều kiện và bạn có thể không nhận ra mình đã tạo ra một "lớp Chúa" - một phương thức thực hiện mọi thứ. Có lẽ lớp đó có thể được tái cấu trúc thành 3 lớp nhỏ hơn, tập trung hơn và do đó dễ kiểm tra hơn.

Bây giờ hãy xem xét thử nghiệm. Nếu một đối tượng sử dụng trình định vị dịch vụ để lấy các phụ thuộc của nó, khung kiểm tra của bạn cũng sẽ cần một trình định vị dịch vụ. Trong thử nghiệm, bạn sẽ định cấu hình bộ định vị dịch vụ để cung cấp các phụ thuộc cho đối tượng được thử nghiệm - có thể là a FakeAccountRepositoryvà a VeryForgivingPasswordStrengthEvaluator, sau đó chạy thử nghiệm. Nhưng đó là công việc nhiều hơn là chỉ định các phụ thuộc trong hàm tạo của đối tượng. Và khung kiểm tra của bạn cũng trở nên phụ thuộc vào công cụ định vị dịch vụ. Đó là một điều khác mà bạn phải cấu hình trong mọi bài kiểm tra, điều này làm cho bài kiểm tra viết kém hấp dẫn hơn.

Tra cứu "Serivce Locator is a Anti-Pattern" cho bài viết của Mark Seeman về nó. Nếu bạn đang ở trong thế giới .Net, hãy lấy cuốn sách của anh ấy. Nó rất tốt.


1
Đọc câu hỏi tôi đã nghĩ về Mã thích ứng thông qua C # của Gary McLean Hall, nó phù hợp khá tốt với câu trả lời này. Nó có một số tương tự tốt như bộ định vị dịch vụ là chìa khóa cho sự an toàn; khi được trao cho một lớp, nó có thể tạo ra sự phụ thuộc theo ý muốn mà có thể không dễ dàng nhận ra.
Dennis

10
Một điều IMO cần phải được bổ sung vào constructor supplied dependenciesvs service locatorlà một trong những nguyên có thể được verfied thời gian biên dịch, trong khi một sau có thể chỉ được xác nhận thời gian chạy.
andras

2
Ngoài ra, một bộ định vị dịch vụ không ngăn cản một thành phần có nhiều phụ thuộc. Nếu bạn phải thêm 10 tham số cho hàm tạo của mình, bạn có cảm giác khá tốt, có gì đó không đúng với mã của bạn. Tuy nhiên, nếu bạn tình cờ thực hiện 10 cuộc gọi tĩnh đến một công cụ định vị dịch vụ, có thể không rõ ràng rằng bạn đã gặp phải một số vấn đề. Tôi đã làm việc trong một dự án lớn với một công cụ định vị dịch vụ và đây là vấn đề lớn nhất. Thật quá dễ dàng để rút ngắn một con đường mới thay vì ngồi lại, phản xạ, thiết kế lại và tái cấu trúc.
Pace

1
Về thử nghiệm - But that's more work than specifying dependencies in the object's constructor.Tôi muốn phản đối. Với một bộ định vị dịch vụ, bạn chỉ cần xác định 3 phụ thuộc mà bạn thực sự cần cho bài kiểm tra của mình. Với DI dựa trên hàm tạo, bạn cần chỉ định TẤT CẢ 10 trong số chúng, ngay cả khi 7 không được sử dụng.
Vilx-

4
@ Vilx- Nếu bạn có nhiều tham số hàm tạo không được sử dụng thì có khả năng lớp của bạn đang vi phạm nguyên tắc Trách nhiệm duy nhất.
RB.

79

Hãy tưởng tượng bạn là một công nhân trong một nhà máy sản xuất giày .

Bạn chịu trách nhiệm lắp ráp giày và vì vậy bạn sẽ cần rất nhiều thứ để làm điều đó.

  • Da
  • Thước dây cuốn
  • Keo dán
  • Móng tay
  • cây búa
  • Cây kéo
  • Dây giày

Và như vậy.

Bạn đang làm việc trong nhà máy và bạn đã sẵn sàng để bắt đầu. Bạn có danh sách hướng dẫn về cách tiến hành, nhưng bạn chưa có bất kỳ tài liệu hoặc công cụ nào.

Trình định vị dịch vụ giống như một Quản đốc có thể giúp bạn có được những gì bạn cần.

Bạn hỏi Bộ định vị dịch vụ mỗi khi bạn cần thứ gì đó và họ sẽ đi tìm nó cho bạn. Trình định vị dịch vụ đã được thông báo trước về những gì bạn có thể yêu cầu và cách tìm thấy nó.

Bạn nên hy vọng rằng bạn không yêu cầu điều gì đó bất ngờ. Nếu Trình định vị chưa được thông báo trước về một công cụ hoặc tài liệu cụ thể, họ sẽ không thể lấy nó cho bạn và họ sẽ chỉ nhún vai với bạn.

dịch vụ định vị

Một dependency injection (DI) container giống như một hộp lớn mà được lấp đầy với tất cả những gì mọi người cần vào đầu ngày.

Khi nhà máy bắt đầu hoạt động, Big Boss được gọi là Thành phần gốc nắm lấy container và giao mọi thứ cho Người quản lý Line .

Người quản lý Line hiện có những gì họ cần để thực hiện nhiệm vụ của họ trong ngày. Họ lấy những gì họ có và chuyển những gì cần thiết cho cấp dưới của họ.

Quá trình này tiếp tục, với sự phụ thuộc nhỏ giọt xuống dây chuyền sản xuất. Cuối cùng, một thùng chứa các vật liệu và công cụ xuất hiện cho Foreman của bạn.

Quản đốc của bạn bây giờ phân phối chính xác những gì bạn cần cho bạn và các nhân viên khác, mà không cần bạn yêu cầu họ.

Về cơ bản, ngay khi bạn đi làm, mọi thứ bạn cần đã có sẵn trong một chiếc hộp đang chờ bạn. Bạn không cần biết gì về cách lấy chúng.

container tiêm phụ thuộc


45
Đây là một mô tả tuyệt vời về những gì trong số 2 điều đó, sơ đồ siêu đẹp! Nhưng điều này không trả lời cho câu hỏi "Tại sao chọn cái này hơn cái kia"?
Matthieu M.

21
"Bạn nên hy vọng rằng bạn không yêu cầu điều gì đó bất ngờ. Nếu Bộ định vị chưa được thông báo trước về một công cụ hoặc tài liệu cụ thể" Nhưng điều đó cũng có thể đúng với container DI.
Kenneth K.

5
@FrankHopkins Nếu bạn bỏ qua việc đăng ký giao diện / lớp với thùng chứa DI, thì mã của bạn sẽ vẫn được biên dịch và nó sẽ nhanh chóng bị lỗi khi chạy.
Kenneth K.

3
Cả hai đều có khả năng thất bại như nhau, tùy thuộc vào cách chúng được thiết lập. Nhưng với bộ chứa DI, bạn có nhiều khả năng gặp phải vấn đề sớm hơn, khi lớp X được xây dựng, thay vì muộn hơn, khi lớp X thực sự cần một phụ thuộc đó. Nó được cho là tốt hơn, bởi vì nó dễ dàng hơn để chạy vào vấn đề, tìm nó và giải quyết nó. Mặc dù vậy, tôi đồng ý với Kenneth rằng câu trả lời cho thấy vấn đề này chỉ tồn tại đối với người định vị dịch vụ, điều này chắc chắn không phải là trường hợp.
GolezTrol

3
Câu trả lời tuyệt vời, nhưng tôi nghĩ rằng nó bỏ lỡ một điều khác; nghĩa là, nếu bạn hỏi người quản đốc ở bất kỳ giai đoạn nào cho một con voi và anh ta biết làm thế nào để có được một con, anh ta sẽ lấy một con cho bạn không có câu hỏi nào - mặc dù bạn không cần nó. Và ai đó đang cố gắng gỡ lỗi một vấn đề trên sàn (một nhà phát triển nếu bạn muốn) sẽ tự hỏi wtf là một con voi làm việc ở đây trên bàn này, do đó làm cho họ khó khăn hơn để suy luận về những gì thực sự đã sai. Trong khi đó với DI, bạn, lớp công nhân, khai báo trước mọi phụ thuộc bạn cần trước khi bạn bắt đầu công việc, do đó làm cho nó dễ dàng hơn để lý luận.
Stephen Byrne

10

Một vài điểm bổ sung mà tôi đã tìm thấy khi quét web:

  • Việc tiêm các phụ thuộc vào hàm tạo giúp dễ hiểu hơn những gì một lớp cần. Các IDE hiện đại sẽ gợi ý những đối số mà hàm tạo chấp nhận và các kiểu của chúng. Nếu bạn sử dụng công cụ định vị dịch vụ, bạn phải đọc lớp trước khi bạn biết phụ thuộc nào là bắt buộc.
  • Tiêm phụ thuộc dường như tuân thủ nguyên tắc "nói không hỏi" nhiều hơn so với định vị dịch vụ. Bằng cách bắt buộc một phụ thuộc phải thuộc một loại cụ thể, bạn "nói" những phụ thuộc nào là bắt buộc. Không thể khởi tạo lớp mà không vượt qua các phụ thuộc cần thiết. Với công cụ định vị dịch vụ, bạn "yêu cầu" dịch vụ và nếu công cụ định vị dịch vụ không được cấu hình đúng, bạn có thể không nhận được những gì được yêu cầu.

4

Tôi đến muộn bữa tiệc này nhưng tôi không thể cưỡng lại.

Sự khác biệt giữa sử dụng tiêm phụ thuộc với container và sử dụng công cụ định vị dịch vụ là gì?

Đôi khi không có gì cả. Điều làm nên sự khác biệt là những gì biết về những gì.

Bạn biết bạn đang sử dụng công cụ định vị dịch vụ khi khách hàng tìm kiếm sự phụ thuộc biết về container. Một khách hàng biết cách tìm ra các phụ thuộc của nó, ngay cả khi đi qua một container để lấy chúng, là mẫu định vị dịch vụ.

Điều này có nghĩa là nếu bạn muốn tránh định vị dịch vụ, bạn không thể sử dụng một container? Không. Bạn chỉ cần giữ khách hàng biết về container. Sự khác biệt chính là nơi bạn sử dụng container.

Hãy nói Clientnhu cầu Dependency. Các container có một Dependency.

class Client { 
    Client() { 
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        this.dependency = (Dependency) beanfactory.getBean("dependency");        
    }
    Dependency dependency;
}

Chúng tôi chỉ theo mô hình định vị dịch vụ vì Clientbiết cách tìm Dependency. Chắc chắn rằng nó sử dụng một mã hóa cứng ClassPathXmlApplicationContextnhưng ngay cả khi bạn tiêm rằng bạn vẫn có một bộ định vị dịch vụ vì Clientcác cuộc gọi beanfactory.getBean().

Để tránh định vị dịch vụ, bạn không phải từ bỏ container này. Bạn chỉ cần di chuyển nó ra Clientđể Clientkhông biết về nó.

class EntryPoint { 
    public static void main(String[] args) {
        BeanFactory beanfactory = new ClassPathXmlApplicationContext("Beans.xml");
        Client client = (Client) beanfactory.getBean("client");

        client.start();
    }
}

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="dependency" class="Dependency">
    </bean>

    <bean id="client" class="Client">
        <constructor-arg value="dependency" />        
    </bean>
</beans>

Lưu ý làm thế nào Clientbây giờ không có ý tưởng container tồn tại:

class Client { 
    Client(Dependency dependency) { 

        this.dependency = dependency;        
    }
    Dependency dependency;
}

Di chuyển container ra khỏi tất cả các máy khách và dán nó vào chính nơi nó có thể xây dựng một biểu đồ đối tượng của tất cả các đối tượng tồn tại lâu dài của bạn. Chọn một trong những đối tượng đó để trích xuất và gọi một phương thức trên đó và bạn bắt đầu đánh dấu toàn bộ biểu đồ.

Điều đó chuyển tất cả các cấu trúc tĩnh vào các thùng chứa XML nhưng vẫn giữ cho tất cả các máy khách của bạn không biết gì về cách tìm các phụ thuộc của chúng.

Nhưng chính vẫn biết cách xác định vị trí phụ thuộc! Có nó làm. Nhưng bằng cách không truyền bá kiến ​​thức xung quanh bạn, bạn đã tránh được vấn đề cốt lõi của công cụ định vị dịch vụ. Quyết định sử dụng một container hiện được đưa ra ở một nơi và có thể được thay đổi mà không cần viết lại hàng trăm khách hàng.


1

Tôi nghĩ rằng cách dễ nhất để hiểu sự khác biệt giữa hai loại và tại sao một container DI lại tốt hơn nhiều so với bộ định vị dịch vụ là nghĩ về lý do tại sao chúng ta lại đảo ngược phụ thuộc ngay từ đầu.

Chúng tôi thực hiện đảo ngược phụ thuộc để mỗi lớp nêu rõ chính xác những gì nó phụ thuộc vào hoạt động. Chúng tôi làm như vậy bởi vì điều này tạo ra khớp nối lỏng lẻo nhất mà chúng tôi có thể đạt được. Khớp nối càng lỏng lẻo, càng dễ kiểm tra và tái cấu trúc (và thường yêu cầu tái cấu trúc ít nhất trong tương lai vì mã sạch hơn).

Hãy xem lớp sau:

public class MySpecialStringWriter
{
  private readonly IOutputProvider outputProvider;
  public MySpecialFormatter(IOutputProvider outputProvider)
  {
    this.outputProvider = outputProvider;
  }

  public void OutputString(string source)
  {
    this.outputProvider.Output("This is the string that was passed: " + source);
  }
}

Trong lớp này, chúng tôi tuyên bố rõ ràng rằng chúng tôi cần một IOutputProvider và không có gì khác để làm cho lớp này hoạt động. Điều này là hoàn toàn có thể kiểm tra và có một sự phụ thuộc vào một giao diện duy nhất. Tôi có thể di chuyển lớp này đến bất kỳ nơi nào trong ứng dụng của mình, bao gồm một dự án khác và tất cả những gì nó cần là truy cập vào giao diện IOutputProvider. Nếu các nhà phát triển khác muốn thêm một cái gì đó mới vào lớp này, đòi hỏi sự phụ thuộc thứ hai, họ phải rõ ràng về những gì họ cần trong hàm tạo.

Hãy xem cùng một lớp với một bộ định vị dịch vụ:

public class MySpecialStringWriter
{
  private readonly ServiceLocator serviceLocator;
  public MySpecialFormatter(ServiceLocator serviceLocator)
  {
    this.serviceLocator = serviceLocator;
  }

  public void OutputString(string source)
  {
    this.serviceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

Bây giờ tôi đã thêm bộ định vị dịch vụ làm phụ thuộc. Dưới đây là những vấn đề rõ ràng ngay lập tức:

  • Vấn đề đầu tiên với điều này là cần nhiều mã hơn để đạt được kết quả tương tự. Thêm mã là xấu. Nó không nhiều mã hơn nhưng nó vẫn còn nhiều hơn.
  • Vấn đề thứ hai là sự phụ thuộc của tôi không còn rõ ràng nữa . Tôi vẫn cần phải tiêm một cái gì đó vào lớp. Ngoại trừ bây giờ điều tôi muốn không rõ ràng. Nó được giấu trong một tài sản của thứ mà tôi yêu cầu. Bây giờ tôi cần quyền truy cập vào cả ServiceLocator và IOutputProvider nếu tôi muốn chuyển lớp sang một hội đồng khác.
  • Vấn đề thứ ba là một nhà phát triển phụ thuộc khác có thể được thực hiện bởi một nhà phát triển khác thậm chí không nhận ra họ đang lấy nó khi họ thêm mã vào lớp.
  • Cuối cùng, mã này khó kiểm tra hơn (ngay cả khi ServiceLocator là giao diện) vì chúng tôi phải giả định ServiceLocator và IOutputProvider thay vì chỉ IOutputProvider

Vậy tại sao chúng ta không biến trình định vị dịch vụ thành một lớp tĩnh? Hãy xem:

public class MySpecialStringWriter
{
  public void OutputString(string source)
  {
    ServiceLocator.OutputProvider.Output("This is the string that was passed: " + source);
  }
}

Điều này đơn giản hơn nhiều, phải không?

Sai lầm.

Giả sử IOutputProvider được triển khai bởi một dịch vụ web chạy rất dài, viết chuỗi trong mười lăm cơ sở dữ liệu khác nhau trên khắp thế giới và mất một thời gian rất dài để hoàn thành.

Hãy thử kiểm tra lớp này. Chúng tôi cần một triển khai khác của IOutputProvider cho thử nghiệm. Làm thế nào để chúng tôi viết bài kiểm tra?

Để làm được điều đó, chúng ta cần thực hiện một số cấu hình ưa thích trong lớp ServiceLocator tĩnh để sử dụng một triển khai IOutputProvider khác khi nó đang được thử nghiệm gọi. Ngay cả viết câu đó cũng đau đớn. Thực hiện nó sẽ là cực hình và nó sẽ là một cơn ác mộng bảo trì . Chúng ta không bao giờ cần phải sửa đổi một lớp cụ thể để kiểm tra, đặc biệt nếu lớp đó không phải là lớp chúng ta thực sự đang cố kiểm tra.

Vì vậy, bây giờ bạn còn lại với một) một bài kiểm tra gây ra thay đổi mã gây khó chịu trong lớp ServiceLocator không liên quan; hoặc b) không có bài kiểm tra nào cả. Và bạn còn lại với một giải pháp kém linh hoạt là tốt.

Vì vậy, các lớp học định vị dịch vụ được tiêm vào các nhà xây dựng. Điều đó có nghĩa là chúng ta còn lại với các vấn đề cụ thể được đề cập trước đó. Trình định vị dịch vụ yêu cầu nhiều mã hơn, nói với các nhà phát triển khác rằng họ cần những thứ không cần, khuyến khích các nhà phát triển khác viết mã tệ hơn và cho chúng tôi ít linh hoạt hơn khi chuyển tiếp.

Đặt các trình định vị dịch vụ đơn giản là tăng khả năng ghép nối trong một ứng dụng khuyến khích các nhà phát triển khác viết mã được ghép cao .


Bộ định vị dịch vụ (SL) và Dependency Injection (DI) đều giải quyết cùng một vấn đề trong cách cư xử tương tự. Sự khác biệt duy nhất nếu bạn đang "nói" DI hoặc "hỏi" SL cho các phụ thuộc.
Matthew Whited

@MatthewWhited Sự khác biệt chính là số lượng phụ thuộc ngầm mà bạn thực hiện với một bộ định vị dịch vụ. Và điều đó tạo ra một sự khác biệt lớn đối với khả năng duy trì và ổn định lâu dài của mã.
Stephen

Nó thực sự không. Bạn có cùng số lượng phụ thuộc một trong hai cách.
Matthew Whited

Ban đầu có, nhưng bạn có thể phụ thuộc với một bộ định vị dịch vụ mà không nhận ra bạn đang dùng phụ thuộc. Điều đó đánh bại một phần lớn lý do tại sao chúng ta thực hiện đảo ngược phụ thuộc ngay từ đầu. Một lớp phụ thuộc vào thuộc tính A và B, lớp khác phụ thuộc vào B và C. Đột nhiên, Trình định vị dịch vụ trở thành lớp thần. Container DI không và hai lớp chỉ phụ thuộc đúng vào A và B và B và C tương ứng. Điều này làm cho việc tái cấu trúc chúng dễ dàng hơn gấp trăm lần. Bộ định vị dịch vụ ngấm ngầm ở chỗ chúng xuất hiện giống như DI nhưng chúng không như vậy.
Stephen
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.