Bu şekilde bu kodu yazıyorum test edilebilir, ancak eksik bir şey var mı?


13

Adında bir arayüz var IContext. Bunun amaçları için, aşağıdakiler dışında ne yaptığı gerçekten önemli değildir:

T GetService<T>();

Bu yöntemin yaptığı, uygulamanın geçerli DI kapsayıcısına bakmak ve bağımlılığı çözümlemeye çalışmaktır. Bence oldukça standart.

ASP.NET MVC uygulamamda kurucum şöyle görünüyor.

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetService<ISomeService>();
    AnotherService = ctx.GetService<IAnotherService>();
}

Yani her hizmet için yapıcı birden fazla parametre eklemek yerine (çünkü bu uygulamayı genişleten geliştiriciler için gerçekten can sıkıcı ve zaman alıcı olacaktır) hizmet almak için bu yöntemi kullanıyorum.

Şimdi yanlış geliyor . Ancak, şu anda kafamda haklı gösterme şeklim bu - bunu alay edebilirim .

Yapabilirim. IContextDenetleyiciyi test etmek için alay etmek zor olmaz . Yine de:

public class MyMockContext : IContext
{
    public T GetService<T>()
    {
        if (typeof(T) == typeof(ISomeService))
        {
            // return another mock, or concrete etc etc
        }

        // etc etc
    }
}

Ama dediğim gibi, yanlış geliyor. Herhangi bir düşünce / kötüye karşılama.


8
Buna Servis Bulucu denir ve bunu sevmiyorum. Konuyla ilgili çok sayıda yazı vardı - marş için martinfowler.com/articles/injection.html ve blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern'e bakın .
Benjamin Hodgson

Martin Fowler makalesinden: "Bu tür servis konum belirleyicilerinin kötü bir şey olduğu şikayetini sık sık duydum çünkü onlar test edilemezler çünkü onlar için uygulamaların yerini alamazsınız. Bu durumda servis bulucu örneği sadece basit bir veri tutucudur. Servislerimin test uygulamaları ile bulucuyu kolayca oluşturabilirim. " Neden sevmediğini açıklayabilir misin? Belki bir cevapta?
LiverpoolsNumber9

8
Haklı, bu kötü tasarım. Çok kolay: public SomeClass(Context c). Bu kod oldukça açık, değil mi? Bu, a that SomeClassbağlıdır Context. Hata, ama bekle, öyle değil! Sadece XBağlamdan aldığı bağımlılığa dayanır . Araçlarla Yani, her zaman bir değişiklik yapmak Contexto olabilir kırmak SomeObjectyalnızca değişse de Contexts Y. Ama evet, sen sadece değiştiğini biliyoruz Ydeğil Xbu yüzden, SomeClassgayet iyi. Ama iyi kod yazma ilgili değil sen bilir ama o senin kodun ilk kez baktığında neler yeni çalışan bilir.
valenterry

@DocBrown Bana tam olarak bunu söyledim - burada farkı görmüyorum. Lütfen daha fazla açıklayabilir misiniz?
valenterry

1
@DocBrown Şimdi anını görüyorum. Evet, eğer Bağlamı tüm bağımlılıkların bir parçasıysa, bu kötü bir tasarım değildir. Ancak kötü adlandırma olabilir, ancak bu sadece bir varsayımdır. OP, bağlamın daha fazla yöntemi (iç nesneler) olup olmadığını netleştirmelidir. Ayrıca, kod tartışmak iyidir, ama bu programmers.stackexchange, bu yüzden bana OP'yi iyileştirmek için şeylerin "arkasında" görmeye çalışmalıyız.
valenterry

Yanıtlar:


5

Yapıcıda birçok parametre yerine bir tane olması bu tasarımın sorunlu kısmı değildir . IContextSınıfınız bir hizmet cephesinden başka bir şey olmadığı sürece , özellikle tüm bağımlılıklarınızda kullanılan bağımlılıkları sağlamak için MyControllerBaseve kodunuzun tamamında kullanılan genel bir servis bulucu değil, kodunuzun bu kısmı IMHO ok.

İlk örneğiniz şu şekilde değiştirilebilir:

protected MyControllerBase(IContext ctx)
{
    TheContext = ctx;
    SomeService = ctx.GetSomeService();
    AnotherService = ctx.GetAnotherService();
}

