Genel API tasarımıyla bağımlılık enjeksiyonunu dengeleme


13

Basit sabit genel API sağlayarak bağımlılık enjeksiyonunu kullanarak test edilebilir tasarımı nasıl dengeleyeceğimizi düşünüyorum. Benim açmazım: insanlar böyle bir şey yapmak isteyeceklerdi var server = new Server(){ ... }ve a'nın sahip olabileceği birçok bağımlılık ve bağımlılık grafiği oluşturma konusunda endişelenmek zorunda değiller Server(,,,,,,). Gelişirken, tüm bunları ele almak için bir IoC / DI çerçevesi kullandığım için çok fazla endişelenmiyorum (herhangi bir kabın yaşam döngüsü yönetimi yönlerini kullanmıyorum, bu da işleri daha da karmaşıklaştıracak).

Şimdi, bağımlılıkların yeniden uygulanması pek olası değildir. Bu durumda bileşenleme, uzatma için dikişler oluşturmaktan ziyade neredeyse sadece test edilebilirlik (ve iyi tasarım!) İçindir. İnsanlar zamanın% 99.999'u varsayılan bir yapılandırma kullanmak isteyecektir. Yani. Bağımlılıkları zor kodlayabilirim. Bunu yapmak istemiyorum, testimizi kaybediyoruz! Ben sabit kodlu bağımlılıkları ve bağımlılıkları alır bir varsayılan bir kurucu sağlayabilir. Bu ... dağınık ve kafa karıştırıcı olması muhtemel, ancak uygulanabilir. Bağımlılık alma kurucu iç yapabilir ve benim birim testleri bir arkadaş montaj (C # varsayarak), genel API düzenler ama bakım için gizlenen bir gizli tuzak bırakıyor yapabilirsiniz. Kitabımda açıkça değil, açıkça bağlı iki kurucuya sahip olmak kötü bir tasarım olurdu.

Şu anda aklıma gelen en az kötülük hakkında. Görüşler? Bilgelik?

Yanıtlar:


11

Bağımlılık enjeksiyonu iyi kullanıldığında güçlü bir kalıptır, ancak uygulayıcıları çoğu zaman bir dış çerçeveye bağımlı hale gelir. Ortaya çıkan API, uygulamamızı XML koli bandı ile birlikte gevşek bir şekilde bağlamak istemeyenler için oldukça acı vericidir. Düz Eski Nesneleri (POO) unutmayın.

Öncelikle, herhangi bir çerçeve dahil olmadan birisinin API'nizi nasıl kullanmasını beklediğinizi düşünün.

  • Sunucuyu nasıl başlatırsınız?
  • Sunucuyu nasıl genişletirsiniz?
  • Harici API'de neyi ortaya çıkarmak istiyorsunuz?
  • Harici API'de neyi gizlemek istiyorsunuz ?

Bileşenleriniz için varsayılan uygulamalar sağlama fikrini ve bu bileşenlere sahip olduğunuzu seviyorum. Aşağıdaki yaklaşım son derece geçerli ve iyi bir OO uygulamasıdır:

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

API belgelerinde bileşenler için varsayılan uygulamaların ne olduğunu belgelediğiniz ve tüm kurulumu gerçekleştiren bir kurucu sağladığınız sürece, iyi olmalısınız. Basamaklı kurucular, kurulum kodu bir ana kurucuda gerçekleştiği sürece kırılgan değildir.

Esasen, herkese açık tüm kurucuları ortaya çıkarmak istediğiniz farklı bileşen kombinasyonlarına sahip olacaklardı. Dahili olarak, varsayılanları doldurur ve kurulumu tamamlayan özel bir kurucuya erteler. Temel olarak, bu size bazı KURU sağlar ve test edilmesi oldukça kolaydır.

friendSunucu nesnesini sınama amacıyla yalıtmak üzere ayarlamak için sınamalarınıza erişim sağlamanız gerekiyorsa, sınayın. Dikkatli olmasını istediğiniz genel API'nın bir parçası olmayacak.

Sadece API tüketicilere tür ve yok gerektiren kütüphanenizi kullanmak için bir IoC / DI çerçeve. Verdiğiniz acıyı hissetmek için birim testlerinizin IoC / DI çerçevesine de güvenmediğinden emin olun. (Bu kişisel bir evcil hayvan huşu, ama çerçeveyi tanıtır getirmez artık birim testi değil - entegrasyon testi haline gelir).


Kabul ediyorum ve kesinlikle IoC yapılandırma çorbasının hayranı değilim - XML ​​yapılandırması, IoC'nin söz konusu olduğu yerlerde kullandığım bir şey değil. Kesinlikle IoC kullanımına ihtiyaç duyduğum şey tam olarak kaçındığım şeydi - bir kamu API tasarımcısının asla yapamayacağı varsayım. Yorum için teşekkürler, düşündüğüm çizgiler boyunca ve tekrar tekrar bir akıl sağlığı kontrol için güven verici!
kolektiv

-1 Bu cevap temel olarak "bağımlılık enjeksiyonu kullanma" diyor ve yanlış varsayımlar içeriyor. Örneğin, HttpListeneryapıcı değişirse Serversınıfın da değişmesi gerekir. Onlar birleşti. Daha iyi bir çözüm, @Winston Ewert'in aşağıda söylediği şeydir; bu, kütüphanenin API'sının dışında önceden belirlenmiş bağımlılıklar içeren varsayılan bir sınıf sağlamaktır. O zaman programcı, onun için karar verdiğinizden beri tüm bağımlılıkları kurmak zorunda değildir, ancak daha sonra bunları değiştirme esnekliğine sahiptir. Üçüncü taraf bağımlılıklarınız olduğunda bu çok önemlidir.
M. Dudley


