2010-01-12 37 views
209

Tôi đang cân nhắc thiết kế thư viện C#, sẽ có nhiều hàm mức cao khác nhau. Tất nhiên, những chức năng cấp cao này sẽ được thực hiện bằng cách sử dụng nguyên tắc thiết kế lớp học SOLID 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ợ" phụ thuộc vào các lớp "người dùng cuối" phổ biến hơn.Thư viện Dependency Inject (DI) "thân thiện"

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

  • DI Agnostic - Mặc dù việc thêm cơ bản "hỗ trợ" cho một hoặc hai trong số các thư viện DI chung (StructureMap, Ninject, vv) 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 công tác DI nào.
  • Không dùng được DI - Nếu người tiêu dùng thư viện không sử dụng DI, thư viện vẫn dễ sử dụng nhất có thể, giảm số 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 để 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 phổ biến (ví dụ: đăng ký StructureMap, mô-đun Ninject) và tập hợp hoặc các lớp Nhà máy không phải DI và chứa khớp nối cho vài nhà máy đó.

Suy nghĩ?

+0

Tham chiếu mới cho bài viết liên kết bị hỏng (SOLID): http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod –

Trả lời

325

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

Để thiết kế các API theo một cách DI container-agnostic, hãy làm theo những nguyên tắc chung:

Chương trình để một giao diện, không phải là một thực hiện

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

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

Các Nguyên tắc Hollywood về DI nói: Đừng gọi DI container, nó sẽ gọi cho bạn.

Không bao giờ trực tiếp yêu cầu sự phụ thuộc bằng cách gọi một vùng chứa từ bên trong mã của bạn. Yêu cầu nó ngầm bằng cách sử dụng Constructor Injection.

Sử dụng Constructor tiêm

Khi bạn cần một sự phụ thuộc, yêu cầu nó tĩnh thông qua các nhà xây dựng:

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; } 
    } 
} 

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

Sử dụng Abstract Factory nếu bạn cần một đối tượng ngắn ngủi

Dependencies tiêm với Constructor 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 trong thời gian ngắn, hoặc để xây dựng phụ thuộc dựa trên một giá trị chỉ được biết đến trong thời gian chạy.

Xem this để biết thêm thông tin.

Soạn chỉ ở Moment Chịu trách nhiệm cuối

Giữ đối tượng tách rời cho đến phút cuối. Thông thường, bạn có thể chờ 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.

Xem thêm chi tiết ở đâ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 các 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 số lớp học Facade mà encapsu sự kết hợp phụ thuộc muộn.

Để cung cấp Mặt tiền linh hoạt có khả năng phát hiện cao, bạn có thể cân nhắc cung cấp Trình tạo thông thạo. 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 ra một Foo mặc định bằng cách viết

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

Nó sẽ, tuy nhiên, rất có thể phát hiện rằng nó có thể cung cấp một sự 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 cho rằng lớp MyFacade đóng gói rất nhiều phụ thuộc khác nhau, tôi hy vọng rõ ràng cách nó sẽ cung cấp mặc định phù hợp khi vẫn làm nsibility có thể khám phá.


FWIW, rất lâu sau khi viết câu trả lời này, tôi mở rộng trên các khái niệm trong tài liệu này và đã viết một bài đăng blog còn khoảng DI-Friendly Libraries, và một người bạn đồng gửi về DI-Friendly Frameworks.

+17

Trong khi điều này nghe tuyệt vời trên giấy, trong kinh nghiệm của tôi một khi bạn có rất nhiều thành phần bên trong tương tác theo những cách phức tạp, bạn kết thúc với rất nhiều nhà máy để quản lý, bảo trì khó hơn. Thêm vào đó, 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, điều này sẽ xung đột với việc quản lý lối sống của chính thùng chứa. Nhà máy và mặt tiền có được trong cách của các container thực sự. –

+4

Tôi chưa tìm thấy dự án nào có * tất cả * của điều này. –

+29

Vâng, đó 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 ... –

1

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

Sau đó, hiển thị lớp phía trên logic DI cho người dùng thư viện để họ có thể sử dụng bất kỳ khung công tác nào bạn đã chọn thông qua giao diện của mình. 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 tự do sử dụng bất kỳ khung công tác nào khác cho mục đích của riêng họ.

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

8

EDIT 2015: thời gian trôi qua, tôi nhận ra rằng toàn bộ sự việc này là một sai lầm lớn. IoC container là khủng khiếp và DI là một cách rất nghèo để đố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à câu hỏi chính nó) là để tránh. Chỉ cần nhận thức được các tác dụng phụ, tách biệt chúng khỏi mã thuần túy và mọi thứ khác hoặ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 cùng một quyết định này khi phát triển SolrNet. Tôi bắt đầu với mục tiêu trở thành DI-thân thiện và không thuyết phục, nhưng khi tôi bổ sung thêm nhiều thành phần nội bộ, các nhà máy nội bộ nhanh chóng trở thành không thể quản lý và thư viện kết quả không linh hoạt.

Tôi đã kết thúc viết very simple embedded IoC container của riêng mình trong khi cũng cung cấp Windsor facilityNinject module. Việc tích hợp thư viện với các vù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, StructureMap, bất cứ thứ gì.

