.NET에서 DI를 구현하는 "올바른"방법은 무엇입니까?


22

비교적 큰 응용 프로그램에서 종속성 주입을 구현하려고하지만 경험이 없습니다. Unity와 Ninject와 같은 IoC와 의존성 인젝터의 개념과 구현을 연구했습니다. 그러나 나를 피하는 한 가지가 있습니다. 애플리케이션에서 인스턴스 생성을 어떻게 구성해야합니까?

내가 생각하고있는 것은 몇 가지 특정 클래스 유형에 대한 객체를 만드는 논리를 포함하는 몇 가지 특정 팩토리를 만들 수 있다는 것입니다. 기본적으로이 클래스에서 정적 커널 인스턴스의 Ninject Get () 메소드를 호출하는 메소드가있는 정적 클래스입니다.

내 응용 프로그램에서 종속성 주입을 구현하는 올바른 접근 방법입니까 아니면 다른 원칙에 따라 구현해야합니까?


5
나는이 있다고 생각하지 않는다 올바른 방법으로, 그러나 많은 권리 방법, 프로젝트에 따라 달라집니다. 모든 의존성이 하나의 단일 지점에 주입되도록 할 수 있기 때문에 다른 것을 고수하고 생성자 주입을 제안합니다. 또한 생성자 서명이 너무 길어지면 클래스가 너무 많은 것을 알 수 있습니다.
Paul Kertscher

어떤 종류의 .net 프로젝트를 작성하고 있는지 알지 못하면 대답하기가 어렵습니다. 예를 들어 WPF에 대한 좋은 대답은 MVC에 대한 나쁜 대답 일 수 있습니다.
JMK

모든 종속성 등록을 솔루션 또는 각 프로젝트에 대한 DI 모듈로 구성하고 테스트하려는 깊이에 따라 일부 테스트에 대해 하나를 구성하는 것이 좋습니다. 물론 네, 생성자 주입을 사용해야합니다. 다른 것들은보다 고급 / 미친 용법입니다.
마크 로저스

답변:


30

사용할 도구에 대해 아직 생각하지 마십시오. IoC 컨테이너없이 DI를 수행 할 수 있습니다.

첫 번째 요점 : Mark Seemann은 .Net의 DI에 관한 매우 훌륭한 책을 가지고 있습니다

둘째 : 컴포지션 루트. 전체 설정이 프로젝트의 진입 점에서 완료되었는지 확인하십시오. 나머지 코드는 사용되는 도구가 아니라 주입에 대해 알아야합니다.

셋째 : 생성자 주입이 가장 가능성이 높은 방법입니다 (원하는 것은 아니지만 그렇게 많지는 않은 경우가 있습니다).

넷째 : 주입 전용 목적으로 불필요한 인터페이스 / 클래스 생성을 피하기 위해 람다 팩토리 및 기타 유사한 기능을 사용하십시오.


5
모든 훌륭한 조언; 특히 첫 번째 부분 : 순수한 DI를 수행하는 방법을 배우고 그 접근에 필요한 상용구 코드의 양을 줄일 수있는 IoC 컨테이너를 살펴보십시오.
David Arno

6
정적 검증의 모든 이점을 유지하려면 IoC 컨테이너를 모두 건너 뛰십시오.
Den

나는 DI에 들어가기 전에이 조언이 좋은 출발점이되고, 실제로 Mark Seemann 책을 가지고 있습니다.
스눕

이 대답을 두 번째로 할 수 있습니다. 우리는 부트 스트 래퍼 로직의 일부를 애플리케이션을 구성하는 모듈로 분리하여 상당히 큰 애플리케이션에서 가난한 사람의 DI (수동 부트 스트 래퍼)를 성공적으로 사용했습니다.
wigy

1
조심해. 람다 주입을 사용하는 것은 특히 테스트로 인한 설계 손상 유형의 광기를 빠르게 추적 할 수 있습니다. 알아. 나는 그 길을 갔다.
jpmc26 2016 년

13

귀하의 질문에는 DI를 올바르게 구현하는 방법과 DI를 사용하기 위해 큰 응용 프로그램을 리팩터링하는 방법의 두 부분이 있습니다.