önemli bir tasarım değişikliği olmazdı MyControllerBase. Bu tasarım iyi veya kötü ise, sadece

  • emin olmak TheContext, SomeServiceve AnotherServiceher zaman vardır tüm sahte nesnelerle başlatıldı veya hepsi gerçek nesnelerle
  • veya 3 nesnenin farklı kombinasyonlarıyla başlatılmasına izin vermek için (yani, bu durumda parametreleri tek tek geçirmeniz gerekir)

Yani kurucuda 3 yerine sadece bir parametre kullanmak tamamen makul olabilir.

Problemlidir şeydir IContextaçığa GetServicealenen yöntemi. IMHO bundan kaçınmalısınız, bunun yerine "fabrika yöntemlerini" açık tutun. Öyleyse bir servis bulucu kullanarak örneğimdeki GetSomeServiceve GetAnotherServiceyöntemlerini uygulamak uygun olur mu? Değişen IMHO. IContextSınıf, hizmet nesnelerinin açık bir listesini sağlamak için IMHO tarafından kabul edilebilir özel bir amaç için basit bir soyut fabrika oluşturmaya devam ettiği sürece . Soyut fabrikalar tipik olarak sadece "tutkal" kodudur ve kendileri de birim olarak test edilmeleri gerekmez. Bununla birlikte, kendinize GetSomeService, gibi yöntemler bağlamında, gerçekten bir servis bulucuya ihtiyacınız olup olmadığını veya açık bir kurucu çağrısının daha basit olup olmadığını sormalısınız .

Bu nedenle, IContextuygulamanın GetServicerasgele sınıflar tarafından keyfi bağımlılıkların çözülmesine izin veren genel, genel bir yöntem etrafında sadece bir sarıcı olduğu bir tasarıma bağlı kaldığınızda, her şey @BenjaminHodgson'un cevabında yazdıklarını uygular.


Buna katılıyorum. Örnekle ilgili sorun genel GetServiceyöntemdir. Açıkça adlandırılmış ve yazılan yöntemlere yeniden başvurmak daha iyidir. Daha da iyisi, IContextuygulamanın yapıcı bağımlılıklarını yapıcıda açıkça belirtmek olacaktır.
Benjamin Hodgson

1
@BenjaminHodgson: bazen daha iyi olabilir, ancak her zaman daha iyi değildir . Kod kokusu ctor parametre listesi uzadıkça not edilir. Eski cevabımı burada görebilirsiniz: programmers.stackexchange.com/questions/190120/…
Doc Brown

@DocBrown yapıcı aşırı enjeksiyon "kod kokusu" gerçek sorun olan SRP ihlali göstergesidir. Birkaç hizmeti sadece bir Cephe sınıfına sarmak, bunları yalnızca özellik olarak göstermek için , sorunun kaynağını ele almak için hiçbir şey yapmaz . Bu yüzden bir Cephe diğer bileşenlerin etrafında basit bir ambalaj olmamalı, ancak ideal olarak bunları tüketecek basitleştirilmiş bir API sunmalıdır (veya başka bir şekilde basitleştirmelidir) ...
AlexFoxGill

... Yapıcı aşırı enjeksiyonuna benzer bir "kod kokusunun" kendi başına bir sorun olmadığını unutmayın .
Kodda

Microsoft bunu pişirdiğinde neredeydin IValidatableObject?
RubberDuck

15

Bu tasarım Servis Bulucu * olarak bilinir ve hoşuma gitmez. Buna karşı birçok argüman var:

Servis Bulucu sizi konteynırınıza bağlar . Düzenli bağımlılık enjeksiyonu kullanarak (kurucu bağımlılıkları açıkça newbelirtir) kabınızı doğrudan farklı bir tanesiyle değiştirebilir veya- ifadelerine geri dönebilirsiniz . Seninle IContextbu mümkün değil.

Servis Bulucu bağımlılıkları gizler . Bir müşteri olarak, bir sınıf örneği oluşturmak için neye ihtiyacınız olduğunu söylemek çok zordur. Bir çeşit şeye ihtiyacınız var IContext, ancak aynı zamandaMyControllerBase işi yapmak için doğru nesneleri döndürmek için bağlamı ayarlamanız gerekiyor . Bu, kurucunun imzasından hiç belli değil. Düzenli DI ile derleyici tam olarak neye ihtiyacınız olduğunu söyler. Sınıfınızın çok fazla bağımlılığı varsa, o acıyı hissetmelisiniz çünkü sizi yeniden canlandırmaya teşvik edecektir. Servis Bulucu, kötü tasarımlardaki sorunları gizler.

