IoC için arabirimler yerine Func kullanma


15

Bağlam: C # kullanıyorum

Bir sınıf tasarladım ve onu izole etmek ve birim testini kolaylaştırmak için tüm bağımlılıklarından geçiyorum; dahili olarak nesne somutlaştırması yapmaz. Ancak, ihtiyaç duyduğu veriyi almak için arayüzlere başvurmak yerine, genel amaçlı Funcs'a ihtiyaç duyduğu veri / davranışı döndürerek başvurdum. Bağımlılıklarını enjekte ettiğimde bunu lambda ifadeleriyle yapabilirim.

Bana göre, bu daha iyi bir yaklaşım gibi görünüyor çünkü ünite testi sırasında sıkıcı bir alay yapmak zorunda değilim. Ayrıca, çevredeki uygulamanın temel değişiklikleri varsa, sadece fabrika sınıfını değiştirmem gerekecek; sınıfta mantığı içeren hiçbir değişikliğe gerek yoktur.

Bununla birlikte, IoC'nin daha önce bu şekilde yapıldığını hiç görmedim, bu da eksik olabileceğim bazı potansiyel tuzaklar olduğunu düşündürüyor. Düşünebildiğim tek şey, F # 'ı tanımlamayan önceki C # sürümüyle küçük uyumsuzluktur ve bu benim durumumda bir sorun değil.

Daha spesifik arabirimlerin aksine, genel temsilciler / IoC için Func gibi üst düzey işlevlerin kullanılmasıyla ilgili herhangi bir sorun var mı?


2
Ne tarif ettiğin olan yüksek mertebeden fonksiyonlar, önemli bir faset fonksiyonel programlama.
Robert Harvey

2
FuncParametreleri adlandırabileceğiniz için, bir amaç yerine bir temsilci kullanacağım .
Stefan Hanke

1
ilgili: stackoverflow: ioc-fabrika-artıları-ve-için-arayüz-karşı-delegeler . Stackoverflow soru, "fabrika" özel duruma daraltır ise soru daha geneldir @TheCatWhisperer
k3b

Yanıtlar:


11

Bir arabirim daha fazla değil, yalnızca bir işlev içeriyorsa ve iki ad (arabirim adı ve arabirim içindeki işlev adı) tanıtmak için zorlayıcı bir neden yoksa, Funcbunun yerine gereksiz kazan plakası kodunu önleyebilir ve çoğu durumda tercih edilir - tıpkı bir DTO tasarlamaya başladığınızda ve sadece bir üye özelliğine ihtiyaç duyduğunda olduğu gibi.

interfacesBağımlılık enjeksiyonu ve IoC'nin popüler hale geldiği Funcsırada, Java veya C ++ sınıfına gerçek bir eşdeğer olmadığı için birçok insanın kullanılmaya alışık olduğunu tahmin ediyorum ( FuncC # 'da o zaman mevcut olup olmadığından bile emin değilim) ). Bu nedenle, kullanımı daha zarif interfaceolsa bile , birçok öğretici, örnek veya ders kitabı formu tercih ediyor Func.

Sen içine görünebilir benim eski yanıt arayüzü ayrımı ilkesine ve Ralf Westphal en Akış Tasarım yaklaşımı hakkında. Bu paradigma, FuncDI'yi sadece kendiniz (ve biraz daha fazla) bahsettiğiniz aynı nedenden dolayı parametrelerle uygular . Gördüğünüz gibi, fikriniz gerçekten yeni bir fikir değil, tam tersi.

Ve evet, bu yaklaşımı tek başına, üretim kodu için, her adım için birim testleri de dahil olmak üzere birkaç ara adımla bir boru hattı şeklinde verileri işlemek gereken programlar için kullandım. Bu yüzden size çok iyi çalışabilmesi için ilk elden deneyim verebilirim.


Bu program, arayüz ayırma prensibi göz önünde bulundurularak bile tasarlanmamıştır, temelde sadece soyut sınıflar olarak kullanılır. Bu yaklaşıma gitmemin bir başka nedeni de, arayüzler iyi düşünülmüyor ve çoğunlukla tek bir sınıf onları uyguladığı için çoğunlukla işe yaramaz. Arayüz ayrımı prensibinin özellikle Func'a sahip olduğumuz için aşırıya kaçması gerekmiyor, ama bence hala çok önemli.
TheCatWhisperer

1
Bu program, arayüz ayırma prensibi göz önünde bulundurularak bile tasarlanmamıştır - açıklamanıza göre, muhtemelen terimin tasarım türünüze uygulanabileceğinin farkında değildiniz.
Doc Brown

Doc, kendimi seleflerim tarafından yapılan, yeniden düzenleme yaptığım ve yeni sınıfımın biraz bağımlı olduğu koda atıfta bulunuyordum. Bu yaklaşımla
gitmemizin bir nedeni

