Cách thức quyền của Windows là gì để thực hiện DI trong .NET?


22

Tôi đang tìm cách thực hiện tiêm phụ thuộc trong một ứng dụng tương đối lớn nhưng không có kinh nghiệm trong đó. Tôi đã nghiên cứu khái niệm và một vài triển khai IoC và các bộ tiêm phụ thuộc có sẵn, như Unity và Ninject. Tuy nhiên, có một điều đang lảng tránh tôi. Làm thế nào tôi nên tổ chức tạo cá thể trong ứng dụng của tôi?

Điều tôi đang nghĩ là tôi có thể tạo ra một vài nhà máy cụ thể sẽ chứa logic tạo các đối tượng cho một vài loại lớp cụ thể. Về cơ bản là một lớp tĩnh với một phương thức gọi phương thức Ninject Get () của một cá thể hạt nhân tĩnh trong lớp này.

Nó sẽ là một cách tiếp cận chính xác của việc thực hiện tiêm phụ thuộc trong ứng dụng của tôi hay tôi nên thực hiện nó theo một số nguyên tắc khác?


5
Tôi không nghĩ rằng có những cách đúng đắn, nhưng nhiều cách đúng, tùy thuộc vào dự án của bạn. Tôi tuân thủ những người khác và đề nghị tiêm constructor, vì bạn sẽ có thể chắc chắn rằng mọi phụ thuộc đều được tiêm vào một điểm duy nhất. Ngoài ra, nếu chữ ký của nhà xây dựng phát triển quá lâu, bạn sẽ biết rằng các lớp làm quá nhiều.
Paul Kertscher

Thật khó để trả lời mà không biết bạn đang xây dựng loại dự án .net nào. Một câu trả lời tốt cho WPF có thể là một câu trả lời tồi cho MVC chẳng hạn.
JMK

Thật tuyệt khi tổ chức tất cả các đăng ký phụ thuộc vào một mô-đun DI cho giải pháp hoặc cho từng dự án, và có thể là một cho một số thử nghiệm tùy thuộc vào mức độ sâu sắc mà bạn muốn thử nghiệm. Ồ vâng, tất nhiên bạn nên sử dụng tiêm constructor, những thứ khác là dành cho những công dụng tiên tiến / điên rồ hơn.
Mark Rogers

Câu trả lời:


30

Đừng nghĩ về công cụ mà bạn sẽ sử dụng. Bạn có thể làm DI mà không cần IoC Container.

Điểm đầu tiên: Mark Seemann có một cuốn sách rất hay về DI in .Net

Thứ hai: thành phần gốc. Hãy chắc chắn rằng toàn bộ thiết lập được thực hiện trên điểm vào của dự án. Phần còn lại của mã của bạn nên biết về tiêm, không phải về bất kỳ công cụ nào đang được sử dụng.

Thứ ba: Công cụ tiêm chích là cách tốt nhất để đi (có những trường hợp bạn không muốn, nhưng không nhiều như vậy).

Thứ tư: xem xét việc sử dụng các nhà máy lambda và các tính năng tương tự khác để tránh tạo ra các giao diện / lớp không cần thiết cho mục đích duy nhất là tiêm.


5
Tất cả lời khuyên tuyệt vời; đặc biệt là phần đầu tiên: tìm hiểu cách thực hiện DI thuần túy, và sau đó bắt đầu xem xét các thùng chứa IoC có thể làm giảm số lượng mã soạn sẵn theo yêu cầu của phương pháp đó.
David Arno

6
... hoặc bỏ qua tất cả các bộ chứa IoC - để giữ tất cả các lợi ích của xác minh tĩnh.
Den

Tôi thích lời khuyên này là một điểm khởi đầu tốt, trước khi vào DI, và tôi thực sự có cuốn sách Mark Seemann.
Snoop

Tôi có thể thứ hai câu trả lời này. Chúng tôi đã sử dụng thành công người nghèo-DI (bootstrapper viết tay) trong một ứng dụng khá lớn bằng cách xé các phần của logic bootstrapper vào các mô-đun tạo ra ứng dụng.
tóc giả

