Thư viện phụ thuộc (DI)


230

Tôi đang cân nhắc thiết kế thư viện C #, sẽ có một số chức năng cấp cao khác nhau. Tất nhiên, các hàm cấp cao đó sẽ được triển khai bằng cách sử dụng các nguyên tắc thiết kế lớp RẮN càng nhiều càng tốt. Như vậy, có thể sẽ có các lớp dành cho người tiêu dùng sử dụng trực tiếp một cách thường xuyên và "các lớp hỗ trợ" là các phụ thuộc của các lớp "người dùng cuối" phổ biến hơn đó.

Câu hỏi là, cách tốt nhất để thiết kế thư viện là gì:

  • DI Agnellect - Mặc dù việc thêm "hỗ trợ" cơ bản cho một hoặc hai trong số các thư viện DI chung (StructMap, Ninject, v.v.) có vẻ hợp lý, tôi muốn người tiêu dùng có thể sử dụng thư viện với bất kỳ khung DI nào.
  • Không thể sử dụng DI - Nếu người tiêu dùng của thư viện không sử dụng DI, thư viện vẫn phải dễ sử dụng nhất có thể, giảm lượng công việc mà người dùng phải làm để tạo tất cả các phụ thuộc "không quan trọng" này chỉ để có được các lớp "thực" mà họ muốn sử dụng.

Suy nghĩ hiện tại của tôi là cung cấp một vài "mô-đun đăng ký DI" cho các thư viện DI chung (ví dụ: sổ đăng ký StructMap, mô-đun Ninject) và một tập hợp hoặc các lớp Factory không phải là DI và chứa khớp nối với một vài nhà máy đó.

Suy nghĩ?


Tham chiếu mới về bài viết liên kết bị hỏng (RẮN): butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

Câu trả lời:


360

Điều này thực sự đơn giản để làm một khi bạn hiểu rằng DI là về các mẫu và nguyên tắc , không phải công nghệ.

Để thiết kế API theo cách không tin tưởng DI Container, hãy làm theo các nguyên tắc chung sau:

Chương trình cho một giao diện, không phải là một triển khai

Nguyên tắc này thực sự là một trích dẫn (từ bộ nhớ) từ Thiết kế mẫu , nhưng nó phải luôn là mục tiêu thực sự của bạn . DI chỉ là một phương tiện để đạt được kết thúc đó .

Áp dụng nguyên tắc Hollywood

Nguyên tắc Hollywood theo thuật ngữ DI nói: Đừng gọi DI Container, nó sẽ gọi cho bạn .

Không bao giờ yêu cầu trực tiếp cho một phụ thuộc bằng cách gọi một container từ trong mã của bạn. Yêu cầu nó hoàn toàn bằng cách sử dụng Con Contortor tiêm .

Sử dụng tiêm xây dựng

Khi bạn cần một phụ thuộc, yêu cầu nó tĩnh thông qua hàm tạo:

public class Service : IService
{
    private readonly ISomeDependency dep;

    public Service(ISomeDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    public ISomeDependency Dependency
    {
        get { return this.dep; }
    }
}

Lưu ý cách lớp Service đảm bảo các bất biến của nó. Khi một thể hiện được tạo, sự phụ thuộc được đảm bảo có sẵn do sự kết hợp của Điều khoản bảo vệ và readonlytừ khóa.

Sử dụng Tóm tắt Factory nếu bạn cần một đối tượng có thời gian tồn tại ngắn

Các phụ thuộc được tiêm Con Contortor tiêm có xu hướng tồn tại lâu dài, nhưng đôi khi bạn cần một đối tượng tồn tại trong thời gian ngắn hoặc để xây dựng sự phụ thuộc dựa trên giá trị chỉ được biết trong thời gian chạy.

Xem điều này để biết thêm thông tin.

Chỉ sáng tác tại thời điểm có trách nhiệm cuối cùng

Giữ các đối tượng tách rời cho đến cuối cùng. Thông thường, bạn có thể đợi và kết nối mọi thứ trong điểm vào của ứng dụng. Đây được gọi là Thành phần gốc .

Thêm chi tiết tại đây:

Đơn giản hóa bằng cách sử dụng Mặt tiền

Nếu bạn cảm thấy rằng API kết quả trở nên quá phức tạp đối với người dùng mới làm quen, bạn luôn có thể cung cấp một vài lớp Mặt tiền gói gọn các kết hợp phụ thuộc chung.

Để cung cấp Mặt tiền linh hoạt với mức độ khám phá cao, bạn có thể xem xét việc cung cấp Fluent Builders. Một cái gì đó như thế này:

public class MyFacade
{
    private IMyDependency dep;