Nhược điểm của việc này là tôi mất khả năng chỉ new dịch vụ.Tôi cũng đã phụ thuộc vào CommonServiceLocator mà tôi có thể tránh được (tôi có thể cấu trúc lại nó trong tương lai) để làm cho vùng chứa nhúng dễ triển khai hơn.

Chi tiết khác trong số blog post này.

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

MonoRail thực hiện its own container là tốt, thực hiện tốt cũ IServiceProvider. Vùng chứa này được sử dụng trong toàn bộ khuôn khổ này thông qua an interface that exposes well-known services. Để có được thùng chứa bê tông, nó có một built-in service provider locator. Các Windsor facility điểm định vị nhà cung cấp dịch vụ này để Windsor, làm cho nó trở thành nhà cung cấp dịch vụ được lựa chọn.

Tóm lại: không có giải pháp hoàn hảo nà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ự tiện lợi.

+5

Trong khi tôi tôn trọng ý kiến ​​của bạn, tôi thấy tổng quát của bạn "tất cả các câu trả lời khác ở đây đã lỗi thời và phải là tránh "cực kỳ ngây thơ. Nếu chúng tôi đã học được bất cứ điều gì kể từ đó, đó là tiêm phụ thuộc là một câu trả lời cho những hạn chế ngôn ngữ. Nếu 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 kiến ​​trúc của chúng tôi và đạt được mô đun. IoC như một khái niệm chung chỉ là một 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. IoC * containers * hoàn toàn không cần thiết cho 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 của module mà chúng ta tạo ra. – tne

+0

@tne Việc bạn đề cập đến tôi là "vô cùng 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 trong C#, VB.NET, Java (ngôn ngữ mà bạn nghĩ "cần" IoC dường như đánh giá bởi mô tả của bạn), nó chắc chắn không phải về giới hạn ngôn ngữ. Đó là về những hạn chế về khái niệm. Tôi mời các bạn tìm hiểu về lập trình hàm thay vì sử dụng các cuộc tấn công quảng cáo-hominem và các đối số kém. –

+12

Tôi đã đề cập * Tôi * đã tìm thấy câu lệnh * của bạn * để ngây thơ; điều này không nói gì về cá nhân bạn và tất cả đều 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ụ ý tôi là 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 xem blog của bạn), đó là lý do tại sao tôi thấy khó chịu khi một người có vẻ hiểu biết sẽ nói những thứ như "chúng tôi không cần IoC". [IoC] (https://en.wikipedia.org/wiki/Inversion_of_control) xảy ra theo nghĩa đen ở mọi nơi trong lập trình hàm (..) – tne

36

Thuật ngữ "tiêm phụ thuộc" không có bất kỳ điều gì liên quan đến container IoC, mặc dù bạn có xu hướng nhìn thấy chúng được đề cập cùng nhau. Nó chỉ đơ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 việc khi bạn viết mã của bạn:

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

  2. Thay vì tạo ra các trường hợp của các giao diện bên trong một lớp học, vượt qua chúng như các đối số nhà xây dựng (cách khác, họ có thể được gán cho các tài sản công cộng; trước đây là constructor tiêm, sau này là sở hữu tiêm).

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

Nếu bạn đang tìm kiếm một ví dụ điển hình, lựa chọn nào tốt hơn .NET Framework bản thân:

  • List<T> thực hiện IList<T>. Nếu bạn thiết kế lớp học 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ư tải chậm, như LINQ to SQL, LINQ to Entities và NHibernate tất cả đều làm hậu trường, thường thông qua tính năng tiêm thuộc tính. Một số lớp khung thực sự chấp nhận một đối số constructor như IList<T>, chẳng hạn như BindingList<T>, được sử dụng cho một số tính năng ràng buộc dữ liệu.

  • LINQ to SQL và EF được xây dựng hoàn toàn xung quanh IDbConnection và các giao diện có liên quan, có thể được truyền qua thông qua các nhà thầu công khai. 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 chỉ hoạt động tốt với chuỗi kết nối đang 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 đối phó với "dịch vụ", như INameCreationService hoặc IExtenderProviderService. Bạn thậm chí không thực sự biết những gì các lớp bê tông . .NET thực sự có thùng chứa IoC riêng của mình, IContainer, được sử dụng cho điều này và lớp Component có phương thức GetService là đị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ó IContainer hoặc định vị cụ thể đó. Bản thân 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 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ể hoán đổi hệ thống cấu hình này với một container 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 của IServiceBehavior có thể được thêm vào sau này. Một lần nữa, bạn có thể dễ dàng nối dây này vào một thùng chứa IoC và có 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ó.

Và cứ tiếp tục như vậy. Bạn sẽ tìm thấy DI trên khắp nơi trong .NET, nó chỉ là bình thường nó được thực hiện rất liền mạch mà bạn thậm chí không nghĩ về nó như DI.

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

+2

Sự trừu tượng thực sự cho một container là IServiceProvider, không phải IContainer. –

+2

@Mauricio: Bạn hoàn toàn đúng, nhưng hãy thử giải thích cho người chưa bao giờ sử dụng hệ thống LWC tại sao 'IContainer' không thực sự là thùng chứa trong một đoạn. ;) – Aaronaught

+0

Aaron, chỉ tò mò là tại sao bạn sử dụng bộ riêng tư; thay vì chỉ định rõ các trường là chỉ đọc? – jr3

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