첫 번째 부분은 "그물에 의존성 주입"마크 시만의를 읽을 @Miyamoto 아키라 (특히 추천에 의해 잘 대답한다 책. 마크의 블로그 도 좋은 무료 자원이다.

두 번째 부분은 훨씬 더 복잡합니다.

좋은 첫 번째 단계는 모든 인스턴스화를 클래스 생성자로 옮기는 것입니다. 종속성을 주입하지 않고 생성자 만 호출해야합니다 new.

이것은 당신이하고있는 모든 SRP 위반을 강조 할 것입니다. 그래서 당신은 작은 협력자들로 수업을 나누기 시작할 수 있습니다.

다음 문제는 구성을 위해 런타임 매개 변수에 의존하는 클래스입니다. 일반적으로 간단한 팩토리를 작성 Func<param,type>하여 생성자로 초기화하고 메소드에서 호출 하여이 문제를 해결할 수 있습니다 .

다음 단계는 의존성에 대한 인터페이스를 만들고 클래스에 이러한 인터페이스를 제외한 두 번째 생성자를 추가하는 것입니다. 매개 변수가없는 생성자는 구체적인 인스턴스를 새로 작성하여 새 생성자로 전달합니다. 이를 일반적으로 'B * stard Injection'또는 'Pos mans DI'라고합니다.

이를 통해 일부 단위 테스트를 수행 할 수 있으며, 이것이 리 팩터의 주요 목표 인 경우 중지 할 수 있습니다. 새로운 코드는 생성자 주입으로 작성되지만 이전 코드는 작성된대로 작동하지만 테스트 할 수는 있습니다.

물론 더 나아갈 수 있습니다. IOC 컨테이너를 사용하려는 경우 다음 단계는 new매개 변수가없는 생성자에서 모든 직접 호출을 IOC 컨테이너에 대한 정적 호출로 대체 하여 서비스 로케이터로 사용하는 것입니다 (ab).

이전과 같이 처리 할 런타임 생성자 매개 변수가 더 많이 발생합니다.

이 작업이 완료되면 매개 변수가없는 생성자를 제거하고 순수 DI로 리팩토링 할 수 있습니다.

궁극적으로 이것은 많은 작업이 될 것이므로 원하는 이유를 결정하고 리 팩터에서 가장 큰 이점을 얻을 수있는 코드베이스 부분을 우선 순위로 정하십시오


3
매우 정교한 답변에 감사드립니다. 내가 직면 한 문제에 접근하는 방법에 대한 몇 가지 아이디어를 주셨습니다. 앱의 전체 아키텍처는 이미 IoC를 염두에두고 구축되었습니다. DI를 사용하고 싶은 주된 이유는 단위 테스트조차 아니기 때문에 보너스가 아니라 가능한 한 적은 노력으로 응용 프로그램의 핵심에 정의 된 다른 인터페이스에 대해 서로 다른 구현을 바꿀 수있는 능력입니다. 문제의 응용 프로그램은 끊임없이 변화하는 환경에서 작동하며 환경의 변화에 ​​따라 새로운 구현을 사용하기 위해 응용 프로그램의 일부를 교체 해야하는 경우가 종종 있습니다.
user3223738

1
다행스럽게도 도움을 줄 수 있으며, 그래도 DI의 가장 큰 장점은 느슨한 커플 링과 이것이 제공하는 쉽게 재구성 할 수 있다는 점입니다. 단위 테스트는 좋은 부작용입니다.
Steve

1

먼저 새 프로젝트를 시작하지 않고 기존 프로젝트를 리팩토링하여이 문제를 심각하게 해결하고 있다고 언급하고 싶습니다.

당신은 그것이 큰 응용 프로그램이라고 말 했으므로 시작할 작은 구성 요소를 선택하십시오. 바람직하게는 다른 것에 의해 사용되지 않는 '리프 노드'구성 요소. 이 응용 프로그램에서 자동화 된 테스트 상태를 모르지만이 구성 요소에 대한 모든 단위 테스트를 중단합니다. 그러니 준비하십시오. 0 단계는 수정하려는 구성 요소에 대한 통합 테스트가없는 경우 작성하는 것입니다. 최후의 수단으로 (테스트 인프라 스트럭처, 작성을위한 바이 인 없음)이 구성 요소가 작동하는지 확인하기 위해 수행 할 수있는 일련의 수동 테스트를 찾아보십시오.

DI 리 팩터에 대한 목표를 설명하는 가장 간단한 방법은이 컴포넌트에서 '새'연산자의 모든 인스턴스를 제거하는 것입니다. 이들은 일반적으로 두 가지 범주로 나뉩니다.

  1. 고정 멤버 변수 : 변수는 한 번 (일반적으로 생성자에서) 설정되며 개체 수명 동안 재 할당되지 않는 변수입니다. 이를 위해 생성자에 객체 인스턴스를 주입 할 수 있습니다. 귀하는 일반적으로 이러한 객체를 폐기 할 책임이 없습니다 (여기서는 절대 말하고 싶지 않지만 실제로는 그 책임이 없어야합니다).

  2. 변형 멤버 변수 / 메소드 변수 : 개체의 수명 동안 특정 시점에 가비지 수집되는 변수입니다. 이를 위해 클래스에 팩토리를 주입하여 이러한 인스턴스를 제공하려고합니다. 공장에서 생성 한 객체를 폐기 할 책임이 있습니다.

IoC 컨테이너 (사운드처럼 들리지 않음)는 이러한 객체를 인스턴스화하고 팩토리 인터페이스를 구현해야합니다. 수정 한 구성 요소를 사용하는 모든 구성 요소를 검색하려면 IoC 컨테이너에 대해 알아야합니다.

위의 과정을 완료하면 선택한 구성 요소의 DI로 얻을 수있는 혜택을 누릴 수 있습니다. 이제 이러한 단위 테스트를 추가 / 수정하기에 좋은시기입니다. 기존 단위 테스트가있는 경우 실제 객체를 주입하여 함께 패치 할 것인지 모의를 사용하여 새로운 단위 테스트를 작성할 것인지 결정해야합니다.

'간단하게'애플리케이션의 각 구성 요소에 대해 위의 과정을 반복하여 주요 내용 만 알아야 할 때까지 IoC 컨테이너에 대한 참조를 위로 이동하십시오.


1
좋은 조언 : 'BIG re-write'가 아닌 작은 구성 요소로 시작
Daniel Hollinrake

0

올바른 접근 방식은 생성자를 사용하는 것입니다.

내가 생각하고있는 것은 몇 가지 특정 클래스 유형에 대한 객체를 만드는 논리를 포함하는 몇 가지 특정 팩토리를 만들 수 있다는 것입니다. 기본적으로이 클래스에서 정적 커널 인스턴스의 Ninject Get () 메소드를 호출하는 메소드가있는 정적 클래스입니다.

그런 다음 종속성 주입보다 서비스 로케이터로 끝납니다.


확실한. 생성자 주입. 인터페이스 구현을 인수 중 하나로 받아들이는 클래스가 있다고 가정 해 봅시다. 그러나 여전히 인터페이스 구현의 인스턴스를 생성하고 어딘가에이 생성자에 전달해야합니다. 바람직하게는 중앙 집중식 코드 조각이어야합니다.
user3223738

2
DI 컨테이너를 초기화 할 때 인터페이스 구현을 지정해야하며 DI 컨테이너가 인스턴스를 생성하고 생성자에 주입합니다.
Low Flying Pelican

개인적으로 생성자 주입이 과도하게 사용됩니다. 나는 종종 10 개의 다른 서비스가 주입되고 실제로 함수 호출에 하나만 필요하다는 것을 너무 자주 보았습니다. 왜 함수 인수의 일부가 아닌가?
urbanhusky

2
10 개의 서로 다른 서비스가 주입되는 경우 누군가가 SRP를 위반하고 있기 때문에 더 작은 구성 요소로 분할해야합니다.
Low Flying Pelican

1
@Fabio 문제는 당신을 사는 것입니다. 12 가지 완전히 다른 것들을 다루는 거대한 클래스를 갖는 것이 좋은 디자인 인 예를 아직 보지 못했습니다. DI가하는 유일한 일은 모든 위반 사항을보다 분명하게하는 것입니다.
Voo

0

당신은 당신이 그것을 사용하고 싶지만 이유를 말하지 않는다고 말합니다.

DI는 인터페이스에서 concretion을 생성하기위한 메커니즘을 제공 할뿐입니다.

이것 자체는 DIP 에서 비롯됩니다 . 코드가 이미이 스타일로 작성되어 있고 생성이 생성되는 단일 장소가있는 경우 DI는 더 이상 파티에 가져 오지 않습니다. 여기에 DI 프레임 워크 코드를 추가하면 코드가 부풀려지고 난독 화됩니다.

당신이 가정 않는 것이 명확하게 볼 수 있도록 그것을 사용하려면, 당신은 일반적으로 초기 응용 프로그램에서 공장 / 빌더 / 컨테이너 (또는 무엇이든)을 설정합니다.

NB Ninject / StructureMap 또는 다른 것에 헌신하기보다는 원하는대로 롤링하는 것이 매우 쉽습니다. 그러나 직원의 합리적인 이직률을 가진 경우, 인식 된 프레임 워크를 사용하도록 휠에 그리스를 바르거나 최소한 그 스타일로 작성하여 학습 곡선이 너무 크지 않도록 할 수 있습니다.


0

실제로 "올바른"방법은 절대로 다른 선택이없는 한 공장을 전혀 사용하지 않는 것입니다 (단위 테스트 및 특정 모형에서와 같이 생산 코드의 경우 공장을 사용하지 않음)! 그렇게하는 것은 실제로 반 패턴이며 모든 비용을 피해야합니다. DI 컨테이너 뒤에 요점은 가젯이 작업을 수행 할 수 있도록하는 것입니다 에 대한 당신.

이전 게시물에서 언급했듯이 IoC 가젯이 앱에서 다양한 종속 개체를 생성하는 책임을 맡기를 원합니다. 즉, DI 가젯이 다양한 인스턴스 자체를 생성하고 관리 할 수 ​​있습니다. 이것이 DI의 핵심입니다. 객체는 객체가 의존하는 객체를 생성 및 / 또는 관리하는 방법을 절대 알고 있어야합니다. 그렇지 않으면 느슨한 커플 링 이 끊어집니다 .

기존 응용 프로그램을 모든 DI로 변환하는 것은 큰 단계이지만, 그렇게하는 데있어 명백한 어려움을 제외하고, 많은 바인딩 작업을 자동으로 수행하는 DI 도구를 탐색하고 싶을 것입니다. (Ninject와 같은 핵심은 "kernel.Bind<someInterface>().To<someConcreteClass>()"인터페이스 선언을 인터페이스 구현에 사용하려는 구체적인 클래스와 일치 시키는 호출입니다. DI 가젯이 생성자 호출을 가로 채서 제공 할 수 있도록하는 "바인드"호출입니다. 필요한 종속 객체 인스턴스 일부 클래스의 일반적인 생성자 (여기에 표시된 의사 코드)는 다음과 같습니다.

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

참고 갑자기 그 코드에 코드 SomeConcreteClassA 또는 SomeOtherConcreteClassB의 만든 / 관리 / 발표 인스턴스 중 하나였다. 사실, 구체적인 계급도 언급되지 않았습니다. 그래서 ... 마법은 어디에서 일어 났습니까?

앱의 시작 부분에서 다음이 발생했습니다 (다시 이것은 의사 코드이지만 실제 (Ninject)와 거의 비슷합니다 ...).

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

이 작은 코드는 Ninject 가젯이 생성자를 찾고, 스캔하고, 처리하도록 구성된 인터페이스 인스턴스 ( "바인드"호출)를 찾은 다음 어디에서 구체적인 클래스의 인스턴스를 작성하고 대체하도록 지시합니다. 인스턴스가 참조됩니다.

Ninject를 Ninject.Extensions.Conventions (또 다른 NuGet 패키지)라고하는 Ninject를 보완하는 멋진 도구가 있습니다. 이 과정을 스스로 구축하면서 겪게 될 훌륭한 학습 경험에서 벗어나지 말고 시작하기 위해서는 조사 도구가 될 수 있습니다.

메모리가 제공되면 Unity (공식적으로 Microsoft의 오픈 소스 프로젝트)에서 동일한 방법으로 메소드 호출을 수행하면 다른 툴에도 비슷한 도우미가 있습니다.

어떤 경로를 선택하든 대량의 DI 교육에 대한 Mark Seemann의 책을 반드시 읽으십시오. 그러나 Mark와 같은 소프트웨어 엔지니어링 세계의 "Great Ones"조차 눈부신 오류를 일으킬 수 있다는 점을 지적해야합니다. 그의 책에서 Ninject는 Ninject를 위해 쓰여진 또 다른 자료입니다. 나는 그것과 그것의 좋은 읽을 거리가 있습니다 : 의존성 주입을위한 Ninject 마스터 링


0

"올바른 방법"은 없지만 따라야 할 몇 가지 간단한 원칙이 있습니다.

  • 응용 프로그램 시작시 컴포지션 루트 만들기
  • 컴포지션 루트가 생성 된 후 DI 컨테이너 / 커널에 대한 참조를 버립니다 (또는 최소한 캡슐화하여 응용 프로그램에서 직접 액세스 할 수 없도록합니다).
  • "신규"를 통해 인스턴스를 만들지 마십시오
  • 필요한 모든 종속성을 생성자로 추상화로 전달

그게 다야. 확실히, 그것은 법이 아닌 원칙이지만, 당신이 그 원칙을 따를 경우 DI를 할 수 있습니다.


그렇다면 "새"없이 DI 컨테이너를 몰라도 런타임 중에 객체를 만드는 방법은 무엇입니까?

NInject의 경우 팩토리 작성을 제공 하는 팩토리 확장 이 있습니다. 물론 생성 된 팩토리는 여전히 커널에 대한 내부 참조를 가지고 있지만 응용 프로그램에서 액세스 할 수는 없습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.