DI (Dependency Inject) "친숙한"라이브러리


230

C # 라이브러리의 디자인을 숙고하고 있습니다. 여기에는 여러 가지 다른 고급 기능이 있습니다. 물론 이러한 고급 기능은 가능한 한 SOLID 클래스 설계 원칙을 사용하여 구현 됩니다. 따라서 소비자가 정기적으로 직접 사용하도록 고안된 클래스와 더 일반적인 "최종 사용자"클래스의 종속성 인 "지원 클래스"가있을 수 있습니다.

문제는 라이브러리를 디자인하는 가장 좋은 방법은 무엇입니까?

  • DI Agnostic-하나 또는 두 개의 공통 DI 라이브러리 (StructureMap, Ninject 등)에 대한 기본 "지원"을 추가하는 것이 합리적이지만 소비자가 DI 프레임 워크에서 라이브러리를 사용할 수 있기를 바랍니다.
  • DI를 사용할 수 없음-라이브러리 소비자가 DI를 사용하지 않는 경우에도 라이브러리는 가능한 한 사용하기 쉬워야하기 때문에 사용자가 이러한 "중요하지 않은"종속성을 만들기 위해 수행해야하는 작업량을 줄여야합니다. 그들이 사용하고자하는 "실제"클래스.

나의 현재 생각은 공통 DI 라이브러리 (예 : StructureMap 레지스트리, Ninject 모듈)에 대한 몇 가지 "DI 등록 모듈"과 DI가 아닌 세트 또는 팩토리 클래스를 제공하고 그 몇 개의 팩토리에 대한 커플 링을 포함하는 것입니다.

생각?


(SOLID) 깨진 링크 기사에 대한 새로운 참조 : butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan

답변:


360

DI가 기술이 아닌 패턴과 원리 에 관한 것임을 이해하면 실제로는 간단합니다 .

DI 컨테이너와 무관하게 API를 디자인하려면 다음 일반 원칙을 따르십시오.

구현이 아닌 인터페이스로 프로그램

이 원칙은 실제로 Design Patterns 의 인용 (메모리에서 인용) 이지만 항상 실제 목표가 되어야합니다 . DI는 그 목적을 달성하기위한 수단 일뿐 입니다.

할리우드 원리 적용

DI 용어의 할리우드 원칙은 다음과 같이 말합니다. DI 컨테이너를 호출하지 마십시오 .

코드 내에서 컨테이너를 호출하여 종속성을 직접 요청하지 마십시오. Constructor Injection 을 사용하여 암시 적으로 요청하십시오 .

생성자 주입 사용

의존성이 필요한 경우 생성자를 통해 정적으로 요청 하십시오.

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

서비스 클래스가 불변을 어떻게 보장하는지 주목하십시오. 인스턴스가 생성되면 Guard Clause와 readonly키워드 의 조합으로 인해 종속성을 사용할 수 있습니다 .

수명이 짧은 객체가 필요한 경우 Abstract Factory를 사용하십시오.

Constructor Injection으로 주입 된 종속성은 오래 지속되는 경향이 있지만 때로는 수명이 짧은 객체가 필요하거나 런타임에만 알려진 값을 기반으로 종속성을 구성해야합니다.

자세한 내용은 이것을 참조하십시오.

마지막 책임있는 순간에만 작성

끝까지 개체를 분리하십시오. 일반적으로 응용 프로그램의 진입 점에서 모든 것을 기다렸다가 연결할 수 있습니다. 이것을 컴포지션 루트 라고합니다 .

자세한 내용은 여기 :

외관을 사용하여 단순화

결과 API가 초보자 사용자에게 너무 복잡해 졌다고 생각되면 공통 종속성 조합을 캡슐화하는 Facade 클래스를 항상 제공 할 수 있습니다 .

높은 수준의 검색 가능성을 가진 유연한 Facade를 제공하기 위해 Fluent Builders 제공을 고려할 수 있습니다. 이 같은:

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

이를 통해 사용자는 다음과 같이 작성하여 기본 Foo를 만들 수 있습니다

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

그러나 커스텀 의존성을 제공 할 수 있다는 것을 발견 할 수있을 것입니다.

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

MyFacade 클래스가 다양한 종속성을 캡슐화한다고 생각한다면 확장 성을 검색하면서도 적절한 기본값을 제공하는 방법이 명확하기를 바랍니다.


FWIW는이 답변을 작성한 후 여기에있는 개념을 확장하여 DI-Friendly Libraries 에 대한 더 긴 블로그 게시물 과 DI-Friendly Frameworks에 대한 관련 게시물을 작성했습니다 .