    public MyFacade()
    {
        this.dep = new DefaultDependency();
    }

    public MyFacade WithDependency(IMyDependency dependency)
    {
        this.dep = dependency;
        return this;
    }

    public Foo CreateFoo()
    {
        return new Foo(this.dep);
    }
}

Điều này sẽ cho phép người dùng tạo Foo mặc định bằng cách viết

var foo = new MyFacade().CreateFoo();

Tuy nhiên, rất có thể phát hiện ra rằng có thể cung cấp một phụ thuộc tùy chỉnh và bạn có thể viết

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

Nếu bạn tưởng tượng rằng lớp MyFacade gói gọn rất nhiều phụ thuộc khác nhau, tôi hy vọng nó rõ ràng làm thế nào nó sẽ cung cấp các giá trị mặc định phù hợp trong khi vẫn có thể mở rộng được.


FWIW, rất lâu sau khi viết câu trả lời này, tôi đã mở rộng các khái niệm trong tài liệu này và viết một bài đăng blog dài hơn về Thư viện thân thiện với DI và một bài đăng đồng hành về Khung thân thiện với DI .


21
Mặc dù điều này nghe có vẻ hay trên giấy, nhưng theo kinh nghiệm của tôi, một khi bạn có nhiều bộ phận bên trong tương tác theo những cách phức tạp, bạn sẽ có rất nhiều nhà máy để quản lý, khiến việc bảo trì khó khăn hơn. Ngoài ra, các nhà máy sẽ phải quản lý lối sống của các thành phần được tạo ra của họ, một khi bạn cài đặt thư viện trong một thùng chứa thực sự, điều này sẽ mâu thuẫn với quản lý lối sống của chính người chứa. Các nhà máy và mặt tiền cản trở các container thực sự.
Mauricio Scheffer

4
Tôi vẫn chưa tìm thấy một dự án làm tất cả những điều này.
Mauricio Scheffer

31
Chà, đó là cách chúng tôi phát triển phần mềm tại Safewhere, vì vậy chúng tôi không chia sẻ kinh nghiệm của bạn ...
Mark Seemann

19
Tôi nghĩ rằng các mặt tiền nên được mã hóa bằng tay vì chúng đại diện cho sự kết hợp các thành phần đã biết (và có lẽ phổ biến). Không cần thiết phải chứa DI vì mọi thứ đều có thể được nối bằng tay (nghĩ rằng DI của Poor Man). Hãy nhớ rằng Facade chỉ là một lớp tiện lợi tùy chọn cho người dùng API của bạn. Người dùng nâng cao có thể vẫn muốn bỏ qua Mặt tiền và kết nối các thành phần theo ý thích của riêng họ. Họ có thể muốn sử dụng DI Contaier của riêng họ cho việc này, vì vậy tôi nghĩ sẽ không có ích gì khi buộc một DI Container cụ thể theo họ nếu họ không sử dụng nó. Có thể nhưng không nên
Mark Seemann

8
Đây có thể là câu trả lời hay nhất tôi từng thấy trên SO.
Nick Hodges

40

Thuật ngữ "tiêm phụ thuộc" hoàn toàn không liên quan gì đến bộ chứa IoC, mặc dù bạn có xu hướng thấy chúng được đề cập cùng nhau. Nó đơn giản có nghĩa là thay vì viết mã của bạn như thế này:

public class Service
{
    public Service()
    {
    }

    public void DoSomething()
    {
        SqlConnection connection = new SqlConnection("some connection string");
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        // Do something with connection and identity variables
    }
}

Bạn viết nó như thế này:

public class Service
{
    public Service(IDbConnection connection, IIdentity identity)
    {
        this.Connection = connection;
        this.Identity = identity;
    }

    public void DoSomething()
    {
        // Do something with Connection and Identity properties
    }

    protected IDbConnection Connection { get; private set; }
    protected IIdentity Identity { get; private set; }
}

Đó là, bạn làm hai điều khi bạn viết mã của mình:

  1. Dựa vào các giao diện thay vì các lớp bất cứ khi nào bạn nghĩ rằng việc triển khai có thể cần phải thay đổi;

