C # 라이브러리의 디자인을 숙고하고 있습니다. 여기에는 여러 가지 다른 고급 기능이 있습니다. 물론 이러한 고급 기능은 가능한 한 SOLID 클래스 설계 원칙을 사용하여 구현 됩니다. 따라서 소비자가 정기적으로 직접 사용하도록 고안된 클래스와 더 일반적인 “최종 사용자”클래스의 종속성 인 “지원 클래스”가있을 수 있습니다.
문제는 라이브러리를 디자인하는 가장 좋은 방법은 무엇입니까?
- DI Agnostic-하나 또는 두 개의 공통 DI 라이브러리 (StructureMap, Ninject 등)에 대한 기본 “지원”을 추가하는 것이 합리적이지만 소비자가 DI 프레임 워크에서 라이브러리를 사용할 수 있기를 바랍니다.
- DI를 사용할 수 없음-라이브러리 소비자가 DI를 사용하지 않는 경우에도 라이브러리는 가능한 한 사용하기 쉬워야하기 때문에 사용자가 이러한 “중요하지 않은”종속성을 만들기 위해 수행해야하는 작업량을 줄여야합니다. 그들이 사용하고자하는 “실제”클래스.
나의 현재 생각은 공통 DI 라이브러리 (예 : StructureMap 레지스트리, Ninject 모듈)에 대한 몇 가지 “DI 등록 모듈”과 DI가 아닌 세트 또는 팩토리 클래스를 제공하고 그 몇 개의 팩토리에 대한 커플 링을 포함하는 것입니다.
생각?
답변
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에 대한 관련 게시물을 작성했습니다 .
답변
“종속성 주입”이라는 용어는 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; }
}
즉, 코드를 작성할 때 두 가지 작업을 수행합니다.
-
구현을 변경해야한다고 생각할 때마다 클래스 대신 인터페이스를 사용하십시오.
-
클래스 내에서 이러한 인터페이스의 인스턴스를 생성하는 대신 생성자 인수로 전달하십시오 (또는, 공용 속성에 할당 할 수 있습니다. 전자는 생성자 주입 이고, 후자는 속성 주입 ).
이 중 어느 것도 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 구성 요소를 다룬 적이 있다면
INameCreationService
or 와 같은 “서비스”를 다룰 수 있습니다IExtenderProviderService
. 당신은 실제 클래스 가 무엇인지 정말로 알지 못합니다 . .NET에는 실제로 자체 IoC 컨테이너가 있으며이 클래스에IContainer
사용되며Component
클래스에는GetService
실제 서비스 로케이터 인 메소드가 있습니다. 물론,IContainer
특정 로케이터 없이 또는 이러한 로케이터 없이 이러한 인터페이스 중 일부 또는 전부를 사용할 수있는 것은 없습니다 . 서비스 자체는 컨테이너와 느슨하게 결합되어 있습니다. -
WCF의 계약은 전적으로 인터페이스를 중심으로 구축됩니다. 실제 구체적 서비스 클래스는 일반적으로 구성 파일에서 기본적으로 이름 인 DI로 참조됩니다. 많은 사람들이 이것을 인식하지 못하지만이 구성 시스템을 다른 IoC 컨테이너로 교체 할 수 있습니다. 더 흥미롭게도 서비스 동작은
IServiceBehavior
나중에 추가 될 수있는 모든 인스턴스 일 것입니다 . 다시 말하지만, 이것을 IoC 컨테이너에 쉽게 연결하여 관련 동작을 선택하도록 할 수 있지만 기능은 완전히 사용할 수 없습니다.
그리고 등등. .NET에서 DI를 찾을 수 있습니다. 일반적으로 DI로 생각조차하지 않도록 완벽하게 수행됩니다.
최대한의 유용성을 위해 DI 가능 라이브러리를 설계하려면 가장 가벼운 제안은 경량 컨테이너를 사용하여 고유 한 기본 IoC 구현을 제공하는 것입니다. IContainer
.NET Framework 자체의 일부이기 때문에 이에 대한 훌륭한 선택입니다.
답변
편집 2015 : 시간이 지났습니다.이 모든 것이 큰 실수라는 것을 깨달았습니다. IoC 컨테이너는 끔찍하며 DI는 부작용을 처리하기에 매우 열악한 방법입니다. 효과적으로, 여기에있는 모든 대답 (및 질문 자체)은 피해야합니다. 부작용을 인식하고 순수 코드와 분리하면 다른 모든 것이 제자리에 있거나 관련이없고 불필요한 복잡성이 발생합니다.
원래 답변은 다음과 같습니다.
SolrNet 을 개발하는 동안 동일한 결정을 내려야했습니다 . DI에 친숙하고 컨테이너에 구애받지 않는다는 목표로 시작했지만 점점 더 많은 내부 구성 요소를 추가함에 따라 내부 공장을 신속하게 관리 할 수 없게되었고 결과 라이브러리는 융통성이 없었습니다.
나는 매우 간단한 임베디드 IoC 컨테이너 를 작성 하는 동시에 Windsor 기능 과 Ninject 모듈을 제공했다 . 라이브러리를 다른 컨테이너와 통합하는 것은 구성 요소를 올바르게 연결하는 것이므로 Autofac, Unity, StructureMap 등과 쉽게 통합 할 수 있습니다.
이것의 단점은 new
서비스를 향상시키는 능력을 잃어 버렸다는 것 입니다. 또한 임베디드 컨테이너를 쉽게 구현하기 위해 피할 수있는 CommonServiceLocator에 의존했습니다 (나중에 리팩터링 할 수 있음).
이 블로그 게시물 에 대한 자세한 내용 .
MassTransit 은 비슷한 것에 의존하는 것 같습니다. 여기에는 몇 가지 메소드가 더있는 CommonServiceLocator의 IServiceLocator 인 IObjectBuilder 인터페이스가 있으며, 각 컨테이너 (예 : NinjectObjectBuilder) 및 일반 모듈 / 기능 (예 : MassTransitModule) 에 대해이를 구현합니다 . 그런 다음 IObjectBuilder 를 사용하여 필요한 것을 인스턴스화합니다. 이것은 물론 유효한 접근 방식이지만 실제로 서비스 로케이터로 사용하여 컨테이너를 너무 많이 통과하기 때문에 개인적으로별로 좋아하지 않습니다.
MonoRail 은 자체 컨테이너 를 구현 하여 오래된 IServiceProvider 를 구현 합니다. 이 컨테이너는 잘 알려진 서비스를 제공하는 인터페이스를 통해이 프레임 워크 전체에서 사용됩니다 . 콘크리트 컨테이너를 얻기 위해 기본 제공 서비스 공급자 로케이터가 있습니다. 윈저 시설 이 선택한 서비스 제공하고, 윈저이 서비스 프로 바이더 로케이터를 가리 킵니다.
결론 : 완벽한 해결책은 없습니다. 모든 디자인 결정과 마찬가지로이 문제는 유연성, 유지 관리 용이성 및 편의성 간의 균형을 요구합니다.
답변
내가 할 일은 컨테이너에 대한 종속성을 가능한 한 많이 제한하기 위해 DI 컨테이너에 독립적으로 라이브러리를 디자인하는 것입니다. 필요한 경우 DI 컨테이너를 다른 컨테이너로 교체 할 수 있습니다.
그런 다음 DI 로직 위의 레이어를 라이브러리 사용자에게 노출하여 인터페이스를 통해 선택한 프레임 워크를 사용할 수 있도록합니다. 이러한 방식으로 노출 된 DI 기능을 계속 사용할 수 있으며 다른 프레임 워크를 자유롭게 사용할 수 있습니다.
라이브러리 사용자가 자신의 DI 프레임 워크를 꽂도록 허용하면 유지 관리 비용이 크게 증가하므로 나에게는 약간 잘못된 것 같습니다. 이것은 또한 스트레이트 DI보다 플러그인 환경이 더 많이됩니다.