21
이것은 종이에서 훌륭하게 들리지만 복잡한 경험으로 상호 작용하는 내부 구성 요소가 많은 경우 내 공장에서 관리해야 할 공장이 많아 유지 관리가 더 어려워집니다. 또한 팩토리는 생성 된 구성 요소의 라이프 스타일을 관리해야합니다. 라이브러리를 실제 컨테이너에 설치하면 컨테이너 자체의 라이프 스타일 관리와 충돌합니다. 공장과 외관은 실제 컨테이너를 방해합니다.
Mauricio Scheffer

4
나는이 모든 것을하는 프로젝트를 아직 찾지 못했다 .
Mauricio Scheffer

31
이것이 바로 Safewhere에서 소프트웨어를 개발하는 방법이므로 경험을 공유하지 않습니다 ...
Mark Seemann

19
외관은 구성 요소의 알려진 (그리고 아마도 일반적인) 조합을 나타 내기 때문에 수작업으로 코딩해야한다고 생각합니다. 모든 것을 손으로 연결할 수 있기 때문에 DI 컨테이너는 필요하지 않습니다 (Poor Man 's DI 생각). Facade는 API 사용자에게 선택적인 편의 클래스 일뿐입니다. 고급 사용자는 여전히 Facade를 우회하고 구성 요소를 원하는대로 연결할 수 있습니다. 그들은 이것을 위해 자신의 DI Contaier를 사용하고 싶을 수 있으므로 특정 DI 컨테이너를 사용하지 않을 경우 강제로 적용하는 것은 불공평하다고 생각합니다. 가능하지만
권하지

8
이것은 내가 본 것 중 가장 좋은 대답 일 수 있습니다.
Nick Hodges

40

"종속성 주입"이라는 용어는 IoC 컨테이너와는 전혀 관련이 없지만, IoC 컨테이너와는 전혀 관련이 없습니다. 단순히 다음과 같이 코드를 작성하는 대신 의미합니다.

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

다음과 같이 작성하십시오.

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

즉, 코드를 작성할 때 두 가지 작업을 수행합니다.

  1. 구현을 변경해야한다고 생각할 때마다 클래스 대신 인터페이스를 사용하십시오.

  2. 클래스 내에서 이러한 인터페이스의 인스턴스를 생성하는 대신 생성자 인수로 전달하십시오 (또는, 공용 속성에 할당 할 수 있습니다. 전자는 생성자 주입 이고, 후자는 속성 주입 ).

이 중 어느 것도 DI 라이브러리의 존재를 전제로하지 않으며 코드 없이는 코드를 작성하기가 더 이상 어렵지 않습니다.

이에 대한 예를 찾고 있다면 .NET Framework 자체 만 살펴보십시오.

  • List<T>구현 IList<T>합니다. 클래스를 사용하도록 IList<T>(또는 IEnumerable<T>) 디자인 하면 Linq to SQL, Linq to Entities 및 NHibernate와 같은 개념을 지연 로딩과 같은 개념을 활용할 수 있습니다 (일반적으로 속성 삽입을 통해). 일부 프레임 워크 클래스는 실제로 여러 데이터 바인딩 기능에 사용 IList<T>되는와 같은 생성자 인수로 허용 BindingList<T>합니다.

  • Linq to SQL 및 EF는 전적으로 IDbConnection공용 생성자를 통해 전달 될 수있는 관련 인터페이스를 중심으로 구축됩니다. 그러나 그것들을 사용할 필요는 없습니다. 기본 생성자는 구성 파일에 어딘가에있는 연결 문자열로 잘 작동합니다.

  • WinForms 구성 요소를 다룬 적이 있다면 INameCreationServiceor 와 같은 "서비스"를 다룰 수 있습니다 IExtenderProviderService. 당신은 실제 클래스 무엇인지 정말로 알지 못합니다 . .NET에는 실제로 자체 IoC 컨테이너가 있으며이 클래스에 IContainer사용되며 Component클래스에는 GetService실제 서비스 로케이터 인 메소드가 있습니다. 물론, IContainer특정 로케이터 없이 또는 이러한 로케이터 없이 이러한 인터페이스 중 일부 또는 전부를 사용할 수있는 것은 없습니다 . 서비스 자체는 컨테이너와 느슨하게 결합되어 있습니다.

  • WCF의 계약은 전적으로 인터페이스를 중심으로 구축됩니다. 실제 구체적 서비스 클래스는 일반적으로 구성 파일에서 기본적으로 이름 인 DI로 참조됩니다. 많은 사람들이 이것을 인식하지 못하지만이 구성 시스템을 다른 IoC 컨테이너로 교체 할 수 있습니다. 더 흥미롭게도 서비스 동작은 IServiceBehavior나중에 추가 될 수있는 모든 인스턴스 일 것입니다 . 다시 말하지만, 이것을 IoC 컨테이너에 쉽게 연결하여 관련 동작을 선택하도록 할 수 있지만 기능은 완전히 사용할 수 없습니다.

그리고 등등. .NET에서 DI를 찾을 수 있습니다. 일반적으로 DI로 생각조차하지 않도록 완벽하게 수행됩니다.

최대한의 유용성을 위해 DI 가능 라이브러리를 설계하려면 가장 가벼운 제안은 경량 컨테이너를 사용하여 고유 한 기본 IoC 구현을 제공하는 것입니다. IContainer.NET Framework 자체의 일부이기 때문에 이에 대한 훌륭한 선택입니다.


2
컨테이너의 실제 추상화는 IContainer가 아니라 IServiceProvider입니다.
Mauricio Scheffer

2
@Mauricio : 물론 맞습니다. 그러나 LWC 시스템을 사용하지 않은 사람에게 왜 IContainer실제로 한 단락의 컨테이너가 아닌지 설명해보십시오 . ;)
Aaronaught

