Як використовувати ін'єкцію залежності та уникнути тимчасової зв’язки?


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 () - це кодовий запах. Ініціалізація об'єкта - це відповідальність конструктора - саме тому у нас є конструктори.
  • Додати Дана служба повинна бути ініціалізована до коментаря doc про 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);
    }
}

1
Дякую, як я думав, в останньому пункті ... І в ServiceFactory ви б використали конструктор DI на самому заводі для залежностей, необхідних для конструктора сервісу, або службовий локатор був би більш підходящим?
Душан

1
@Dusan не використовує службу пошуку. Якщо Serviceє інші залежності, ніж ті Context, які не були б забезпечені Client, вони можуть бути надані через DI, щоб ServiceFactoryпередаватись Serviceтоді, коли createServiceвикликається.
Містер Міндор

@Dusan Якщо вам потрібно поставити різні залежності для різних сервісів (тобто для цього потрібна залежність1_1, але наступна - залежність1_2), але якщо ця модель інакше працює для вас, ви можете використовувати аналогічну схему, яку часто називають схемою Builder. Builder дозволяє встановити частину предмета з часом, якщо це необхідно. Тоді ви можете зробити це ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);і залишитись з частково налаштованою службою, потім пізнішеService s = partial.context(context).build()
Aaron

1

InitializeМетод повинен бути вилучений з IServiceінтерфейсу, так як це деталь реалізації. Натомість визначте інший клас, який приймає конкретний екземпляр Сервісу та викликає метод ініціалізації на ньому. Тоді цей новий клас реалізує інтерфейс 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.

Вводити фабрику добре, якщо ви просто хочете уникати передачі контексту як параметра. Скажіть, що для цієї конкретної реалізації потрібен контекст, і ви не хочете додавати його в інтерфейс

Але у вас по суті є та ж проблема, що робити, якщо фабрика ще не має ініціалізованого контексту.


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()
    {
        // ...
    }
}

І, відповідь на ваше головне питання - це введення власності .

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

        //...
    }

Таким чином ви можете викликати всі залежності за допомогою ін'єкції властивостей . Але це може бути величезна кількість. Якщо це так, ви можете використовувати Constructor Injection для них, але ви можете встановити свій контекст за властивістю, перевіривши, чи він недійсний.


Добре, чудово, але ... кожен екземпляр клієнта повинен мати власний екземпляр послуги, ініціалізований з різними контекстними даними. Ці контекстні дані не є статичними або заздалегідь відомими, тому їх не можна вводити DI в конструктор. Потім, як я можу отримати / створити екземпляр послуги разом з іншими залежностями у своїх клієнтів?
Душан

Хм, звичайно, що статичний конструктор працює перед тим, як встановити контекст? і ініціалізувати у конструкторі ризики винятку
Еван

Я схиляюся до ін'єкційної фабрики, яка може створити та ініціалізувати службу із заданими контекстними даними (а не сама інжекційна служба), але я не впевнений, чи є кращі рішення.
Душан

@Ewan Ви маєте рацію. Я спробую знайти для цього рішення. Але до цього я її зараз видалю.
Інженер

0

Місько Гевери має дуже корисну публікацію в блозі про випадок, з яким ви стикалися. Вам обом потрібні новинки та ін’єкції для вашого Serviceкласу, і ця публікація в блозі може вам допомогти.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.