Bağımlılık enjeksiyonu nasıl kullanılır ve zamansal bağlantıdan nasıl kaçınılır?


11

Ben Serviceyapıcı yoluyla bağımlılıkları alır ama aynı zamanda kullanılabilmesi için özel veri (bağlam) ile başlatılması gerektiğini varsayalım :

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

Şimdi - Bağlam verileri önceden bilmiyor, bu yüzden onu bağımlılık olarak kaydedemiyorum ve DI'yi servise enjekte etmek için kullanamıyorum

Örnek istemci şöyle görünür:

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

Gördüğünüz gibi - geçici eşleştirme var ve yöntem kodu kokularını başlatma, çünkü önce ve sonra service.Initializearayabilmek için aramam gerekiyor .service.DoSomethingservice.DoOtherThing

Bu sorunları giderebileceğim diğer yaklaşımlar nelerdir?

Davranışın ek açıklaması:

İstemcinin her örneğinin, müşterinin özel bağlam verileriyle başlatılmış kendi hizmet örneğine sahip olması gerekir. Dolayısıyla, bu bağlam verisi statik değildir veya önceden bilinmemektedir, bu nedenle yapıcıya DI tarafından enjekte edilemez.

Yanıtlar:


18

Başlatma sorunuyla başa çıkmanın birkaç yolu vardır:

  • Https://softwareengineering.stackexchange.com/a/334994/301401 adresinde yanıtlandığı gibi , init () yöntemleri bir kod kokusudur. Bir nesneyi başlatmak, kurucunun sorumluluğundadır - bu yüzden sonuçta yapıcılarımız var.
  • Ekle Verilen hizmet , kurucunun doc yorumuna başlatılmalı ve hizmet başlatılmamışsa kurucunun Clientatmasına izin verilmelidir . Bu, sorumluluğu size IServicenesneyi veren kişiye taşır .

Ancak, örneğinizde, Clientiletilen değerleri bilen tek kişi budur Initialize(). Bu şekilde tutmak istiyorsanız, aşağıdakileri öneririm:

  • Bir ekleyin IServiceFactoryve yapıcıya Clientiletin. Ardından , müşteriniz tarafından kullanılabilecek serviceFactory.createService(new Context(...))bir başlangıç ​​değeri veren aramayı arayabilirsiniz IService.

Fabrikalar çok basit olabilir ve ayrıca init () yöntemlerinden kaçınmanıza ve bunun yerine yapıcıları kullanmanıza izin verebilir:

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

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

İstemcide, OnStartup()aynı zamanda bir başlatma yöntemidir (sadece farklı bir ad kullanır). Mümkünse ( Contextverileri biliyorsanız ), fabrika doğrudan Clientyapıcıda çağrılmalıdır . Bu mümkün değilse, saklamanız IServiceFactoryve aramanız gerekir OnStartup().

ServiceBağımlılık sağlamadığı zaman ClientDI tarafından aşağıdakiler yoluyla sağlanacaktır 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
Teşekkür ederim, tıpkı düşündüğüm gibi, son noktada ... Ve ServiceFactory'de, servis yapıcısı veya servis bulucu için gereken bağımlılıklar için fabrikadaki DI kurucusunu kullanır mısınız?
Dusan

1
@Dusan Servis Bulucu kullanmaz. Eğer Servicebaşka bağımlılıklar vardır Contexttarafından sağlanacak olmaz, Clientonlar için DI yoluyla sağlanabilir ServiceFactorygeçirilecek Servicezaman createServicedenir.
Bay Mind

@Dusan Farklı Hizmetlere farklı bağımlılıklar sağlamanız gerekiyorsa (ör. Bu bir bağımlılığa1_1 ihtiyaç duyar ancak bir sonrakine bağımlılık1_2 gerekir), ancak bu kalıp sizin için uygunsa, genellikle Oluşturucu kalıbı adı verilen benzer bir kalıp kullanabilirsiniz. Bir Oluşturucu, gerektiğinde parça parça bir nesne kurmanıza izin verir. Sonra bunu yapabilirsin ... ServiceBuilder partial = new ServiceBuilder().dependency1(dependency1_1).dependency2(dependency2_1).dependency3(dependency3_1);ve kısmi kurulum hizmetinden Service s = partial.context(context).build()
Aaron