1
Hãy cẩn thận. Sử dụng tiêm lambda có thể là một cách nhanh chóng đến điên rồ, đặc biệt là loại thiệt hại thiết kế do thử nghiệm gây ra. Tôi biết. Tôi đã đi xuống con đường đó.
jpmc26

13

Có hai phần cho câu hỏi của bạn - cách triển khai DI đúng cách và cách cấu trúc lại một ứng dụng lớn để sử dụng DI.

Phần đầu tiên được trả lời tốt bởi @Miyamoto Akira (đặc biệt là khuyến nghị nên đọc cuốn sách "tiêm phụ thuộc vào .net" của Mark Seemann. Blog của Marks cũng là một tài nguyên miễn phí tốt.

Phần thứ hai là một thỏa thuận tốt phức tạp hơn.

Bước đầu tiên tốt là chỉ cần di chuyển tất cả các khởi tạo vào các hàm tạo của lớp - không chèn các phần phụ thuộc, chỉ cần đảm bảo rằng bạn chỉ gọi newtrong hàm tạo.

Điều này sẽ nêu bật tất cả các vi phạm SRP mà bạn đã thực hiện, vì vậy bạn có thể bắt đầu chia lớp thành các cộng tác viên nhỏ hơn.

Vấn đề tiếp theo bạn sẽ tìm thấy sẽ là các lớp dựa vào các tham số thời gian chạy để xây dựng. Bạn thường có thể khắc phục điều này bằng cách tạo các nhà máy đơn giản, thường là Func<param,type>, khởi tạo chúng trong hàm tạo và gọi chúng trong các phương thức.

Bước tiếp theo sẽ là tạo các giao diện cho các phần phụ thuộc của bạn và thêm một hàm tạo thứ hai vào các lớp của bạn ngoại trừ các giao diện này. Hàm tạo không tham số của bạn sẽ tạo ra các thể hiện cụ thể và chuyển chúng đến hàm tạo mới. Điều này thường được gọi là 'B * stard Injection' hoặc 'Poor mans DI'.

Điều này sẽ cung cấp cho bạn khả năng thực hiện một số thử nghiệm đơn vị và nếu đó là mục tiêu chính của bộ tái cấu trúc, có thể là nơi bạn dừng lại. Mã mới sẽ được viết bằng hàm constructor, nhưng mã cũ của bạn có thể tiếp tục hoạt động như được viết nhưng vẫn có thể kiểm tra được.

Tất nhiên bạn có thể đi xa hơn. Nếu bạn có ý định sử dụng bộ chứa IOC, thì bước tiếp theo có thể là thay thế tất cả các cuộc gọi trực tiếp đến newcác nhà xây dựng không tham số của bạn bằng các cuộc gọi tĩnh đến bộ chứa IOC, về cơ bản (ab) sử dụng nó làm công cụ định vị dịch vụ.

Điều này sẽ đưa ra nhiều trường hợp tham số hàm tạo thời gian chạy để xử lý như trước.

Một khi điều này được thực hiện, bạn có thể bắt đầu loại bỏ các hàm tạo không tham số và tái cấu trúc thành DI thuần túy.

Cuối cùng, đây sẽ là rất nhiều công việc, vì vậy hãy chắc chắn rằng bạn quyết định lý do tại sao bạn muốn làm điều đó và ưu tiên các phần của cơ sở mã sẽ có lợi nhất từ ​​bộ tái cấu trúc


3
Cảm ơn cho một câu trả lời rất công phu. Bạn đã cho tôi một vài ý tưởng về cách tiếp cận vấn đề tôi đang gặp phải. Toàn bộ kiến ​​trúc của ứng dụng đã được xây dựng với IoC. Lý do chính tôi muốn sử dụng DI thậm chí không phải là thử nghiệm đơn vị, nó là một phần thưởng mà là khả năng trao đổi các triển khai khác nhau cho các giao diện khác nhau, được xác định trong cốt lõi của ứng dụng của tôi, với càng ít nỗ lực càng tốt. Ứng dụng trong câu hỏi hoạt động trong một môi trường thay đổi liên tục và tôi thường phải trao đổi các phần của ứng dụng để sử dụng các triển khai mới theo các thay đổi trong môi trường.
dùng3223738

1
Vui mừng tôi có thể giúp, và vâng tôi đồng ý rằng ưu điểm lớn nhất của DI là khớp nối lỏng lẻo và khả năng cấu hình lại dễ dàng mà điều này mang lại, với thử nghiệm đơn vị là một hiệu ứng phụ đẹp.
Steve

1

Đầu tiên tôi muốn đề cập rằng bạn đang tự làm khó mình hơn bằng cách tái cấu trúc một dự án hiện có thay vì bắt đầu một dự án mới.

Bạn nói rằng nó là một ứng dụng lớn, vì vậy hãy chọn một thành phần nhỏ để bắt đầu. Tốt nhất là một thành phần 'nút lá' không được sử dụng bởi bất kỳ thứ gì khác. Tôi không biết trạng thái của thử nghiệm tự động trên ứng dụng này là gì, nhưng bạn sẽ phá vỡ tất cả các thử nghiệm đơn vị cho thành phần này. Vì vậy, hãy chuẩn bị cho điều đó. Bước 0 là viết các bài kiểm tra tích hợp cho thành phần bạn sẽ sửa đổi nếu chúng chưa tồn tại. Như một phương sách cuối cùng (không có cơ sở hạ tầng kiểm tra; không mua để viết nó), hãy tìm ra một loạt các thử nghiệm thủ công mà bạn có thể thực hiện để xác minh thành phần này đang hoạt động.

Cách đơn giản nhất để nêu mục tiêu của bạn cho bộ tái cấu trúc DI là bạn muốn xóa tất cả các phiên bản của toán tử 'mới' khỏi thành phần này. Chúng thường rơi vào hai loại:

  1. Biến thành viên bất biến: Đây là các biến được đặt một lần (thường là trong hàm tạo) và không được gán lại cho vòng đời của đối tượng. Đối với những điều này, bạn có thể đưa một thể hiện của đối tượng vào hàm tạo. Bạn thường không chịu trách nhiệm trong việc xử lý các đối tượng này (tôi không muốn nói không bao giờ ở đây, nhưng bạn thực sự không nên có trách nhiệm đó).

  2. Biến thành viên / biến phương thức biến: Đây là các biến sẽ nhận rác được thu thập tại một số điểm trong suốt vòng đời của đối tượng. Đối với những điều này, bạn sẽ muốn đưa một nhà máy vào lớp của mình để cung cấp những trường hợp này. Bạn chịu trách nhiệm xử lý các đối tượng được tạo bởi một nhà máy.

Container IoC của bạn (ninject nó nghe có vẻ như) sẽ chịu trách nhiệm khởi tạo các đối tượng đó và thực hiện các giao diện nhà máy của bạn. Bất cứ điều gì đang sử dụng thành phần bạn đã sửa đổi sẽ cần biết về bộ chứa IoC để nó có thể truy xuất thành phần của bạn.

Khi bạn đã hoàn thành những điều trên, bạn sẽ có thể gặt hái bất kỳ lợi ích nào mà bạn hy vọng nhận được từ DI trong thành phần đã chọn. Bây giờ sẽ là thời điểm tốt để thêm / sửa các bài kiểm tra đơn vị đó. Nếu đã có các bài kiểm tra đơn vị hiện tại, bạn sẽ phải đưa ra quyết định về việc bạn có muốn vá chúng lại với nhau hay không bằng cách tiêm các đối tượng thực hoặc viết các bài kiểm tra đơn vị mới bằng cách sử dụng giả.

'Đơn giản là' lặp lại những điều trên cho từng thành phần trong ứng dụng của bạn, di chuyển tham chiếu đến bộ chứa IoC khi bạn đi cho đến khi chỉ cần biết chính về nó.


1
Lời khuyên tốt: bắt đầu với một thành phần nhỏ thay vì 'Viết lại LỚN'
Daniel Hollinrake

0

Cách tiếp cận đúng là sử dụng tiêm constructor, nếu bạn sử dụng

Điều tôi đang nghĩ là tôi có thể tạo ra một vài nhà máy cụ thể sẽ chứa logic tạo các đối tượng cho một vài loại lớp cụ thể. Về cơ bản là một lớp tĩnh với một phương thức gọi phương thức Ninject Get () của một cá thể hạt nhân tĩnh trong lớp này.

sau đó bạn kết thúc với định vị dịch vụ, hơn là tiêm phụ thuộc.


Chắc chắn rồi. Xây dựng tiêm. Giả sử tôi có một lớp chấp nhận triển khai giao diện là một trong những đối số. Nhưng tôi vẫn cần tạo một thể hiện của việc thực hiện giao diện và chuyển nó đến hàm tạo này ở đâu đó. Tốt nhất là nó nên là một đoạn mã tập trung.
dùng3223738

2
Không, khi bạn khởi tạo bộ chứa DI, bạn phải chỉ định việc thực hiện các giao diện và bộ chứa DI sẽ tạo cá thể và tiêm vào hàm tạo.
Chim bồ câu bay thấp

Cá nhân, tôi thấy tiêm constructor bị lạm dụng. Tôi đã thấy quá thường xuyên rằng 10 dịch vụ khác nhau được tiêm và chỉ có một dịch vụ thực sự cần thiết cho một cuộc gọi chức năng - tại sao đó không phải là một phần của đối số chức năng?
urbanhusky

2
Nếu 10 dịch vụ khác nhau được tiêm, đó là do ai đó đang vi phạm SRP, nên được chia thành các thành phần nhỏ hơn.
Chim bồ câu bay thấp

1
@Fabio Câu hỏi là cái gì sẽ mua cho bạn. Tôi vẫn chưa thấy một ví dụ về việc có một lớp khổng lồ liên quan đến hàng tá thứ hoàn toàn khác nhau là một thiết kế tốt. Điều duy nhất DI làm là làm cho tất cả những vi phạm đó rõ ràng hơn.
Voo

0

Bạn nói rằng bạn muốn sử dụng nó nhưng không nói rõ tại sao.

DI không có gì khác hơn là cung cấp một cơ chế để tạo ra bê tông hóa từ các giao diện.

Điều này trong bản thân nó đến từ DIP . Nếu mã của bạn đã được viết theo phong cách này và bạn có một nơi duy nhất tạo ra sự cụ thể hóa, DI không mang lại gì thêm cho bữa tiệc. Thêm mã khung DI ở đây chỉ đơn giản là làm phình ra và làm xáo trộn cơ sở mã của bạn.

Giả sử bạn làm muốn sử dụng nó, bạn thường thiết lập các nhà máy / builder / container (hoặc bất kỳ) đầu trong việc áp dụng vì vậy nó rất rõ ràng.

NB rất dễ dàng để tự cuộn nếu bạn muốn thay vì cam kết với Ninject / StructMap hoặc bất cứ điều gì. Tuy nhiên, nếu bạn có một đội ngũ nhân viên hợp lý, có thể họ sẽ sử dụng một khuôn khổ được công nhận hoặc ít nhất là viết nó theo phong cách đó để nó không quá giống với đường cong học tập.


0

Trên thực tế, cách "đúng" là hoàn toàn KHÔNG sử dụng nhà máy trừ khi hoàn toàn không có lựa chọn nào khác (như trong thử nghiệm đơn vị và các giả định nhất định - đối với mã sản xuất bạn KHÔNG sử dụng nhà máy)! Làm như vậy thực sự là một mô hình chống và nên tránh bằng mọi giá. Toàn bộ điểm phía sau một thùng chứa DI là cho phép tiện ích thực hiện công việc cho bạn.

Như đã nêu ở trên trong một bài viết trước, bạn muốn tiện ích IoC của mình chịu trách nhiệm tạo ra các đối tượng phụ thuộc khác nhau trong ứng dụng của bạn. Điều đó có nghĩa là để cho tiện ích DI của bạn tự tạo và quản lý các phiên bản khác nhau. Đây là toàn bộ điểm phía sau DI - các đối tượng của bạn KHÔNG BAO GIỜ biết cách tạo và / hoặc quản lý các đối tượng mà chúng phụ thuộc vào. Để làm khác phá vỡ khớp nối lỏng lẻo.

Chuyển đổi một ứng dụng hiện có thành tất cả DI là một bước tiến lớn, nhưng bỏ qua những khó khăn rõ ràng khi làm như vậy, bạn cũng sẽ muốn (chỉ để làm cho cuộc sống của bạn dễ dàng hơn một chút) để khám phá một công cụ DI sẽ tự động thực hiện phần lớn các liên kết của bạn (cốt lõi của thứ gì đó như Ninject là các "kernel.Bind<someInterface>().To<someConcreteClass>()"cuộc gọi mà bạn thực hiện để khớp với các khai báo giao diện của bạn với các lớp cụ thể mà bạn muốn sử dụng để triển khai các giao diện đó. Đó là các lệnh gọi "Bind" cho phép tiện ích DI của bạn chặn các cuộc gọi của nhà xây dựng của bạn và cung cấp Các thể hiện đối tượng phụ thuộc cần thiết. Một hàm tạo điển hình (mã giả được hiển thị ở đây) cho một số lớp có thể là:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

Lưu ý rằng khôngđâu trong mã đó là bất kỳ mã nào được tạo / quản lý / phát hành, ví dụ của someConcittleClassA hoặc của someOtherConcittleClassB. Như một vấn đề của thực tế, cả lớp cụ thể thậm chí không được tham chiếu. Vậy ... phép thuật đã xảy ra ở đâu?

Trong phần khởi động của ứng dụng của bạn, phần sau đã diễn ra (một lần nữa, đây là mã giả nhưng nó khá gần với thứ thật (Ninject) ...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

Một đoạn mã nhỏ đó bảo tiện ích Ninject tìm kiếm các hàm tạo, quét chúng, tìm các thể hiện của các giao diện mà nó đã được cấu hình để xử lý (đó là các lệnh gọi "Bind"), sau đó tạo và thay thế một thể hiện của lớp cụ thể ở bất cứ đâu ví dụ được tham chiếu.

Có một công cụ hay bổ sung cho Ninject rất hay gọi là Ninject.Extensions.Conventions (một gói NuGet khác) sẽ thực hiện phần lớn công việc này cho bạn. Không phải từ bỏ trải nghiệm học tập tuyệt vời mà bạn sẽ trải qua khi bạn tự xây dựng điều này, nhưng để bắt đầu, đây có thể là một công cụ để điều tra.

Nếu bộ nhớ phục vụ, Unity (chính thức từ Microsoft bây giờ là một dự án Nguồn mở) có một cuộc gọi phương thức hoặc hai thực hiện cùng một công cụ, các công cụ khác có các trình trợ giúp tương tự.

Dù bạn chọn con đường nào, chắc chắn hãy đọc cuốn sách của Mark Seemann cho phần lớn khóa đào tạo DI của bạn, tuy nhiên, cần phải chỉ ra rằng ngay cả "Những người vĩ đại" của thế giới công nghệ phần mềm (như Mark) cũng có thể mắc lỗi rõ ràng - Mark đã quên tất cả về Ninject trong cuốn sách của mình vì vậy đây là một tài nguyên khác được viết riêng cho Ninject. Tôi có nó và nó rất hay: Làm chủ Ninject cho Dependency Injection


0

Không có "cách đúng", nhưng có một vài nguyên tắc đơn giản để làm theo:

  • Tạo root thành phần khi khởi động ứng dụng
  • Sau khi gốc thành phần đã được tạo, hãy ném tham chiếu đến vùng chứa / nhân DI đi (hoặc ít nhất là đóng gói nó để nó không thể truy cập trực tiếp từ ứng dụng của bạn)
  • Không tạo phiên bản qua "mới"
  • Truyền tất cả các phụ thuộc cần thiết dưới dạng trừu tượng cho hàm tạo

Đó là tất cả. Chắc chắn, đó là những nguyên tắc không phải là luật, nhưng nếu bạn tuân theo chúng, bạn có thể chắc chắn rằng bạn làm DI (xin vui lòng sửa cho tôi nếu tôi sai).


Vậy, làm thế nào để tạo các đối tượng trong thời gian chạy mà không "mới" và không biết DI container?

Trong trường hợp của NInject, có một phần mở rộng nhà máy cung cấp việc tạo ra các nhà máy. Chắc chắn, các nhà máy được tạo vẫn có một giới thiệu nội bộ cho kernel, nhưng điều đó không thể truy cập được từ ứng dụng của bạn.

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.