의존성 주입을 사용하고 시간적 결합을 피하는 방법?


11

Service생성자를 통해 종속성을 수신하지만 사용하기 전에 사용자 정의 데이터 (컨텍스트)로 초기화해야 한다고 가정 합니다.

public interface IService
{
    void Initialize(Context context);
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3)
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));
    }

    public void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

public class Context
{
    public int Value1;
    public string Value2;
    public string Value3;
}

이제 컨텍스트 데이터를 미리 알 수 없으므로 종속성으로 등록하고 DI를 사용하여 서비스에 주입 할 수 없습니다

다음은 예제 클라이언트의 모습입니다.

public class Client
{
    private readonly IService service;

    public Client(IService service)
    {
        this.service = service ?? throw new ArgumentNullException(nameof(service));
    }

    public void OnStartup()
    {
        service.Initialize(new Context
        {
            Value1 = 123,
            Value2 = "my data",
            Value3 = "abcd"
        });
    }

    public void Execute()
    {
        service.DoSomething();
        service.DoOtherThing();
    }
}

당신이 볼 수 있듯이 - 내가 먼저 필요 호출 할 수 있기 때문에 시간적 커플 링 및 초기화 방법 코드가 포함 된 냄새가 service.Initialize전화를 할 수 있도록 service.DoSomething하고 service.DoOtherThing이후.

이러한 문제를 제거 할 수있는 다른 방법은 무엇입니까?

행동에 대한 추가 설명 :

클라이언트의 각 인스턴스는 클라이언트의 특정 컨텍스트 데이터로 초기화 된 고유 한 서비스 인스턴스를 가져야합니다. 따라서 해당 컨텍스트 데이터는 정적이거나 미리 알려져 있지 않으므로 생성자에서 DI에 의해 주입 될 수 없습니다.

답변:


18

초기화 문제를 처리하는 방법에는 여러 가지가 있습니다.

  • https://softwareengineering.stackexchange.com/a/334994/301401 에서 대답했듯이 init () 메소드는 코드 냄새입니다. 객체를 초기화하는 것은 생성자의 책임입니다. 그래서 우리는 결국 생성자가 있습니다.
  • 추가 지정된 서비스는Client 생성자 의 doc 주석 으로 초기화해야하며 서비스가 초기화되지 않은 경우 생성자가 던지도록해야합니다. 이것은 책임을 당신에게 IService물건 을주는 사람에게 옮깁니다 .

그러나 귀하의 예에서는 Client에 전달되는 값을 아는 유일한 것입니다 Initialize(). 그렇게 유지하려면 다음을 제안합니다.

  • 를 추가 IServiceFactory하고 Client생성자에 전달하십시오 . 그런 다음 클라이언트가 사용할 수 serviceFactory.createService(new Context(...))있는 초기화를 제공하는 호출 IService할 수 있습니다.

팩토리는 매우 간단하고 init () 메소드를 피하고 대신 생성자를 사용할 수 있습니다.

public interface IServiceFactory
{
    IService createService(Context context);
}

public class ServiceFactory : IServiceFactory
{
    public Service createService(Context context)
    {
        return new Service(context);
    }
}

클라이언트에서는 OnStartup()초기화 방법이기도합니다 (다른 이름 만 사용함). 따라서 가능하다면 ( Context데이터 를 알고 있다면 ) 팩토리는 Client생성자 에서 직접 호출해야합니다 . 이것이 가능하지 않으면를 저장 IServiceFactory하고에 전화해야합니다 OnStartup().

Service에서 제공하지 않는 의존성이 Client그들이 통해 DI에 의해 제공 될 수를 ServiceFactory:

public interface IServiceFactory
{
    IService createService(Context context);
}    

public class ServiceFactory : IServiceFactory
{        
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;

    public ServiceFactory(object dependency1, object dependency2, object dependency3)
    {
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
        this.dependency3 = dependency3;
    }

    public Service createService(Context context)
    {
        return new Service(context, dependency1, dependency2, dependency3);
    }
}

1
마지막 시점에서 생각했던 것처럼 감사합니다 ... ServiceFactory에서 서비스 생성자 또는 서비스 로케이터에 필요한 종속성에 대해 팩토리 자체에서 생성자 DI를 사용 하시겠습니까?
Dusan

1
@Dusan은 Service Locator를 사용하지 않습니다. 에 의해 제공되지 않는의 Service이외의 종속성이있는 경우 DI를 통해 호출 될 때 전달되도록 DI를 통해 제공 될 수 있습니다 . ContextClientServiceFactoryServicecreateService
Mr.Mindor 19 년

@Dusan 다른 서비스에 서로 다른 종속성을 제공해야하는 경우 (예 :이 항목은 dependency1_1이 필요하지만 다음 항목은 dependency1_2가 필요함)이 패턴이 다른 방식으로 작동하면 빌더 패턴이라고하는 유사한 패턴을 사용할 수 있습니다. 빌더를 사용하면 필요한 경우 시간이 지남에 따라 오브젝트 단편을 설정할 수 있습니다. 그러면 당신은 할 수 있습니다 ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);그리고 부분적으로 설정된 서비스를 남겨두고 나중에하세요Service s = partial.context(context).build()
Aaron