Servis Bulucu çalışma zamanı hatalarına neden olur . GetServiceBozuk bir tür parametresiyle çağrı yaparsanız bir istisna alırsınız. Başka bir deyişle, GetServiceişleviniz tam bir işlev değildir. (Toplam işlevler FP dünyasından bir fikirdir, ancak temel olarak işlevlerin her zaman bir değer döndürmesi gerektiği anlamına gelir.) Derleyicinin yardımcı olması ve bağımlılıkları yanlış yaptığınızda size bildirmesi daha iyi.

Servis Bulucu, Liskov İkame İlkesini ihlal ediyor . Davranışları tür bağımsız değişkenine göre değiştiğinden, Servis Bulucu, arabirimde sonsuz sayıda yöntemi etkinmiş gibi görülebilir! Bu tartışma burada ayrıntılı olarak açıklanmıştır .

Servis Bulucu'nun test edilmesi zor . IContextTestler için sahte bir örnek verdiniz , bu iyi, ama kesinlikle bu kodu ilk başta yazmak zorunda kalmamanız daha iyi. Sahte bağımlılıklarınızı servis bulucuya gitmeden doğrudan enjekte edin.

Kısacası, sadece yapma . Çok fazla bağımlılığı olan sınıflar sorununa baştan çıkarıcı bir çözüm gibi görünüyor, ancak uzun vadede hayatınızı sefil hale getireceksiniz.

* Service Locator'ı, Resolve<T>keyfi bağımlılıkları çözebilen ve kod tabanı boyunca (yalnızca Composition Root'ta değil) kullanılan genel bir yöntemle bir nesne olarak tanımlıyorum . Bu, Servis Cephesi (bilinen bazı küçük bağımlılıklar kümesini bir araya getiren bir nesne) veya Soyut Fabrika (tek bir türden örnekler oluşturan bir nesne ile aynı değildir - bir Soyut Fabrikanın türü genel olabilir ancak yöntem değildir) .


1
Hizmet Bulucu kalıbından (kabul ediyorum) şikayet ediyorsunuz. Ama aslında, OP örneğinde, MyControllerBasene belirli bir DI konteynerine bağlı değildir ne de Servis Bulucu anti-paterni için bu "gerçekten" bir örnek değildir.
Doc Brown

@DocBrown katılıyorum. Hayatımı kolaylaştırdığı için değil, yukarıda verilen örneklerin çoğu kodumla ilgili olmadığı için.
LiverpoolsNumber9

2
Bana göre, Servis Bulucu anti-paterninin ayırt edici özelliği genel GetService<T>yöntemdir. Keyfi bağımlılıkları çözmek, OP örneğinde mevcut ve doğru olan gerçek kokudur.
Benjamin Hodgson

1
Servis Bulucu kullanmanın bir diğer sorunu esnekliği azaltmasıdır: her servis arabiriminin yalnızca bir uygulamasına sahip olabilirsiniz. Bir IFrobnicator'a dayanan iki sınıf oluşturursanız, ancak daha sonra birinin orijinal DefaultFrobnicator uygulamanızı kullanması gerektiğine karar verirseniz, ancak diğeri gerçekten onun etrafında bir CacheingFrobnicator dekoratörü kullanmalıdır. bağımlılıkları doğrudan enjekte etmek, yapmanız gereken tek şey kurulum kodunuzu (veya DI çerçevesi kullanıyorsanız yapılandırma dosyasını) değiştirmektir. Dolayısıyla bu bir OCP ihlalidir.
Jules

1
@DocBrown GetService<T>()Yöntem, rasgele sınıfların istenmesine izin verir: "Bu yöntemin yaptığı, uygulamanın geçerli DI kapsayıcısına bakmak ve bağımlılığı çözmeye çalışır. Oldukça standart sanırım." . Bu cevabın üst kısmındaki yorumunuza yanıt veriyordum. Bu% 100 Servis Bulucu
AlexFoxGill

5