Aaron, 왜 당신이 개인 세트를 사용하는지 궁금합니다. 필드를 읽기 전용으로 지정하는 대신?
jr3 2016 년

@Jreeter : private readonly필드 가 아닌 이유는 무엇입니까? 선언 클래스에서만 사용되지만 OP가 이것이 프레임 워크 / 라이브러리 수준 코드라고 지정하면 서브 클래스 화를 의미합니다. 이러한 경우 일반적으로 하위 클래스에 중요한 종속성이 노출되기를 원합니다. 명시적인 getter / setter를 사용 하여 private readonly필드 속성을 작성할 수 있었지만 예제에서 공간을 낭비하고 실제로 유지하기 위해 더 많은 코드를 사용하여 실제 이익을 얻지 못했습니다.
Aaronaught

설명해 주셔서 감사합니다! 자식이 읽기 전용 필드에 액세스 할 수 있다고 가정했습니다.
jr3 2016 년

5

편집 2015 : 시간이 지났습니다.이 모든 것이 큰 실수라는 것을 깨달았습니다. IoC 컨테이너는 끔찍하며 DI는 부작용을 처리하기에 매우 열악한 방법입니다. 효과적으로, 여기에있는 모든 대답 (및 질문 자체)은 피해야합니다. 부작용을 인식하고 순수 코드와 분리하면 다른 모든 것이 제자리에 있거나 관련이없고 불필요한 복잡성이 발생합니다.

원래 답변은 다음과 같습니다.


SolrNet 을 개발하는 동안 동일한 결정을 내려야했습니다 . DI에 친숙하고 컨테이너에 구애받지 않는다는 목표로 시작했지만 점점 더 많은 내부 구성 요소를 추가함에 따라 내부 공장을 신속하게 관리 할 수 ​​없게되었고 결과 라이브러리는 융통성이 없었습니다.

나는 매우 간단한 임베디드 IoC 컨테이너 를 작성 하는 동시에 Windsor 기능Ninject 모듈을 제공했다 . 라이브러리를 다른 컨테이너와 통합하는 것은 구성 요소를 올바르게 연결하는 것이므로 Autofac, Unity, StructureMap 등과 쉽게 통합 할 수 있습니다.

이것의 단점은 new서비스를 향상시키는 능력을 잃어 버렸다는 것 입니다. 또한 임베디드 컨테이너를 쉽게 구현하기 위해 피할 수있는 CommonServiceLocator에 의존했습니다 (나중에 리팩터링 할 수 있음).

블로그 게시물 에 대한 자세한 내용 .

MassTransit 은 비슷한 것에 의존하는 것 같습니다. 여기에는 몇 가지 메소드가 더있는 CommonServiceLocator의 IServiceLocator 인 IObjectBuilder 인터페이스가 있으며, 각 컨테이너 (예 : NinjectObjectBuilder) 및 일반 모듈 / 기능 (예 : MassTransitModule) 에 대해이를 구현합니다 . 그런 다음 IObjectBuilder사용하여 필요한 것을 인스턴스화합니다. 이것은 물론 유효한 접근 방식이지만 실제로 서비스 로케이터로 사용하여 컨테이너를 너무 많이 통과하기 때문에 개인적으로별로 좋아하지 않습니다.

MonoRail 은 자체 컨테이너 를 구현 하여 오래된 IServiceProvider 를 구현 합니다. 이 컨테이너는 잘 알려진 서비스를 제공하는 인터페이스를 통해이 프레임 워크 전체에서 사용됩니다 . 콘크리트 컨테이너를 얻기 위해 기본 제공 서비스 공급자 로케이터가 있습니다. 윈저 시설 이 선택한 서비스 제공하고, 윈저이 서비스 프로 바이더 로케이터를 가리 킵니다.

