Bağımlılık Enjeksiyonu (DI) “dost” kütüphanesi


230

Birkaç farklı üst düzey işlevleri olacak bir C # kitaplığı tasarımı düşünmek. Tabii ki, bu üst düzey işlevler mümkün olduğunca SOLID sınıfı tasarım ilkeleri kullanılarak uygulanacaktır . Bu nedenle, muhtemelen tüketicilerin doğrudan düzenli olarak kullanması amaçlanan sınıflar ve bu daha yaygın "son kullanıcı" sınıflarının bağımlılıkları olan "destek sınıfları" olacaktır.

Soru şu ki, kütüphaneyi tasarlamanın en iyi yolu nedir?

  • DI Agnostic - Ortak DI kütüphanelerinden biri veya ikisi için (StructureMap, Ninject, vb.) Temel "destek" eklemek makul görünse de, tüketicilerin kütüphaneyi herhangi bir DI çerçevesiyle kullanabilmelerini istiyorum.
  • DI kullanılamaz - Kütüphanenin bir tüketicisi DI kullanmıyorsa, kütüphanenin kullanımı hala mümkün olduğunca kolay olmalı ve kullanıcının bu "önemsiz" bağımlılıkları yaratmak için yapması gereken iş miktarını azaltmalıdır. kullanmak istedikleri "gerçek" sınıflar.

Şu anki düşüncem, ortak DI kütüphaneleri (örneğin bir StructureMap kayıt defteri, Ninject modülü) ve DI olmayan ve bu birkaç fabrikaya bağlantıyı içeren bir küme veya Fabrika sınıfları için birkaç "DI kayıt modülü" sağlamaktır.

Düşünceler?


Bozuk bağlantı makalesine (SOLID) yeni referans: butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
M.Hassan 14:05

Yanıtlar:


360

DI'nin teknoloji değil kalıplar ve ilkeler ile ilgili olduğunu anladıktan sonra bunu yapmak basittir .

API'yi DI Container-agnostik bir şekilde tasarlamak için şu genel ilkeleri izleyin:

Bir uygulamaya değil, bir arayüze programlayın

Bu ilke aslında Tasarım Desenlerinden bir alıntıdır (ancak bellekten) , ancak her zaman gerçek hedefiniz olmalıdır . DI sadece bu amaca ulaşmak için bir araçtır .

Hollywood Prensibini Uygula

DI terimleriyle Hollywood Prensibi diyor ki: DI Container'ı arama, seni arayacak .

Kodunuzun içinden bir kapsayıcı arayarak asla doğrudan bağımlılık istemeyin. Yapıcı Enjeksiyon kullanarak örtük olarak isteyin .

Yapıcı Enjeksiyonu Kullan

Bir bağımlılığa ihtiyacınız olduğunda , yapıcı aracılığıyla statik olarak isteyin :

public class Service : IService
{
    private readonly ISomeDependency dep;

    public Service(ISomeDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    public ISomeDependency Dependency
    {
        get { return this.dep; }
    }
}

Service sınıfının değişmezlerini nasıl garanti ettiğine dikkat edin. Bir örnek oluşturulduktan sonra, Guard Clause ve readonlyanahtar kelimenin birleşimi nedeniyle bağımlılığın kullanılabilir olması garanti edilir .

Kısa ömürlü bir nesneye ihtiyacınız varsa Abstract Factory kullanın

Konstrüktör Enjeksiyonu ile enjekte edilen bağımlılıklar uzun ömürlü olma eğilimindedir, ancak bazen kısa ömürlü bir nesneye ihtiyacınız vardır veya bağımlılığı sadece çalışma zamanında bilinen bir değere dayanarak inşa edebilirsiniz.

Bkz bu fazla bilgi için.

Yalnızca Son Sorumlu An'da yaz

Nesneleri sonuna kadar ayırın. Normalde, uygulamanın giriş noktasında her şeyi bekleyebilir ve kablolayabilirsiniz. Buna Kompozisyon Kökü denir .

Daha fazla ayrıntı burada:

Cephe Kullanarak Basitleştirin

Ortaya çıkan API'nin acemi kullanıcılar için çok karmaşık hale geldiğini düşünüyorsanız, her zaman ortak bağımlılık kombinasyonlarını kapsayan birkaç Cephe sınıfı sağlayabilirsiniz .

Yüksek derecede keşfedilebilirliğe sahip esnek bir Cephe sağlamak için Akıcı İnşaatçılar sağlamayı düşünebilirsiniz. Bunun gibi bir şey:

public class MyFacade
{
    private IMyDependency dep;