1
“... Bağımlılık enjeksiyonunun ve IoC'nin popüler hale geldiği sırada, Func sınıfına gerçek bir eşdeğer yoktu” Doc tam olarak neredeyse cevapladığım şeydi ama gerçekten yeterince güvende hissetmedim! Bu konuda şüphemizi doğruladığınız için teşekkürler.
Graham

9

IoC ile bulduğum ana avantajlardan biri, tüm bağımlılıklarımı arabirimlerini adlandırarak adlandırmamı sağlayacak ve kapsayıcı, tür adlarını eşleştirerek hangisine yapıcıya tedarik edeceğini bilecek. Bu uygundur ve bağımlılıklardan çok daha açıklayıcı adlara izin verir Func<string, string>.

Ayrıca genellikle tek, basit bir bağımlılıkla bile, bazen birden fazla işleve sahip olması gerektiğini buluyorum - bir arayüz, bu işlevleri kendi kendini belgeleyen bir şekilde gruplandırmanıza izin verir, vs hepsi gibi okunan birden fazla parametreye sahip olmak Func<string, string>, Func<string, int>.

Bir bağımlılık olarak aktarılan bir temsilci olmanın yararlı olduğu zamanlar kesinlikle vardır. Çok az üyeli bir arayüze sahip bir temsilci kullandığınızda yapılan bir karar çağrısıdır. Argümanın amacının ne olduğu gerçekten açık olmadıkça, genellikle kendi kendini belgeleyen kod üretme tarafında hata yapacağım; yani. bir arayüz yazma.


Orijinal uygulayıcı artık orada olmadığında birisinin kodunu ayıklamak zorunda kaldım ve size ilk deneyimlerden, hangi lambda'nın yığının çok fazla iş ve gerçekten sinir bozucu bir süreç olduğu yerde çalıştığını öğrenebilirim. Orijinal uygulayıcı arayüzler kullansaydı, aradığım hatayı bulmak bir esinti olurdu.
cwap

cwap, acını hissediyorum. Bununla birlikte, benim özel uygulamada, lambdaların hepsi tek bir işleve işaret eden tek bir astardır. Ayrıca hepsi tek bir yerde üretilir.
TheCatWhisperer

Benim tecrübelerime göre, bu sadece bir alet meselesi. ReSharpers Inspect-> Gelen Çağrılar, fonkları / lambdaları yukarı katlarken iyi bir iş çıkarır.
Christian

3

Daha spesifik arabirimlerin aksine, genel temsilciler / IoC için Func gibi üst düzey işlevlerin kullanılmasıyla ilgili herhangi bir sorun var mı?