1

Initialize방법은 제거해야 IService이 구현의 세부 사항대로, 인터페이스. 대신, 구체적인 Service 인스턴스를 가져 와서 initialize 메소드를 호출하는 다른 클래스를 정의하십시오. 그런 다음이 새로운 클래스는 IService 인터페이스를 구현합니다.

public class ContextDependentService : IService
{
    public ContextDependentService(Context context, Service service)
    {
        this.service = service;

        service.Initialize(context);
    }

    // Methods in the IService interface
}

이렇게하면 ContextDependentService클래스가 초기화 되는 경우를 제외하고 클라이언트 코드가 초기화 절차를 무시합니다 . 이 기발한 초기화 절차에 대해 알아야하는 응용 프로그램 부분을 최소한 제한하십시오.


1

여기에 두 가지 옵션이있는 것 같습니다.

  1. 초기화 코드를 컨텍스트로 이동하고 초기화 된 컨텍스트를 삽입하십시오.

예.

public InitialisedContext Initialise()
  1. 통화 초기화가 아직 완료되지 않은 경우 첫 번째 통화 실행

예.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. 실행을 호출 할 때 컨텍스트가 초기화되지 않은 경우 예외를 throw하십시오. SqlConnection처럼.

컨텍스트를 매개 변수로 전달하지 않으려면 팩토리를 주입하는 것이 좋습니다. 이 특정 구현에만 컨텍스트가 필요하며 인터페이스에 추가하지 않으려는 경우

그러나 팩토리에 아직 초기화 된 컨텍스트가없는 경우 본질적으로 동일한 문제가 있습니다.


0

인터페이스를 db 컨텍스트 및 초기화 방법에 의존해서는 안됩니다. 구체적인 클래스 생성자에서 할 수 있습니다.

public interface IService
{
    void DoSomething();
    void DoOtherThing();
}

public class Service : IService
{
    private readonly object dependency1;
    private readonly object dependency2;
    private readonly object dependency3;
    private readonly object context;

    public Service(
        object dependency1,
        object dependency2,
        object dependency3,
        object context )
    {
        this.dependency1 = dependency1 ?? throw new ArgumentNullException(nameof(dependency1));
        this.dependency2 = dependency2 ?? throw new ArgumentNullException(nameof(dependency2));
        this.dependency3 = dependency3 ?? throw new ArgumentNullException(nameof(dependency3));

        // context is concrete class details not interfaces.
        this.context = context;

        // call init here constructor.
        this.Initialize(context);
    }

    protected void Initialize(Context context)
    {
        // Initialize state based on context
        // Heavy, long running operation
    }

    public void DoSomething()
    {
        // ...
    }

    public void DoOtherThing()
    {
        // ...
    }
}

그리고 주요 질문에 대한 답변은 Property Injection 입니다.

public class Service
    {
        public Service(Context context)
        {
            this.context = context;
        }

        private Dependency1 _dependency1;
        public Dependency1 Dependency1
        {
            get
            {
                if (_dependency1 == null)
                    _dependency1 = Container.Resolve<Dependency1>();

                return _dependency1;
            }
        }

        //...
    }

이 방법으로 속성 삽입으로 모든 종속성을 호출 할 수 있습니다 . 그러나 그것은 엄청난 숫자 일 수 있습니다. 그렇다면 생성자 주입을 사용할 수 있지만 속성이 null인지 확인하여 컨텍스트별로 컨텍스트를 설정할 수 있습니다.


좋아, 훌륭하지만 ... 클라이언트의 각 인스턴스에는 다른 컨텍스트 데이터로 초기화 된 자체 서비스 인스턴스가 있어야합니다. 해당 컨텍스트 데이터는 정적이거나 사전에 알려지지 않았으므로 생성자에서 DI에 의해 주입 될 수 없습니다. 그런 다음 클라이언트의 다른 종속성과 함께 서비스 인스턴스를 어떻게 가져 오거나 생성합니까?
Dusan

흠 컨텍스트를 설정하기 전에 정적 생성자가 실행되지 않습니까? 생성자 예외에서 초기화
Ewan

서비스 자체를 주입하는 대신 주어진 컨텍스트 데이터로 서비스를 생성하고 초기화 할 수있는 공장에 주입하고 있지만 더 나은 솔루션이 있는지 확실하지 않습니다.
Dusan

@Ewan 당신이 맞아요. 나는 그것을위한 해결책을 찾으려고 노력할 것이다. 그러나 그 전에는 지금 제거하겠습니다.
Engineert

0

Misko Hevery는 귀하가 직면 한 사건에 대한 매우 유용한 블로그 게시물을 보유하고 있습니다. 당신 둘 필요 newable주사 당신을위한 Service클래스와 이 블로그 게시물 을하는 데 도움이 될 수 있습니다.

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