Servis Bulucu anti-paternine karşı en iyi argümanlar Mark Seemann tarafından açıkça belirtilmiştir, bu yüzden bunun neden kötü bir fikir olduğuna çok fazla girmeyeceğim - bu, kendiniz için anlamak için zaman ayırmanız gereken bir öğrenme yolculuğudur (I ayrıca Mark'ın kitabını tavsiye ederiz ).

Tamam, soruyu cevaplamak için - asıl probleminizi yeniden belirtelim :

Yani her hizmet için yapıcı birden fazla parametre eklemek yerine (çünkü bu uygulamayı genişleten geliştiriciler için gerçekten can sıkıcı ve zaman alıcı olacaktır) hizmet almak için bu yöntemi kullanıyorum.

StackOverflow bu sorunu gideren bir soru var . Buradaki yorumlardan birinde belirtildiği gibi:

En iyi açıklama: "Yapıcı Enjeksiyonunun harika faydalarından biri, Tek Sorumluluk İlkesinin ihlallerini göze çarpan bir şekilde açık hale getirmesidir."

Sorununuzun çözümü için yanlış yere bakıyorsunuz. Bir sınıfın ne zaman çok şey yaptığını bilmek önemlidir. Sizin durumunuzda , "Temel Denetleyici" ye gerek olmadığından şüpheleniyorum . Aslında, OOP'de neredeyse her zaman mirasa gerek yoktur . Davranış ve paylaşılan işlevsellikteki değişimler, tamamen daha iyi faktörlü ve kapsüllenmiş kodla sonuçlanan ve bağımlılıkların üst sınıf kurucularına aktarılmasına gerek olmayan, uygun arabirim kullanımıyla elde edilebilir.

Bir Baz Kontrolörü olduğu yerde üzerinde çalışmış projelerin tümünde, bu gibi uygun özellikler ve yöntemler paylaşmak amaçlıdır yapıldığını IsUserLoggedIn()ve GetCurrentUserId(). DUR ! Bu, mirasın korkunç bir kötüye kullanımıdır. Bunun yerine, bu yöntemleri ortaya çıkaran bir bileşen oluşturun ve ihtiyacınız olan yerde ona bağımlı olun. Bu şekilde, bileşenleriniz test edilebilir olarak kalır ve bağımlılıkları açıktır.

Başka bir şey dışında, MVC desenini kullanırken her zaman sıska kontrolörleri tavsiye ederim . Burada daha fazla bilgi edinebilirsiniz, ancak modelin özü basittir, MVC'deki denetleyicilerin sadece bir şey yapması gerekir: MVC çerçevesi tarafından iletilen argümanları ele alın, diğer endişeleri başka bir bileşene devredin. Bu yine işyerinde Tek Sorumluluk İlkesidir.

Daha doğru bir karar vermek için kullanım durumunuzu bilmenize yardımcı olur, ancak dürüst olmak gerekirse, bir temel sınıfın iyi faktörlü bağımlılıklara tercih edildiği herhangi bir senaryo düşünemiyorum.


+1 - bu, diğer cevapların hiçbirinin gerçekten ele almadığı soruya yeni bir
Benjamin Hodgson

1
"Konstrüktör Enjeksiyonunun harika faydalarından biri, Tek Sorumluluk Prensibi'nin ihlallerini göze çarpan bir şekilde açık hale getirmesidir." Gerçekten iyi bir cevap. Hepsine katılmıyorum. Benim kullanım durumum, tuhaf bir şekilde, 100'den fazla denetleyiciye sahip bir sistemde kodu çoğaltmak zorunda kalmamaktır (en azından). Ancak yeniden SRP - her enjekte hizmet tek bir sorumluluğu vardır, hepsini kullanan denetleyici gibi - nasıl bir refrakter olurdu ??!
LiverpoolsNumber9

1
@ LiverpoolsNumber9 Anahtar, BaseController'da protectedyeni bileşenlere tek katmanlı temsilciler dışında hiçbir şey kalmayıncaya kadar işlevselliği BaseController'dan bağımlılıklara taşımaktır . Daha sonra BaseController'ı kaybedebilir ve bu protectedyöntemleri bağımlılıklara doğrudan çağrılarla değiştirebilirsiniz - bu, modelinizi basitleştirecek ve tüm bağımlılıklarınızı açık hale getirecektir (bu, 100'lü denetleyicilere sahip bir projede çok iyi bir şeydir!)
AlexFoxGill

@ LiverpoolsNumber9 - BaseController'ınızın bir kısmını bir çöp kutusuna kopyalayabilirseniz, bazı somut önerilerde bulunabilirim
AlexFoxGill

-2

Ben buna herkesin katkılarına dayanarak bir cevap ekliyorum. Herkese çok teşekkürler. Önce cevabım: "Hayır, bunda yanlış bir şey yok".

Doc Brown'ın "Service Facade" cevabı Bu cevabı kabul ettim çünkü aradığım şey (cevap "hayır" ise) bazı örnekler veya yaptığım şey üzerine biraz genişlemeydi. Bunu, A) bir adı olduğunu ve B) bunu yapmanın muhtemelen daha iyi yolları olduğunu öne sürerek sağladı.