  2. Thay vì tạo các thể hiện của các giao diện này bên trong một lớp, hãy chuyển chúng dưới dạng đối số của hàm tạo (thay vào đó, chúng có thể được gán cho các thuộc tính công cộng; cái trước là hàm tạo của hàm tạo , lớp sau là hàm thuộc tính ).

Không ai trong số này giả định sự tồn tại của bất kỳ thư viện DI nào và nó thực sự không làm cho mã trở nên khó viết hơn nếu không có.

Nếu bạn đang tìm kiếm một ví dụ về điều này, thì không có gì khác ngoài chính .NET Framework:

  • List<T>thực hiện IList<T>. Nếu bạn thiết kế lớp của mình để sử dụng IList<T>(hoặc IEnumerable<T>), bạn có thể tận dụng các khái niệm như lười tải, như Linq sang SQL, Linq đến Thực thể và NHibernate đều thực hiện phía sau hậu trường, thường là thông qua việc tiêm thuộc tính. Một số lớp khung thực sự chấp nhận một IList<T>đối số như một hàm tạo, chẳng hạn như BindingList<T>, được sử dụng cho một số tính năng liên kết dữ liệu.

  • Linq to SQL và EF được xây dựng hoàn toàn xung quanh IDbConnectionvà các giao diện liên quan, có thể được truyền qua các nhà xây dựng công cộng. Bạn không cần phải sử dụng chúng, mặc dù; các hàm tạo mặc định hoạt động tốt với một chuỗi kết nối nằm trong tệp cấu hình ở đâu đó.

  • Nếu bạn từng làm việc trên các thành phần WinForms, bạn xử lý "dịch vụ", như INameCreationServicehoặc IExtenderProviderService. Bạn thậm chí không thực sự biết các lớp cụ thể là gì . .NET thực sự có bộ chứa IoC riêng IContainer, được sử dụng cho việc này và Componentlớp có một GetServicephương thức là trình định vị dịch vụ thực tế. Tất nhiên, không có gì ngăn cản bạn sử dụng bất kỳ hoặc tất cả các giao diện này mà không có IContainerhoặc bộ định vị cụ thể đó. Các dịch vụ chỉ được kết hợp lỏng lẻo với container.

  • Hợp đồng trong WCF được xây dựng hoàn toàn xung quanh các giao diện. Lớp dịch vụ cụ thể thực tế thường được tham chiếu theo tên trong tệp cấu hình, về cơ bản là DI. Nhiều người không nhận ra điều này nhưng hoàn toàn có thể trao đổi hệ thống cấu hình này với một bộ chứa IoC khác. Có lẽ thú vị hơn, các hành vi dịch vụ là tất cả các trường hợp IServiceBehaviorcó thể được thêm vào sau này. Một lần nữa, bạn có thể dễ dàng kết nối nó vào một thùng chứa IoC và để nó chọn các hành vi có liên quan, nhưng tính năng này hoàn toàn có thể sử dụng mà không cần một.

Vân vân và vân vân. Bạn sẽ tìm thấy DI ở khắp mọi nơi trong .NET, chỉ là thông thường nó được thực hiện hoàn hảo đến mức bạn thậm chí không nghĩ đó là DI.

Nếu bạn muốn thiết kế thư viện hỗ trợ DI của mình để có thể sử dụng tối đa thì đề xuất tốt nhất có lẽ là cung cấp triển khai IoC mặc định của riêng bạn bằng cách sử dụng một thùng chứa nhẹ. IContainerlà một lựa chọn tuyệt vời cho việc này vì nó là một phần của .NET Framework.


2
Sự trừu tượng hóa thực sự cho một container là IServiceProvider, không phải IContainer.
Mauricio Scheffer

2
@Maurermo: Tất nhiên là bạn đúng, nhưng hãy thử giải thích với một người chưa bao giờ sử dụng hệ thống LWC tại sao IContainerthực sự không phải là container trong một đoạn. ;)
Aaronaught

Aaron, chỉ tò mò về lý do tại sao bạn sử dụng thiết lập riêng tư; thay vì chỉ xác định các lĩnh vực là chỉ đọc?
ngày

