Jak stosować wstrzykiwanie zależności i unikać czasowego łączenia?

11

Załóżmy, że mam ten, Servicektóry odbiera zależności za pośrednictwem konstruktora, ale przed użyciem można go także zainicjować za pomocą niestandardowych danych (kontekstu):

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

Teraz - dane kontekstowe nie są wcześniej znane, więc nie mogę zarejestrować ich jako zależności i użyć DI, aby wprowadzić je do usługi

Tak wygląda przykładowy klient:

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

Jak widać - w grę wchodzą tymczasowe sprzężenia i inicjalizacja zapachów kodu metody, ponieważ najpierw muszę zadzwonić, service.Initializeaby móc zadzwonić, service.DoSomethinga service.DoOtherThingpotem.

Jakie są inne podejścia, w których mogę wyeliminować te problemy?

Dodatkowe wyjaśnienie zachowania:

Każde wystąpienie klienta musi mieć własne wystąpienie usługi zainicjowane konkretnymi danymi kontekstowymi klienta. Tak więc dane kontekstowe nie są statyczne ani znane z góry, więc nie można ich wprowadzić do konstruktora.

Dusan
źródło

Odpowiedzi:

18

Istnieje kilka sposobów rozwiązania problemu inicjalizacji:

  • Zgodnie z odpowiedzią w https://softwareengineering.stackexchange.com/a/334994/301401 , metody init () mają zapach kodu. Inicjowanie obiektu jest obowiązkiem konstruktora - dlatego w końcu mamy konstruktory.
  • Dodaj Daną usługę należy zainicjować do komentarza doc Clientkonstruktora i pozwolić konstruktorowi na rzucenie, jeśli usługa nie została zainicjowana. Przenosi to odpowiedzialność na tego, który daje ci IServiceprzedmiot.

Jednak w twoim przykładzie Clientjest to jedyny, który zna wartości, które są przekazywane Initialize(). Jeśli chcesz tak pozostać, proponuję:

  • Dodaj IServiceFactoryi przekaż go do Clientkonstruktora. Następnie możesz zadzwonić, serviceFactory.createService(new Context(...))co daje inicjalizację, z IServicektórej może korzystać klient.

Fabryki mogą być bardzo proste, a także pozwalają uniknąć metod init () i zamiast tego korzystać z konstruktorów:

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

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

W kliencie OnStartup()jest również metoda inicjalizacji (używa tylko innej nazwy). Jeśli to możliwe (jeśli znasz Contextdane), fabryka powinna zostać bezpośrednio wywołana w Clientkonstruktorze. Jeśli nie jest to możliwe, musisz je zapisać IServiceFactoryi wywołać OnStartup().

Gdy Servicezależności, których nie zapewnia Client, byłyby dostarczane przez DI poprzez 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
źródło
1
Dziękuję, tak jak myślałem, w ostatnim punkcie ... A w ServiceFactory, czy użyłbyś konstruktora DI w samej fabryce, aby zależności potrzebne konstruktorowi usług lub lokalizatorowi usług byłyby bardziej odpowiednie?
Dusan
1
@Dusan nie używaj Lokalizatora usług. Jeśli Serviceistnieją zależności inne niż te Context, które nie byłyby zapewnione przez Client, można je podać za pośrednictwem DI do ServiceFactoryprzekazania do Servicemomentu createServicewywołania.
Mr.Mindor
@Dusan Jeśli musisz podać różne zależności dla różnych usług (tj. Ta wymaga zależności1_1, ale kolejna zależy zależności1_2), ale jeśli ten wzór działa inaczej, możesz użyć podobnego wzorca często nazywanego wzorcem konstruktora. Konstruktor umożliwia ustawienie fragmentarycznego obiektu w miarę upływu czasu. Następnie możesz to zrobić ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);i pozostać z częściowo skonfigurowaną usługą, a później zrobićService s = partial.context(context).build()
Aaron
1

InitializeMetoda powinna być usunięta z IServiceinterfejsu użytkownika, jak to jest szczegółowo realizacji. Zamiast tego zdefiniuj inną klasę, która pobiera konkretną instancję usługi i wywołuje na niej metodę inicjalizacji. Następnie ta nowa klasa implementuje interfejs IService:

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

        service.Initialize(context);
    }

    // Methods in the IService interface
}

Dzięki temu kod klienta nie będzie wiedział o procedurze inicjalizacji, z wyjątkiem przypadków, w których ContextDependentServiceinicjowana jest klasa. Ograniczasz przynajmniej te części aplikacji, które muszą wiedzieć o tej chwiejnej procedurze inicjalizacji.

Greg Burghardt
źródło
1

Wydaje mi się, że masz tutaj dwie opcje

  1. Przenieś kod inicjalizacji do kontekstu i wstrzykuj zainicjowany kontekst

na przykład.

public InitialisedContext Initialise()
  1. Wykonaj pierwsze połączenie, aby wykonać połączenie inicjujące, jeśli jeszcze nie zostało wykonane

na przykład.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. Rzuć wyjątki, jeśli kontekst nie zostanie zainicjowany po wywołaniu polecenia Wykonaj. Jak SqlConnection.

Wstrzyknięcie fabryki jest w porządku, jeśli chcesz uniknąć przekazywania kontekstu jako parametru. Powiedz, że tylko ta konkretna implementacja potrzebuje kontekstu i nie chcesz dodawać jej do interfejsu

Ale zasadniczo masz ten sam problem, co jeśli fabryka nie ma jeszcze zainicjowanego kontekstu.

Ewan
źródło
0

Nie powinieneś zależeć od interfejsu od żadnego kontekstu db i metody inicjalizacji. Możesz to zrobić w konstruktorze klas betonowych.

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

Odpowiedź na twoje główne pytanie brzmi: Zastrzyk nieruchomości .

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

        //...
    }

W ten sposób możesz wywoływać wszystkie zależności za pomocą Zastrzyku właściwości . Ale może to być ogromna liczba. Jeśli tak, możesz użyć Konstruktora dla nich, ale możesz ustawić swój kontekst według właściwości, sprawdzając, czy jest on pusty.

Engineert
źródło
OK, świetnie, ale ... każda instancja klienta musi mieć własną instancję usługi zainicjowaną przy użyciu różnych danych kontekstowych. Dane kontekstowe nie są statyczne ani znane wcześniej, więc nie można ich wprowadzić do konstruktora. W jaki sposób mogę uzyskać / utworzyć instancję usługi wraz z innymi zależnościami u moich klientów?
Dusan
hmm, czy ten statyczny konstruktor nie uruchomi się przed ustawieniem kontekstu? i inicjowanie w wyjątkach ryzyka konstruktora
Ewan
Skłaniam się do wstrzyknięcia fabryki, która może utworzyć i zainicjować usługę na podstawie danych kontekstowych (zamiast wstrzykiwać samą usługę), ale nie jestem pewien, czy istnieją lepsze rozwiązania.
Dusan
@Ewan Masz rację. Spróbuję znaleźć na to rozwiązanie. Ale wcześniej to usunę.
Engineert
0

Misko Hevery ma bardzo pomocny post na blogu na temat twojej sprawy. Oboje potrzebujesz neowalnego i wstrzykiwalnego dla swojej Serviceklasy, a ten post na blogu może ci pomóc.

Ersoy
źródło