Как использовать внедрение зависимостей и избежать временной связи?

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

Есть несколько способов справиться с проблемой инициализации:

  • Как указано в /software//a/334994/301401 , методы init () являются запахом кода. Инициализация объекта является обязанностью конструктора - поэтому у нас есть конструкторы.
  • Добавить Данный сервис должен быть инициализирован в комментарии документа Clientконструктора и позволить конструктору выдать, если сервис не инициализирован. Это переносит ответственность на того, кто дает вам 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);
    }
}
Pschill
источник
1
Спасибо, как я и думал, в последнем пункте ... И в ServiceFactory, вы бы использовали конструктор DI в самой фабрике для зависимостей, необходимых для конструктора сервиса, или локатор службы был бы более подходящим?
Душан
1
@Dusan не используйте сервисный локатор. Если Serviceесть другие , чем зависимости Context, которые не будут предоставляться Client, они могут быть предоставлены через DI в ServiceFactoryбудет приняты к Serviceкогда createServiceназывается.
Mr.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. Просто выбросьте исключения, если Context не инициализирован при вызове Execute. Как SqlConnection.

Впрыскивать фабрику - это хорошо, если вы просто хотите избежать передачи контекста в качестве параметра. Скажем, только этой конкретной реализации нужен контекст, и вы не хотите добавлять его в интерфейс

Но у вас, по сути, та же проблема: что, если у фабрики еще нет инициализированного контекста?

Ewan
источник
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()
    {
        // ...
    }
}

И ответом на ваш главный вопрос будет « Инъекция собственности» .

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 . Но это может быть огромное количество. Если это так, вы можете использовать Constructor Injection для них, но вы можете установить свой контекст по свойству, проверив, является ли он пустым.

Engineert
источник
Хорошо, отлично, но ... каждый экземпляр клиента должен иметь свой собственный экземпляр службы, инициализированный различными контекстными данными. Эти данные контекста не являются статичными или известны заранее, поэтому они не могут быть вставлены DI в конструктор. Затем, как мне получить / создать экземпляр службы вместе с другими зависимостями в моих клиентах?
Душан
хмм, разве статический конструктор не запустится до того, как вы установите контекст? и инициализировать в конструкторе риски исключений
Ewan
Я склоняюсь к внедрению фабрики, которая может создавать и инициализировать службу с заданными контекстными данными (а не внедрять саму службу), но я не уверен, что есть лучшие решения.
Душан
@ Иван Ты прав. Я постараюсь найти решение для этого. Но до этого я его сейчас уберу.
Engineert
0

Миско Хевери имеет очень полезную запись в блоге о деле, с которым вы столкнулись. Вы оба нуждаетесь в новых и инъекционных препаратах для своего Serviceкласса, и этот пост может помочь вам.

Жирный П.
источник