1
@ MichaelDudley, OP'nin sorusunu okudun mu? Tüm "varsayımlar" OP'nin sağladığı bilgilere dayanmaktadır. Bir süreliğine, sizin dediğiniz gibi "piç enjeksiyon", özellikle bağımlılıkları çalışma zamanı sırasında değişmeyen sistemler için tercih edilen bağımlılık enjeksiyon biçimiydi. Seterler ve alıcılar da bir başka bağımlılık enjeksiyonudur. OP'nin sağladığı bilgilere dayanarak örnekler kullandım.
Berin Loritsch

4

Bu gibi durumlarda Java, gerçek "düzgün" davranış sunucusundan bağımsız tutulan bir fabrika tarafından oluşturulan "varsayılan" bir yapılandırmaya sahip olmak normaldir. Böylece, kullanıcınız şöyle yazar:

var server = DefaultServer.create();

ise Serverbireyin yapıcı hala tüm bağımlılıklarından kabul eder ve derin özelleştirme için kullanılabilir.


+1, SRP. Bir dişçi muayenehanesinin sorumluluğu bir dişçi muayenehanesi kurmak değildir. Bir sunucunun sorumluluğu bir sunucu oluşturmak değildir.
R. Schmitz

2

"Genel API" sağlayan bir alt sınıf sağlamaya ne dersiniz?

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

Kullanıcı new StandardServer()yolda olabilir ve yolda olabilir. Sunucunun nasıl çalıştığı üzerinde daha fazla kontrol sahibi olmak isterse Sunucu taban sınıfını da kullanabilirler. Bu az çok kullandığım yaklaşım. (Konuyu henüz görmediğim için bir çerçeve kullanmıyorum.)

Bu hala dahili API'yi açığa çıkarıyor ama bence yapmalısın. Nesnelerinizi bağımsız olarak çalışması gereken farklı faydalı bileşenlere ayırdınız. Üçüncü bir tarafın bileşenlerden birini ne zaman ayrı olarak kullanmak isteyebileceği konusunda hiçbir bilgi yoktur.


1

Bağımlılık alma kurucu iç yapabilir ve benim birim testleri bir arkadaş montaj (C # varsayarak), genel API düzenler ama bakım için gizlenen bir gizli tuzak bırakıyor yapabilirsiniz.

Bu bana "kötü bir gizli tuzak" gibi gelmiyor. Genel kurucu sadece "varsayılan" bağımlılıkları olan dahili olanı çağırıyor olmalıdır. Kamu kurucusunun değiştirilmemesi gerektiği açık olduğu sürece, her şey yolunda olmalıdır.


0

Fikrinize tamamen katılıyorum. Yalnızca birim testi amacıyla bileşen kullanım sınırlarını kirletmek istemiyoruz. Bu DI tabanlı test çözümünün tüm problemidir.

InjectableFactory / InjectableInstance modelini kullanmanızı öneririm . InjectableInstance , değiştirilebilir değer sahibi olan basit bir yeniden kullanılabilir yardımcı program sınıflarıdır . Bileşen arabirimi, varsayılan uygulama ile başlatılan bu değer sahibine bir başvuru içerir. Arabirim , değer sahibine delege olan bir singleton yöntemi get () sağlayacaktır. Bileşen istemcisi, doğrudan bağımlılığı önlemek için uygulama örneği almak üzere new yerine get () yöntemini çağırır . Test süresi boyunca, isteğe bağlı olarak varsayılan uygulamayı bir sahte ile değiştirebiliriz. Aşağıda bir örnek TimeProvider kullanacağımDate () ' in bir soyutlaması olan sınıf, böylece birim testinin farklı anları simüle etmek için enjekte etmesini ve alay etmesini sağlar. Maalesef, java'yı daha aşina olduğum için burada java kullanacağım. C # benzer olmalıdır.

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

InjectableInstance uygulaması oldukça kolaydır , java bir referans uygulaması var . Ayrıntılar için lütfen blog yazım bölümüne bakın Enjekte Edilebilir Fabrika ile Bağımlılık Dolayı


Yani ... bağımlılık enjeksiyonunu değiştirilebilir bir singleton ile mi değiştiriyorsunuz? Kulağa korkunç geliyor, bağımsız testler nasıl yapılır? Her test için yeni bir süreç mi açıyorsunuz yoksa belirli bir diziye mi zorluyorsunuz? Java benim güçlü kıyafeti değil, bir şeyleri yanlış mı anladım?
nvoigt

Java maven projesinde, junit motoru varsayılan olarak farklı sınıf yükleyicide her testi çalıştıracağı için paylaşılan örnek bir sorun değildir, ancak aynı sınıf yükleyiciyi yeniden kullanabileceği için bazı IDE'de bu sorunla karşılaşıyorum. Bu sorunu çözmek için sınıf, her testten sonra orijinal durumuna sıfırlamak üzere çağrılacak bir sıfırlama yöntemi sağlar. Uygulamada, farklı testlerin farklı alay kullanmak istediği nadir bir durumdur. Bu modeli yıllardır büyük ölçekli projeler için uyguladık, Her şey sorunsuz çalışıyor, taşınabilirlik ve test edilebilirlikten ödün vermeden okunabilir, temiz koddan keyif aldık.
Jianwu Chen
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.