@Jreeter: Bạn có nghĩa là tại sao họ không private readonlylĩnh vực? Thật tuyệt nếu chúng chỉ được sử dụng bởi lớp khai báo nhưng OP đã chỉ định rằng đây là mã cấp độ khung / thư viện, hàm ý phân lớp. Trong những trường hợp như vậy, bạn thường muốn các phụ thuộc quan trọng tiếp xúc với các lớp con. Tôi có thể đã viết một private readonlytrường một thuộc tính với getters / setters rõ ràng, nhưng ... lãng phí không gian trong một ví dụ, và nhiều mã hơn để duy trì trong thực tế, không có lợi ích thực sự.
Aaronaught

Cảm ơn bạn đã làm rõ! Tôi cho rằng các trường chỉ đọc sẽ có thể được truy cập bởi đứa trẻ.
3

5

EDIT 2015 : thời gian đã trôi qua, tôi nhận ra rằng toàn bộ điều này là một sai lầm rất lớn. Các container IoC rất tệ và DI là một cách rất kém để đối phó với các tác dụng phụ. Có hiệu quả, tất cả các câu trả lời ở đây (và chính câu hỏi) là phải tránh. Đơn giản chỉ cần lưu ý đến các tác dụng phụ, tách chúng khỏi mã thuần túy và mọi thứ khác rơi vào vị trí hoặc là sự phức tạp không liên quan và không cần thiết.

Câu trả lời gốc sau:


Tôi đã phải đối mặt với quyết định tương tự trong khi phát triển SolrNet . Tôi bắt đầu với mục tiêu thân thiện với DI và không tin tưởng vào container, nhưng khi tôi thêm ngày càng nhiều thành phần nội bộ, các nhà máy nội bộ nhanh chóng trở nên không thể quản lý được và thư viện kết quả là không thể tin được.

Cuối cùng tôi đã viết bộ chứa IoC nhúng rất đơn giản của riêng mình đồng thời cung cấp một cơ sở Windsormô-đun Ninject . Việc tích hợp thư viện với các thùng chứa khác chỉ là vấn đề kết nối đúng các thành phần, vì vậy tôi có thể dễ dàng tích hợp nó với Autofac, Unity, StructMap, bất cứ điều gì.

Nhược điểm của việc này là tôi mất khả năng chỉ cần newlên dịch vụ. Tôi cũng lấy một sự phụ thuộc vào CommonServiceLocator mà tôi có thể tránh được (tôi có thể tái cấu trúc nó trong tương lai) để làm cho container được nhúng dễ thực hiện hơn.

Thêm chi tiết trong bài viết trên blog này .

MassTransit dường như dựa vào một cái gì đó tương tự. Nó có giao diện IObjectBuilder thực sự là IServiceLocator của CommonServiceLocator với một vài phương thức, sau đó nó thực hiện điều này cho mỗi container, tức là NinjectObjectBuilder và một mô-đun / cơ sở thông thường, ví dụ MassTransitModule . Sau đó, nó dựa vào IObjectBuilder để khởi tạo những gì nó cần. Tất nhiên đây là một cách tiếp cận hợp lệ, nhưng cá nhân tôi không thích nó lắm vì nó thực sự đi qua container quá nhiều, sử dụng nó như một công cụ định vị dịch vụ.

MonoRail cũng thực hiện bộ chứa riêng của mình , trong đó thực hiện IServiceProvider cũ tốt . Container này được sử dụng trong toàn bộ khung này thông qua giao diện hiển thị các dịch vụ nổi tiếng . Để có được thùng chứa bê tông, nó có một bộ định vị nhà cung cấp dịch vụ tích hợp . Cơ sở Windsor chỉ định vị nhà cung cấp dịch vụ này cho Windsor, làm cho nó trở thành nhà cung cấp dịch vụ được chọn.

Điểm mấu chốt: không có giải pháp hoàn hảo. Như với bất kỳ quyết định thiết kế nào, vấn đề này đòi hỏi sự cân bằng giữa tính linh hoạt, khả năng bảo trì và sự thuận tiện.


