如何使用依赖注入并避免时间耦合?


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.DoSomethingservice.DoOtherThing事后。

我还有哪些其他方法可以消除这些问题?

有关行为的其他说明:

客户端的每个实例都需要使用客户端的特定上下文数据初始化其自己的服务实例。因此,上下文数据不是静态的,也不是事先已知的,因此DI不能将其注入构造函数中。

Answers:


18

有几种方法可以处理初始化问题:

  • /software//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除之外Context没有其他依赖项Client,则可以通过DI将其提供给,ServiceFactory以传递给Servicewhen createService调用。
Mindor先生,

@Dusan如果您需要为不同的服务提供不同的依赖关系(即:这个需要依赖关系1_1,而下一个需要依赖关系1_2),但是如果该模式对您有用,那么您可以使用类似的模式,通常称为Builder模式。使用构建器,您可以根据需要逐步设置对象。然后,您可以执行此操作... 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. 如果在调用Execute时未初始化Context,则仅引发异常。像SqlConnection。

如果只想避免将上下文作为参数传递,则注入工厂是可以的。说仅此特定实现需要上下文,而您不想将其添加到接口

但是您本质上也有同样的问题,如果工厂还没有初始化的上下文呢?


0

您不应将接口依赖于任何数据库上下文和初始化方法。您可以在具体的类构造函数中执行此操作。

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

        //...
    }

这样,您可以通过Property Injection调用所有依赖项。但这可能是巨大的。如果是这样,则可以为它们使用构造方法注入,但是可以通过检查属性是否为null来按属性设置上下文。


好的,很好,但是...每个客户端实例都需要使用不同的上下文数据初始化它自己的服务实例。该上下文数据不是静态的,也不是事先已知的,因此DI不能将其注入构造函数中。然后,如何在客户端中获取/创建服务实例以及其他依赖关系?
杜尚(Dusan),

嗯,在设置上下文之前,静态构造函数是否会运行?并在构造函数中初始化存在风险异常
Ewan

我倾向于注入可以使用给定的上下文数据创建和初始化服务的工厂(而不是注入服务本身),但是我不确定是否有更好的解决方案。
杜尚(Dusan),

@Ewan你是对的。我将尝试找到解决方案。但是在此之前,我现在将其删除。
Engineert

0

Misko Hevery有一篇非常有用的博客文章,介绍了您所遇到的情况。您的课程都需要可更新可注入的内容Service此博客文章可能会对您有所帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.