Pek sayılmaz. Bir Func kendi tür arayüzdür (İngilizce anlamı, C # anlamı değil). "Bu parametre sorulduğunda X'i sağlayan bir şeydir." Func, bilgileri yalnızca gerektiği gibi tembel olarak sağlama avantajına da sahiptir. Bunu biraz yapıyorum ve ölçülü olarak tavsiye ederim.

Dezavantajları gelince:

  • IoC kapsayıcıları genellikle bağımlılıkları basamaklı bir şekilde bağlamak için biraz sihir yapar ve muhtemelen bazı şeyler olduğunda Tve bazı şeyler olduğunda iyi oynamazlar Func<T>.
  • Func'ların bazı dolaylamaları vardır, bu yüzden mantık yürütmek ve hata ayıklamak biraz daha zor olabilir.
  • Fonksiyonlar gecikmeyi başlatır, yani çalışma zamanı hataları test sırasında garip zamanlarda ortaya çıkabilir veya hiç görünmeyebilir. Ayrıca, işlem sırası sorunlarının olasılığını ve kullanımınıza bağlı olarak başlatma siparişindeki kilitlenmeleri artırabilir.
  • Func'a geçtiğiniz şey, hafif yükü ve beraberinde getirdiği komplikasyonlarla bir kapanış olacaktır.
  • Func'u çağırmak, nesneye doğrudan erişmekten biraz daha yavaştır. (Önemsiz bir programda fark edeceğiniz yeterli değil, ama orada)

1
Fabrikayı geleneksel bir şekilde oluşturmak için IoC Container'ı kullanıyorum, sonra fabrika arayüz yöntemlerini lambdaslara paketliyor ve ilk noktasında bahsettiğiniz sorunu çözüyor. Güzel nokta.
TheCatWhisperer

1

Basit bir örnek verelim - belki bir günlük kaydı enjekte ediyorsunuz.

Sınıf enjekte etme

class Worker: IWorker
{
    ILogger _logger;

    Worker(ILogger logger)
    {
        _logger = logger;
    }
    void SomeMethod()
    {
        _logger.Debug("This is a debug log statement.");
    }
}        

Bence neler olduğu çok açık. Dahası, bir IoC kabı kullanıyorsanız, açıkça bir şey enjekte etmeniz bile gerekmez, sadece kompozisyon kökünüze eklersiniz:

container.RegisterType<ILogger, ConcreteLogger>();
container.RegisterType<IWorker, Worker>();
....
var worker = container.Resolve<IWorker>();

Hata ayıklama sırasında Worker, bir geliştiricinin hangi beton sınıfının kullanıldığını belirlemek için kompozisyon köküne danışması yeterlidir.

Bir geliştiricinin daha karmaşık mantığa ihtiyacı varsa, çalışmak için tüm arayüze sahiptir:

    void SomeMethod()
    { 
       if (_logger.IsDebugEnabled) {
           _logger.Debug("This is a debug log statement.");
       }
    }

Bir yöntem enjekte etme

class Worker
{
    Action<string> _methodThatLogs;

    Worker(Action<string> methodThatLogs)
    {
        _methodThatLogs = methodThatLogs;
    }
    void SomeMethod()
    {
        _methodThatLogs("This is a logging statement");
    }
}        

İlk olarak, yapıcı parametresinin artık daha uzun bir adı olduğuna dikkat edin methodThatLogs. Bu gereklidir, çünkü bir Action<string>kişinin ne yapması gerektiğini söyleyemezsiniz . Arayüz ile tamamen açıktı, ancak burada parametre adlandırmaya dayanmak zorundayız. Bu, doğal olarak daha az güvenilir ve bir yapı sırasında uygulanması zor görünüyor.

Şimdi, bu yöntemi nasıl enjekte ederiz? IoC kapsayıcısı sizin için yapmaz. Böylece, somutlaştırdığınızda açıkça enjekte edersiniz Worker. Bu birkaç problem yaratır:

  1. Bir örnek oluşturmak daha fazla iştir Worker
  2. Hata ayıklamaya çalışan geliştiriciler Worker, somut örneğin ne denildiğini anlamanın daha zor olduğunu göreceklerdir. Sadece kompozisyon köküne danışamazlar; kod üzerinden izlemeleri gerekecek.

Daha karmaşık mantığa ihtiyacımız olursa ne olur? Tekniğiniz sadece bir yöntem ortaya koyar. Şimdi sanırım karmaşık şeyleri lambdaya fırlatabilirsin:

var worker = new Worker((s) => { if (log.IsDebugEnabled) log.Debug(s) } );

ancak birim testlerinizi yazarken, bu lambda ifadesini nasıl test edersiniz? Anonimdir, bu nedenle birim test çerçeveniz doğrudan başlatamaz. Belki de bunu yapmanın akıllıca bir yolunu bulabilirsiniz, ancak muhtemelen bir arayüz kullanmaktan daha büyük bir PITA olacaktır.

Farklılıkların özeti:

  1. Sadece bir yöntemin enjekte edilmesi, amacın çıkarımını zorlaştırırken, bir arayüz amacı açıkça iletir.
  2. Sadece bir yöntemin enjekte edilmesi, enjeksiyonu alan sınıfa daha az işlevsellik kazandırır. Bugün ihtiyacın olmasa bile, yarın ihtiyacın olabilir.
  3. Bir IoC kapsayıcısını kullanarak yalnızca bir yöntemi otomatik olarak enjekte edemezsiniz.
  4. Belirli bir durumda hangi kök sınıfının iş başında olduğunu kompozisyon kökünden söyleyemezsiniz.
  5. Lambda ifadesinin kendisini birim olarak test etmek bir sorundur.

Yukarıdakilerin hepsinde sorun yoksa, sadece yöntemi enjekte etmek sorun yaratmaz. Aksi takdirde geleneğe bağlı kalmanızı ve bir arayüz enjekte etmenizi öneririm.


Belirtmeliydim, yaptığım sınıf bir işlem mantığı sınıfı. Bilgilendirilmiş bir karar vermek için ihtiyaç duyduğu verileri elde etmek için dış sınıflara güveniyor, böyle bir kaydedicinin sınıfı indükleyen bir yan etki kullanılmıyor. Örneğinizle ilgili bir sorun, özellikle bir IoC konteyneri kullanma bağlamında kötü OO olmasıdır. Sınıfa ek karmaşıklık katan bir if ifadesi kullanmak yerine, basitçe eylemsiz bir günlükçüye geçirilmelidir. Ayrıca, lambdalara mantık eklemek gerçekten test etmeyi zorlaştıracaktır ... kullanım amacını yenmek, bu yüzden yapılmaz.
TheCatWhisperer

Lambdalar uygulamada bir arabirim yöntemine işaret eder ve birim testlerinde rastgele veri yapıları oluşturur.
TheCatWhisperer

İnsanlar neden her zaman açıkça keyfi bir örnek olan minutialara odaklanıyor? Eğer uygun günlük kaydı hakkında konuşmakta ısrar ederseniz, inert bir kaydedici bazı durumlarda korkunç bir fikir olabilir, örneğin günlüğe kaydedilecek dizenin hesaplanması pahalı olduğunda.
John Wu

Ne demek istediğini emin değilim "lambdas bir Arayüz yöntemi işaret." Delege imzasıyla eşleşen bir yöntemin uygulanması gerektiğini düşünüyorum . Yöntem bir arayüze ait olursa, bu tesadüfi; emin olmak için derleme veya çalışma zamanı denetimi yoktur. Yanlış anlıyor muyum? Belki yazınıza bir kod örneği ekleyebilirsiniz?
John Wu

Buradaki sonuçlarınıza katıldığımı söyleyemem. Birincisi, sınıfınızı minimum miktarda bağımlılığa bağlamanız gerekir, bu yüzden lambdas kullanmak, enjekte edilen her öğenin tam olarak bir bağımlılık olmasını sağlar ve bir arabirimdeki birden çok şeyin bir birleşimi değildir. Ayrıca Worker, her bağımlılığın karşılanmasına kadar her lambda'nın bağımsız olarak enjekte edilmesine izin vererek, bir seferde uygulanabilir bir bağımlılık oluşturma modelini de destekleyebilirsiniz . Bu, FP'deki kısmi uygulamaya benzer. Ayrıca, lambdaların kullanılması, bağımlılıkların örtük durumunun ve gizli bağlantıların ortadan kaldırılmasına yardımcı olur.
Aaron M. Eshbach

0

Uzun zaman önce yazdığım şu kodu düşünün:

public interface IPhysicalPathMapper
{
    /// <summary>
    /// Gets the physical path represented by the relative URL.
    /// </summary>
    /// <param name="relativeURL"></param>
    /// <returns></returns>
    String GetPhysicalPath(String relativeURL);
}

public class EmailBuilder : IEmailBuilder
{
    public IPhysicalPathMapper PhysicalPathMapper { get; set; }
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templateRelativeURL, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(this.PhysicalPathMapper.GetPhysicalPath(templateRelativeURL));

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

Bir şablon dosyasına göreli bir konum alır, belleğe yükler, ileti gövdesini oluşturur ve e-posta nesnesini birleştirir.

Sen de görünebilir IPhysicalPathMapperve düşünmek "Sadece bir işlevi yoktur. Bu bir olabilir Func." Ama aslında, buradaki sorun var IPhysicalPathMapperolmamalı. Çok daha iyi bir çözüm sadece yolu parametrelendirmektir :

public class EmailBuilder : IEmailBuilder
{
    public ITextFileLoader TextFileLoader { get; set; }
    public IEmailTemplateParser EmailTemplateParser { get; set; }
    public IEmaiBodyRenderer EmailBodyRenderer { get; set; }

    public String FromAddress { get; set; }

    public MailMessage BuildMailMessage(String templatePath, Object model, IEnumerable<String> toAddresses)
    {
        String templateText = this.TextFileLoader.LoadTextFromFile(templatePath);

        EmailTemplate template = this.EmailTemplateParser.Parse(templateText);

        MailMessage email = new MailMessage()
        {
            From = new MailAddress(this.FromAddress),
            Subject = template.Subject,
            IsBodyHtml = true,
            Body = this.EmailBodyRenderer.RenderBodyToHtml(template.BodyTemplate, model)
        };

        foreach (MailAddress recipient in toAddresses.Select<String, MailAddress>(toAddress => new MailAddress(toAddress)))
        {
            email.To.Add(recipient);
        }

        return email;
    }
}

Bu, bu kodu geliştirmek için bir dizi başka soruyu gündeme getirir. Örneğin, belki sadece bir kabul etmeli EmailTemplateve belki de önceden oluşturulmuş bir şablonu kabul etmeli ve belki de sadece astarlı olmalıdır.

Bu yüzden kontrolün yaygın bir desen olarak ters çevrilmesini sevmiyorum. Genellikle tüm kodlarınızı oluşturmak için bu tanrı benzeri bir çözüm olarak kabul edilir. Ancak gerçekte, bunu yaygın bir şekilde kullanırsanız (az da olsa), tamamen geriye doğru kullanılan çok sayıda gereksiz arabirimi tanıtmaya teşvik ederek kodunuzu daha da kötüleştirir . (Arayanın söz konusu bağımlılıkları değerlendirmekten ve sonucu çağrıyı çağıran sınıfın kendisinden geçirmekten gerçekten sorumlu olması gerektiği için geriye doğru.)

Arayüzler az miktarda kullanılmalı, kontrol ve bağımlılık enjeksiyonunun ters çevrilmesi de az miktarda kullanılmalıdır. Bunların büyük miktarları varsa, kodunuzun deşifre edilmesi çok daha zor hale 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.