Benjamin Hodgson'un "Servis Bulucu" yanıtı Burada edindiğim bilgiyi takdir ettiğim kadar, sahip olduğum şey bir "Servis Bulucu" değil. Bu bir "Hizmet Cephe" dir. Bu cevaptaki her şey doğrudur ama benim durumum için değil.

USR'nin cevapları

Bunu daha ayrıntılı olarak ele alacağım:

Bu şekilde çok fazla statik bilgi veriyorsunuz. Kararları çalışma zamanına birçok dinamik dil gibi ertelersiniz. Bu şekilde statik doğrulama (güvenlik), dokümantasyon ve takım desteğini (otomatik tamamlama, yeniden düzenleme, kullanım bulma, veri akışı) kaybedersiniz.

Hiçbir takım kaybetmiyorum ve "statik" yazmayı kaybetmiyorum. Hizmet cephesi DI kabımda yapılandırdıklarımı iade edecek veya default(T). Ve ne döndürdüğü "yazılır". Yansıma kapsül içine alınmıştır.

Yapıcı argümanları olarak ek hizmetler eklemenin neden büyük bir yük olduğunu anlamıyorum.

Kesinlikle "nadir" değil. Bir temel denetleyici kullandığım için , yapıcısını her değiştirmem gerektiğinde, 10, 100, 1000 diğer denetleyiciyi değiştirmem gerekebilir.

Bir bağımlılık enjeksiyon çerçevesi kullanırsanız, parametre değerlerini manuel olarak iletmeniz bile gerekmez. Sonra tekrar bazı statik avantajları kaybedersiniz, ancak çok fazla değil.

Bağımlılık enjeksiyonu kullanıyorum. Mesele bu.

Ve son olarak, Jules'un Benjamin'in cevabı hakkındaki yorumu herhangi bir esnekliği kaybetmiyorum. Bu benim servis cephem. GetService<T>DI kapsayıcısını yapılandırırken yaptığım gibi, farklı uygulamaları birbirinden ayırmak istediğim kadar parametre ekleyebilirim . Yani, örneğin, ben değiştirebilir GetService<T>()için GetService<T>(string extraInfo = null)bu "potansiyel problem" almak için.


Her neyse, herkese tekrar teşekkürler. Bu oldu gerçekten yararlı. Şerefe.


4
Burada bir Servis Cepheniz olduğunu kabul etmiyorum. GetService<T>()Yöntemi (denemesi için) çözmek yapabiliyor keyfi bağımlılıkları . Bu, cevabımın dipnotunda açıkladığım gibi, bir Servis Cephesi değil , bir Servis Bulucu yapar . @DocBrown'un önerdiği gibi küçük bir yöntem GetServiceX()/ GetServiceY()yöntemle değiştirseydiniz, bir Cephe olurdu.
Benjamin Hodgson

2
Bu makalenin , esasen ne yaptığınız olan Özet Hizmet Bulucu hakkındaki son bölümünü okumalı ve bunlara dikkat etmelisiniz . Bu anti-modelin tüm bir projeyi bozduğunu gördüm - alıntıya özellikle dikkat edin "bir bakım geliştiricisi olarak hayatınızı daha da kötüleştirecektir, çünkü yaptığınız her değişikliğin sonuçlarını kavramak için önemli miktarda beyin gücü kullanmanız gerekecektir. "
AlexFoxGill

3
Statik yazımda - noktayı kaçırıyorsunuz. İhtiyaç duyduğu bir yapıcı parametresi ile DI kullanarak bir sınıf sağlamayan kod yazmaya çalışırsam, derlemez. Bu, sorunlara karşı statik, derleme zamanı güvenliktir. Kodunuzu yazarsanız, kurucunuza gerçekten ihtiyaç duyduğu argümanı sağlamak için doğru şekilde yapılandırılmamış bir IContext sağlarsanız, çalışma zamanında
derlenir

3
@ LiverpoolsNumber9 Peki neden güçlü bir dilde yazılmış? Derleyici hataları hatalara karşı ilk savunma hattıdır. Birim testleri ikincisidir. Seninki de bir sınıfa ve onun bağımlılığına bağlı bir etkileşim olduğu için birim testleri ile alınmazdı, bu yüzden üçüncü savunma hattına gireceksin: entegrasyon testleri. Bunları ne sıklıkta çalıştırdığınızı bilmiyorum, ancak şimdi bir derleyici hatasının altını çizmek için IDE'nizin milisaniye yerine, dakika veya daha fazla sırada geri bildirimden bahsediyorsunuz.
Ben Aaronson

2
@ LiverpoolsNumber9 yanlış: testleriniz artık bir tane doldurmalı IContextve enjekte
etmeli
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.