12
Trong khi tôi tôn trọng ý kiến ​​của bạn, tôi thấy bạn quá khái quát "tất cả các câu trả lời khác ở đây đã lỗi thời và nên tránh" vô cùng ngây thơ. Nếu chúng ta học được bất cứ điều gì kể từ đó, thì việc tiêm phụ thuộc là một câu trả lời cho những hạn chế về ngôn ngữ. Không phát triển hoặc từ bỏ các ngôn ngữ hiện tại của chúng tôi, có vẻ như chúng tôi sẽ cần DI để làm phẳng các kiến ​​trúc của chúng tôi và đạt được tính mô đun. IoC như một khái niệm chung chỉ là hệ quả của những mục tiêu này và chắc chắn là một mục tiêu mong muốn. Các bộ chứa IoC hoàn toàn không cần thiết cho cả IoC hoặc DI, và tính hữu dụng của chúng phụ thuộc vào các thành phần mô-đun chúng tôi tạo ra.
tne

@tne Việc bạn đề cập đến tôi là "cực kỳ ngây thơ" là một quảng cáo không được hoan nghênh. Tôi đã viết các ứng dụng phức tạp mà không có DI hoặc IoC bằng C #, VB.NET, Java (ngôn ngữ bạn nghĩ "cần" IoC rõ ràng đánh giá theo mô tả của bạn), chắc chắn không phải là về giới hạn ngôn ngữ. Đó là về những hạn chế về khái niệm. Tôi mời bạn tìm hiểu về lập trình chức năng thay vì dùng đến các cuộc tấn công quảng cáo và tranh luận kém.
Mauricio Scheffer

18
Tôi đã đề cập tôi thấy tuyên bố của bạn là ngây thơ; Điều này không nói gì về cá nhân bạn và tất cả là về ý kiến ​​của tôi. Xin đừng cảm thấy bị xúc phạm bởi một người lạ ngẫu nhiên. Ngụ ý rằng tôi không biết gì về tất cả các phương pháp lập trình chức năng có liên quan cũng không hữu ích; bạn không biết điều đó và bạn đã sai. Tôi biết bạn có kiến ​​thức ở đó (nhanh chóng nhìn vào blog của bạn), đó là lý do tại sao tôi thấy khó chịu khi một anh chàng có vẻ hiểu biết sẽ nói những điều như "chúng ta không cần IoC". IoC xảy ra theo nghĩa đen ở khắp mọi nơi trong lập trình chức năng (..)
tne

4
và trong phần mềm cấp cao nói chung. Trên thực tế, các ngôn ngữ chức năng (và nhiều phong cách khác) thực tế chứng minh rằng họ giải quyết IoC mà không cần DI, tôi nghĩ cả hai chúng tôi đều đồng ý với điều đó, và đó là tất cả những gì tôi muốn nói. Nếu bạn quản lý mà không có DI trong các ngôn ngữ bạn đề cập, bạn đã làm điều đó như thế nào? Tôi giả sử không bằng cách sử dụng ServiceLocator. Nếu bạn áp dụng các kỹ thuật chức năng, bạn sẽ kết thúc tương đương với các bao đóng, đó là các lớp được thêm vào phụ thuộc (trong đó các phụ thuộc là các biến đóng). Làm thế nào khác? (Tôi thực sự tò mò, bởi vì tôi không giống như DI một trong hai.)
tne

1
Các container IoC là từ điển có thể thay đổi toàn cầu, lấy đi tham số làm công cụ lý luận, do đó cần tránh. IoC (khái niệm) như trong "đừng gọi cho chúng tôi, chúng tôi sẽ gọi cho bạn" sẽ áp dụng về mặt kỹ thuật mỗi khi bạn vượt qua một chức năng như một giá trị. Thay vì DI, mô hình hóa tên miền của bạn với ADT sau đó viết trình thông dịch (cf Free).
Mauricio Scheffer

1

Những gì tôi sẽ làm là thiết kế thư viện của mình theo cách bất khả tri của container DI để hạn chế sự phụ thuộc vào container càng nhiều càng tốt. Điều này cho phép trao đổi trên container DI cho người khác nếu cần.

Sau đó, hiển thị lớp phía trên logic DI cho người dùng của thư viện để họ có thể sử dụng bất kỳ khuôn khổ nào bạn đã chọn thông qua giao diện của bạn. Bằng cách này, họ vẫn có thể sử dụng chức năng DI mà bạn tiếp xúc và họ có thể tự do sử dụng bất kỳ khuôn khổ nào khác cho mục đích riêng của họ.

Việc cho phép người dùng thư viện cắm khung DI riêng của họ có vẻ hơi sai đối với tôi vì nó làm tăng đáng kể số lượng bảo trì. Điều này sau đó cũng trở thành một môi trường plugin hơn là DI thẳng.

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.