    public MyFacade()
    {
        this.dep = new DefaultDependency();
    }

    public MyFacade WithDependency(IMyDependency dependency)
    {
        this.dep = dependency;
        return this;
    }

    public Foo CreateFoo()
    {
        return new Foo(this.dep);
    }
}

Bu, kullanıcının yazarak varsayılan bir Foo oluşturmasına olanak tanır

var foo = new MyFacade().CreateFoo();

Bununla birlikte, özel bir bağımlılık sağlamanın mümkün olduğu çok keşfedilebilir ve yazabilirsiniz.

var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();

MyFacade sınıfının birçok farklı bağımlılığı kapsadığını hayal ederseniz, genişletilebilirliği hala keşfedilebilir hale getirirken uygun varsayılanları nasıl sağlayacağını umarım.


FWIW, bu cevabı yazdıktan çok sonra, buradaki kavramları genişlettim ve DI-Friendly Libraries hakkında daha uzun bir blog yazısı ve DI-Friendly Frameworks hakkında bir eşlik yazısı yazdım .


21
Bu kağıt üzerinde harika görünse de, deneyimime göre, karmaşık şekillerde etkileşime giren çok sayıda iç bileşene sahip olduğunuzda, yönetilmesi gereken çok sayıda fabrikaya sahip olursunuz, bakımı zorlaştırırsınız. Ayrıca, fabrikalar oluşturdukları bileşenlerin yaşam tarzlarını yönetmek zorunda kalacaklar, kütüphaneyi gerçek bir kaba yükledikten sonra bu, kabın kendi yaşam tarzı yönetimiyle çelişecek. Fabrikalar ve cepheler gerçek konteynerlerin önüne geçer.
Mauricio Scheffer

4
Tüm bunları yapan bir proje henüz bulamadım .
Mauricio Scheffer

31
Safewhere'da bu şekilde yazılım geliştiriyoruz, bu yüzden deneyiminizi paylaşmıyoruz ...
Mark Seemann

19
Cephelerin elle kodlanması gerektiğini düşünüyorum, çünkü bileşenlerin bilinen (ve muhtemelen ortak) kombinasyonlarını temsil ediyorlar. DI Konteyner gerekli olmamalıdır, çünkü her şey elle bağlanabilir (Zavallı Adamın DI'sini düşünün). Bir Cephe'nin API'nizin kullanıcıları için isteğe bağlı bir kolaylık sınıfı olduğunu unutmayın. İleri düzey kullanıcılar yine de Cephe'yi atlamak ve bileşenleri kendi beğenilerine bağlamak isteyebilirler. Bunun için kendi DI Contaier'lerini kullanmak isteyebilirler, bu yüzden kullanmayacaklarsa belirli bir DI Konteyneri onlara zorlamak kaba olur. Mümkün ama önerilmez
Mark Seemann

8
Bu, SO'da şimdiye kadar gördüğüm en iyi cevap olabilir.
Nick Hodges

40

"Bağımlılık enjeksiyonu" teriminin, bir arada bahsettiğinizi görseniz bile, bir IoC kapsayıcısı ile hiçbir ilgisi yoktur. Basitçe, kodunuzu şöyle yazmak yerine:

public class Service
{
    public Service()
    {
    }

    public void DoSomething()
    {
        SqlConnection connection = new SqlConnection("some connection string");
        WindowsIdentity identity = WindowsIdentity.GetCurrent();
        // Do something with connection and identity variables
    }
}

Bu şekilde yazıyorsunuz:

public class Service
{
    public Service(IDbConnection connection, IIdentity identity)
    {
        this.Connection = connection;
        this.Identity = identity;
    }

    public void DoSomething()
    {
        // Do something with Connection and Identity properties
    }

    protected IDbConnection Connection { get; private set; }
    protected IIdentity Identity { get; private set; }
}

Yani, kodunuzu yazarken iki şey yaparsınız:

  1. Uygulamanın değiştirilmesi gerekebileceğini düşündüğünüzde sınıflar yerine arabirimlere güvenin;

  2. Bir sınıf içinde bu arayüzlerin örneklerini oluşturmak yerine, bunları yapıcı argümanları olarak geçirin (alternatif olarak, genel mülklere atanabilirler; birincisi yapıcı enjeksiyonudur , ikincisi özellik enjeksiyonudur ).

Bunların hiçbiri herhangi bir DI kütüphanesinin varlığını varsaymaz ve kodun bir tane olmadan yazılmasını gerçekten zorlaştırmaz.

Bunun bir örneğini arıyorsanız, .NET Framework'ün kendisinden başka bir yere bakmayın:

  • List<T>uygular IList<T>. Sınıfınızı IList<T>(veya IEnumerable<T>) kullanacak şekilde tasarlarsanız, tembel yükleme, Linq to SQL, Linq to Entities ve NHibernate gibi kavramların, genellikle özellik enjeksiyonu yoluyla, perde arkasında yaptığı gibi avantajlardan yararlanabilirsiniz. Bazı çerçeve sınıfları, aslında , birkaç veri bağlama özelliği için kullanılan IList<T>bir yapıcı argümanı olarak kabul eder BindingList<T>.

  • Linq to SQL ve EF tamamen IDbConnectionkamu kurucuları aracılığıyla iletilebilen ve ilgili arayüzler etrafında inşa edilmiştir. Yine de bunları kullanmanıza gerek yoktur; varsayılan kurucular bir yerde bir yapılandırma dosyasında oturan bir bağlantı dizesi ile iyi çalışır.

  • WinForms bileşenleri üzerinde çalışıyorsanız INameCreationServiceveya gibi "hizmetler" ile ilgilenirsiniz IExtenderProviderService. Hatta gerçekten somut sınıflar neyi bilmiyorum vardır . .NET aslında bunun için kullanılan kendi IoC kapsayıcısına sahiptir IContainerve Componentsınıfın GetServicegerçek servis bulucu olan bir yöntemi vardır. Elbette, hiçbir şey bu arayüzlerin herhangi birini veya tümünü IContainerbelirli bir konumlandırıcı olmadan veya kullanmadan engellemez . Hizmetlerin kendileri sadece konteynerle gevşek bir şekilde bağlıdır.

  • WCF'deki sözleşmeler tamamen arayüzler etrafında inşa edilmiştir. Gerçek somut hizmet sınıfına, genellikle DI olan bir yapılandırma dosyasındaki adla başvurulur. Birçok kişi bunu fark etmez, ancak bu yapılandırma sistemini başka bir IoC konteyneriyle değiştirmek tamamen mümkündür. Belki de daha ilginç bir şekilde, hizmet davranışlarının tümü IServiceBehaviordaha sonra eklenebilecek örneklerdir . Yine, bunu kolayca bir IoC kabına bağlayabilir ve ilgili davranışları seçmesini sağlayabilirsiniz, ancak özellik bir tane olmadan tamamen kullanılabilir.

Ve bu böyle devam eder. DI'yi .NET'te her yerde bulacaksınız, normalde öyle sorunsuz bir şekilde yapıldığından, DI olarak bile düşünmüyorsunuz.

DI-etkin kütüphanenizi maksimum kullanılabilirlik için tasarlamak istiyorsanız, en iyi öneri muhtemelen hafif bir kap kullanarak kendi varsayılan IoC uygulamanızı sağlamaktır. IContainerbunun için mükemmel bir seçimdir çünkü .NET Framework'ün bir parçasıdır.


2
Bir kap için gerçek soyutlama IContainer değil IServiceProvider'dır.
Mauricio Scheffer

2
@Mauricio: Elbette haklısın, ama LWC sistemini hiç kullanmayan birine neden IContainerbir paragrafta aslında kap olmadığını açıklamaya çalış . ;)
Aaronaught

Aaron, neden özel set kullandığınızı merak ediyor; alanları salt okunur olarak belirtmek yerine?
jr3

@Jreeter: Yani neden private readonlytarla değiller ? Yalnızca bildiren sınıf tarafından kullanılıyorsa, OP bunun alt sınıflandırma anlamına gelen çerçeve / kütüphane düzeyinde kod olduğunu belirtti. Bu gibi durumlarda genellikle alt sınıflara maruz kalan önemli bağımlılıklar istersiniz. Bir private readonlyalan ve açık getters / setters ile bir özellik yazmış olabilir , ama ... bir örnekte boşa harcanan ve gerçek bir yarar için pratikte korumak için daha fazla kod.
Aaronaught

Açıklama için teşekkür ederim! Ben salt okunur alanların çocuk tarafından erişilebilir olacağını varsaydım.
jr3

5

EDIT 2015 : zaman geçti, şimdi tüm bunların büyük bir hata olduğunu anladım. IoC kapları korkunçtur ve DI yan etkilerle başa çıkmak için çok zayıf bir yoldur. Etkili olarak, buradaki tüm cevaplardan (ve sorunun kendisinden) kaçınılmalıdır. Sadece yan etkilerin farkında olun, bunları saf koddan ayırın ve diğer her şey yerine oturur veya alakasız ve gereksiz karmaşıklıktır.

Orijinal cevap aşağıdaki gibidir:


SolrNet'i geliştirirken de aynı kararla yüzleşmek zorunda kaldım . DI dostu ve konteyner agnostik olma hedefiyle başladım, ancak daha fazla dahili bileşen ekledikçe, iç fabrikalar hızla yönetilemez hale geldi ve ortaya çıkan kütüphane esnek değildi.

Bir Windsor tesisi ve bir Ninject modülü sunarken kendi çok basit gömülü IoC konteyneri yazdım . Kütüphaneyi diğer kaplarla entegre etmek sadece bileşenleri düzgün bir şekilde bağlamakla ilgilidir, bu yüzden kolayca Autofac, Unity, StructureMap ile entegre edebilirim.

Bu dezavantajı ben sadece newhizmet kadar yeteneğini kaybetti . Ayrıca , gömülü kabın uygulanmasını kolaylaştırmak için kaçınabileceğim (gelecekte yeniden düzenleyebilirim) CommonServiceLocator'a bağımlılık yaptım.

Bu blog yazısında daha fazla ayrıntı .

MassTransit benzer bir şeye güveniyor gibi görünüyor. Gerçekten CommonServiceLocator'ın IServiceLocator olan bir IObjectBuilder arabirimine sahiptir, daha sonra bunu her kap için, yani NinjectObjectBuilder ve normal bir modül / tesis, yani MassTransitModule için uygular . Daha sonra ihtiyacı olan şeyi somutlaştırmak için IObjectBuilder'a güvenir . Bu elbette geçerli bir yaklaşım, ama şahsen çok sevmiyorum çünkü aslında konteynerden çok fazla geçiyor, bir servis bulucu olarak kullanıyor.

Monoray uygulayan kendi kabı hangi uygulayan, hem de eski iyi IServiceProvider . Bu kap, bu çerçevede bilinen hizmetleri sunan bir arabirim aracılığıyla kullanılır . Beton konteyneri almak için, yerleşik bir servis sağlayıcı bulucuya sahiptir . Windsor tesis seçilen servis sağlayıcı hale Windsor bu servis sağlayıcı bulma aracı işaret ediyor.

Alt satır: mükemmel bir çözüm yoktur. Herhangi bir tasarım kararında olduğu gibi, bu konu esneklik, sürdürülebilirlik ve rahatlık arasında bir denge gerektirir.


12
Fikrinize saygı duysam da, aşırı genellemenizi "buradaki diğer tüm cevapların modası geçmiş ve kaçınılması gerektiğini" son derece naif buluyorum. O zamandan beri bir şey öğrendiysek, bağımlılık enjeksiyonunun dil sınırlamalarına bir cevap olduğu söylenebilir. Mevcut dillerimizi geliştirmeden veya terk etmeden, mimarilerimizi düzleştirmek ve modülerliğe ulaşmak için DI'ye ihtiyacımız olacak gibi görünüyor . Genel bir kavram olarak IoC, bu hedeflerin sadece bir sonucudur ve kesinlikle arzu edilir. IoC kapları , IoC veya DI için kesinlikle gerekli değildir ve yararlılıkları, yaptığımız modül kompozisyonlarına bağlıdır.
tne

@tne "Son derece naif" olduğumdan bahsettiğiniz şey hoş karşılanamayan bir ad-hominem. C #, VB.NET, Java'da DI veya IoC olmadan karmaşık uygulamalar yazdım (görünüşe göre açıklamanıza göre görünüşte "IoC'ye ihtiyacınız olduğunu düşündüğünüz diller), kesinlikle dil sınırlamaları ile ilgili değil. Kavramsal sınırlamalar hakkında. Sizi ad-hominem saldırılarına ve zayıf argümanlara başvurmak yerine fonksiyonel programlama hakkında bilgi edinmeye davet ediyorum.
Mauricio Scheffer

18
Bahsettiğim ben senin bulundu beyanı naif olmak; bu kişisel olarak sizin hakkınızda hiçbir şey söylemez ve tamamen benim fikrimdir. Lütfen rastgele bir yabancı tarafından hakarete kapılmayın. Tüm ilgili fonksiyonel programlama yaklaşımlarından habersiz olduğumu ima etmek de yardımcı olmuyor; bunu bilmiyorsun ve yanılıyorsun. Bildiğim sen bunu can sıkıcı bir görünüşte bilgili adam "ihtiyacımız yok IoC" gibi şeyler söyleyebilirim bulundu, bu yüzden de (hızlı blogunuza baktım) orada bilgili. IoC , işlevsel programlamada tam anlamıyla her yerde gerçekleşir (..)
tne

4
ve genel olarak üst düzey yazılımlarda. Aslında, fonksiyonel diller (ve diğer birçok stil), IoC'yi DI olmadan çözdüklerini kanıtlıyor, bence ikimiz de buna katılıyoruz ve söylemek istediğim tek şey bu. Bahsettiğiniz dillerde DI olmadan başardıysanız, bunu nasıl yaptınız? Ben kullanmıyorum varsayıyorum ServiceLocator. İşlevsel teknikler uygularsanız, bağımlılık enjekte edilen sınıflar (bağımlılıkların kapalı değişkenler olduğu) olan kapanışların eşdeğeri ile sonuçlanırsınız. Başka nasıl? (Gerçekten merak ediyorum, çünkü
DI'yi

1
IoC kapları, parametrikliği bir akıl yürütme aracı olarak alan küresel değişebilir sözlüklerdir, bu nedenle kaçınılmalıdır. IoC (konsept) "bizi arama, sizi arayacağız" gibi bir işlevi değer olarak her geçtiğinizde teknik olarak uygulanır. DI yerine, alan adınızı ADT'lerle modelleyin ve ardından tercümanlar yazın (cf Free).
Mauricio Scheffer

1

Yapacağım kitaplığı konteynere bağımlılığı olabildiğince sınırlamak için DI kapının agnostik bir şekilde tasarlanması. Bu, gerekirse DI kabını başka bir tanesiyle değiştirmeye izin verir.

Ardından, DI mantığının üzerindeki katmanı, arayüzünüzde seçtiğiniz çerçeveyi kullanabilmeleri için kitaplığın kullanıcılarına gösterin. Bu şekilde, maruz kaldığınız DI işlevini kullanmaya devam edebilirler ve kendi amaçlarıyla başka bir çerçeveyi kullanmakta özgürdürler.

Kütüphane kullanıcılarının kendi DI çerçevelerini takmalarına izin vermek, bakım miktarını önemli ölçüde artırdığı için benim için biraz yanlış görünüyor. Bu aynı zamanda düz DI'den daha fazla bir eklenti ortamı haline gelir.

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.