1

InitializeYöntem çıkarılmalıdır IServicebu bir uygulama ayrıntı olarak, arayüz. Bunun yerine, Service'in somut örneğini alan ve bunun üzerinde initialize yöntemini çağıran başka bir sınıf tanımlayın. Sonra bu yeni sınıf IService arabirimini uygular:

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

        service.Initialize(context);
    }

    // Methods in the IService interface
}

Bu, ContextDependentServicesınıfın nereden başlatıldığı dışında istemci kodunu başlatma yordamından habersiz tutar . En azından uygulamanızın bu gizli başlatma prosedürü hakkında bilmeniz gereken kısımlarını sınırlandırıyorsunuz.


1

Bana öyle geliyor ki burada iki seçeneğin var

  1. Başlatma kodunu Bağlam'a taşıyın ve Başlatılan Bağlamı enjekte edin

Örneğin.

public InitialisedContext Initialise()
  1. Zaten yapılmadıysa, İlk arama işlemini başlatmak için ilk çağrıyı yapın

Örneğin.

public async Task Execute()
{
     //lock context
     //check context is not initialised
     // init if required
     //execute code...
}
  1. Execute'u çağırdığınızda Bağlam başlatılmazsa istisnalar atmanız yeterlidir. SqlConnection gibi.

Bağlamı parametre olarak geçirmekten kaçınmak istiyorsanız, fabrikaya enjekte etmek iyidir. Yalnızca bu özel uygulamanın bir bağlama ihtiyacı olduğunu ve onu Arayüze eklemek istemediğinizi varsayalım

Ama aslında aynı sorunla karşı karşıyasınız, ya fabrika henüz başlatılmış bir bağlama sahip değilse.


0

Arayüzünüzü herhangi bir db içeriğine bağımlı hale getirmemeli ve başlatma yöntemini kullanmamalısınız. Beton sınıfı yapıcıda yapabilirsiniz.

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

Ve ana sorunuzun cevabı Mülkiyet Enjeksiyonu olacaktır .

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

        //...
    }

Bu şekilde Mülk Enjeksiyonu ile tüm bağımlılıkları çağırabilirsiniz . Ama çok büyük bir sayı olabilir. Öyleyse, bunlar için Yapıcı Enjeksiyonunu kullanabilirsiniz, ancak içeriğinizi null olup olmadığını kontrol ederek özelliğe göre ayarlayabilirsiniz.


Tamam, harika, ama ... istemcinin her örneğinin, farklı bağlam verileriyle başlatılan hizmetin kendi örneğine sahip olması gerekir. Bu bağlam verisi statik değildir veya önceden bilinmemektedir, bu nedenle yapıcıya DI tarafından enjekte edilemez. Ardından, istemcilerimdeki diğer bağımlılıklarla birlikte hizmetin örneğini nasıl alabilirim / oluşturabilirim?
Dusan

hmm bağlamı ayarlamadan önce statik yapıcı çalıştırmaz mı? ve kurucu risk istisnalarında başlangıç
Ewan

Hizmeti verilen bağlam verileriyle (hizmetin kendisini enjekte etmek yerine) oluşturabilen ve başlatabilen enjeksiyon fabrikasına yöneliyorum, ancak daha iyi çözümler olup olmadığından emin değilim.
Dusan

@Ewan Haklısın. Bunun için bir çözüm bulmaya çalışacağım. Ama bundan önce, şimdilik kaldıracağım.
Engineert

0

Misko Hevery'nin karşılaştığınız dava hakkında çok yararlı bir blog yazısı var. Hem ihtiyaç newable ve enjektabl sizin için Servicesınıf ve bu blog yayınında size yardımcı olabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.