결론 : 완벽한 해결책은 없습니다. 모든 디자인 결정과 마찬가지로이 문제는 유연성, 유지 관리 용이성 및 편의성 간의 균형을 요구합니다.


12
귀하의 의견을 존중하지만 귀하의 지나치게 일반화 된 "여기의 다른 답변은 모두 구식이며 피해야합니다"는 것은 매우 순진합니다. 그 이후로 무엇인가를 배운다면 의존성 주입은 언어 제한에 대한 해답입니다. 현재 언어를 발전 시키거나 포기하지 않으면 서 아키텍처를 평탄화하고 모듈성을 달성 하려면 DI 가 필요할 것 같습니다 . 일반적인 개념으로서의 IoC는 이러한 목표의 결과 일 뿐이며 바람직한 목표입니다. IoC 컨테이너 는 IoC 또는 DI에 반드시 필요한 것은 아니며, 유용성은 우리가 만든 모듈 구성에 따라 다릅니다.
tne

@tne 저의 "매우 순진한"이라는 언급은 반갑지 않은 광고입니다. 나는 C #, VB.NET, Java에서 DI 또는 IoC없이 복잡한 응용 프로그램을 작성했습니다 (귀하의 설명에 따라 IoC가 "필요하다고 생각할 것"이라고 말한 언어). 언어 제한에 관한 것은 아닙니다. 개념적 한계에 관한 것입니다. 나는 ad-hominem 공격과 열악한 주장에 의존하는 대신 함수형 프로그래밍에 대해 배우도록 권유합니다.
Mauricio Scheffer 12

18
나는 당신의 진술 이 순진 하다는 것을 알았습니다 . 이것은 개인적으로 당신에 대해 아무 말도하지 않으며 내 의견에 관한 것입니다. 임의의 낯선 사람에게 모욕을 느끼지 마십시오. 모든 관련 기능 프로그래밍 접근 방식을 모르는 것은 도움이되지 않습니다. 당신은 그것을 모르고, 당신은 틀릴 것입니다. 나는 당신 이 거기에 대해 잘 알고 있다는 것을 알고 있었다 (빠른 블로그를 보았 기 때문에). 나는 우리가 IoC를 필요로하지 않는 것처럼 보이는 지식이있는 사람이 짜증나는 것을 알게되었다. IoC는 문자 그대로 함수형 프로그래밍의 모든 곳에서 발생합니다 (..)
tne

4
일반적으로 고급 소프트웨어에서. 실제로, 기능적 언어 (및 다른 많은 스타일)는 실제로 DI없이 IoC를 해결한다는 것을 증명합니다. 우리 둘 다 동의한다고 생각합니다. 언급 한 언어로 DI없이 관리했다면 어떻게 했습니까? 사용하지 않는 것으로 가정 ServiceLocator합니다. 함수형 기법을 적용하면 종속성 주입 클래스와 같은 클로저로 끝나게됩니다 (종속성은 닫힌 변수 임). 또 어떻게 요? (나는 DI도 좋아하지 않기 때문에 진정으로 궁금합니다.)
tne

1
IoC 컨테이너는 전역 변경 가능한 사전이므로 추론을위한 도구로 매개 변수를 제거하므로 피해야합니다. "우리에게 전화하지 마십시오"와 같은 IoC (개념)는 함수를 값으로 전달할 때마다 기술적으로 적용됩니다. DI 대신 ADT를 사용하여 도메인을 모델링 한 다음 통역사를 작성하십시오 (무료).
Mauricio Scheffer 10

1

내가 할 일은 컨테이너에 대한 종속성을 가능한 한 많이 제한하기 위해 DI 컨테이너에 독립적으로 라이브러리를 디자인하는 것입니다. 필요한 경우 DI 컨테이너를 다른 컨테이너로 교체 할 수 있습니다.

그런 다음 DI 로직 위의 레이어를 라이브러리 사용자에게 노출하여 인터페이스를 통해 선택한 프레임 워크를 사용할 수 있도록합니다. 이러한 방식으로 노출 된 DI 기능을 계속 사용할 수 있으며 다른 프레임 워크를 자유롭게 사용할 수 있습니다.

라이브러리 사용자가 자신의 DI 프레임 워크를 꽂도록 허용하면 유지 관리 비용이 크게 증가하므로 나에게는 약간 잘못된 것 같습니다. 이것은 또한 스트레이트 DI보다 플러그인 환경이 더 많이됩니다.

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