İşlevsel Programlama bağımlılık enjeksiyon kalıplarına uygulanabilir bir alternatif midir?


21

Geçenlerde C # 'da İşlevsel Programlama başlıklı bir kitap okudum ve bana göre işlevsel programlamanın değişmez ve vatansız doğası bağımlılık enjeksiyon modellerine benzer sonuçlar elde ediyor ve muhtemelen özellikle birim test konusunda daha iyi bir yaklaşım.

Her iki yaklaşımla da deneyimi olan birisinin, asıl soruya cevap vermek için düşüncelerini ve deneyimlerini paylaşması halinde minnettar olurum: İşlevsel Programlama bağımlılık enjeksiyon modellerine uygulanabilir bir alternatif midir?


10
Bu bana pek bir şey ifade etmiyor, değişmezlik bağımlılıkları ortadan kaldırmıyor.
Telastyn

Bağımlılıkları ortadan kaldırmadığı konusunda hemfikirim. Muhtemelen yanlış olan benim anlayışımdır, ancak bu çıkarımı yaptım çünkü orijinal nesneyi değiştiremezsem, onu kullanan herhangi bir işleve iletmem (onu enjekte etmem) gerekir.
Matt Cashatt


5
Ayrıca, OO Programcılarının hem bir OO hem de FP perspektifinden gerçekten detaylı bir analizini yapan, İşlevsel Programlamaya Loving'e Nasıl İhanet Edileceğini Öğreniyoruz .
Robert Harvey,

1
Bu soru, bağlantı verdiği makaleler ve kabul edilen cevap da yararlı olabilir: stackoverflow.com/questions/11276319/… Korkutucu Monad kelimesini yoksay. Runar'ın cevabında işaret ettiği gibi, bu durumda karmaşık bir kavram değildir (sadece bir işlev).
15'de

Yanıtlar:


27

Bağımlılık yönetimi, aşağıdaki iki nedenden ötürü OOP'ta büyük bir sorundur:

  • Veri ve kodun sıkı eşleşmesi.
  • Ubiquitous yan etkilerin kullanılması.

Çoğu OO programcısı, veri ve kodun sıkı bir şekilde birleştirilmesinin tamamen faydalı olduğunu düşünür, ancak bunun bir bedeli vardır. Veri akışını katmanlar arasında yönetmek, herhangi bir paradigmada programlamanın kaçınılmaz bir parçasıdır. Verilerinizi ve kodunuzu birleştirmek, bir işlevi belirli bir noktada kullanmak istiyorsanız , nesnesini bu noktaya getirmenin bir yolunu bulmanız gerekecek ek sorunu da ekler .

Yan etkilerin kullanılması da benzer zorluklar yaratır. Bazı işlevler için bir yan etki kullanıyorsanız, ancak uygulanmasını değiştirebilmek istiyorsanız, bu bağımlılığı enjekte etmekten başka seçeneğiniz yok.

Örnek olarak, e-posta adresleri için web sayfalarını kazıyan ve ardından e-postayla gönderen bir spamcı program olarak düşünün. Eğer bir DI zihniyetiniz varsa, şu anda arayüzlerin arkasına yerleştireceğiniz hizmetleri ve hangi servislerin nereye enjekte edileceğini düşünüyorsunuz. Bu tasarımı okuyucu için bir alıştırma olarak bırakacağım. Bir FP zihniyetiniz varsa, şu anda en düşük fonksiyon katmanının giriş ve çıkışlarını düşünüyorsunuz:

  • Bir web sayfası adresi girin, o sayfanın metnini verin.
  • Bir sayfanın metnini girin, o sayfanın bağlantılarının bir listesini alın.
  • Bir sayfanın metnini girin, o sayfaya bir e-posta adresi listesi verin.
  • Bir e-posta adresi listesi girin, yinelenenleri kaldırılmış bir e-posta adresi listesi verin.
  • Bir e-posta adresi girin, bu adres için bir spam e-posta gönderin.
  • Bir spam e-posta girin, bu e-postayı göndermek için SMTP komutlarını verin.

Giriş ve çıkışlar açısından düşündüğünüz zaman, fonksiyon bağımlılığı yoktur, sadece veri bağımlılığı vardır. Onları ünite testi için bu kadar kolay kılan şey budur. Bir sonraki katmanınız, bir işlevin çıktısının diğerinin girişine beslenmesini düzenler ve gerektiğinde çeşitli uygulamaları kolayca değiştirebilir.

Çok gerçek anlamda, fonksiyonel programlama doğal olarak, fonksiyon bağımlılıklarınızı her zaman tersine çevirmenizi sağlar ve bu nedenle genellikle bundan sonra yapmak için özel bir önlem almak zorunda kalmazsınız. Bunu yaptığınızda, daha yüksek dereceli fonksiyonlar, kapaklar ve kısmi uygulama gibi araçlar daha az boylerle elde etmeyi kolaylaştırır.

Kendisinin sorunlu olan bağımlılıklar olmadığını unutmayın. Yanlış yöne işaret eden bağımlılıklar . Bir sonraki katman yukarı gibi bir işlevi olabilir:

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

Bu katmanın, bunun gibi zor kodlanmış bağımlılıklara sahip olması tamamen iyidir, çünkü tek amacı alt kat fonksiyonlarını birbirine yapıştırmaktır. Bir uygulamayı değiştirmek, farklı bir kompozisyon oluşturmak kadar kolaydır:

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

Bu kolay yeniden birleşme, yan etkilerin eksikliği ile mümkün olmaktadır. Alt katman fonksiyonları birbirinden tamamen bağımsızdır. Bir sonraki katman processText, bazı kullanıcı yapılandırmalarına göre hangisinin gerçekten kullanılacağını seçebilir :

actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText

Yine, bir sorun değil çünkü tüm bağımlılıklar bir yönden işaret ediyor. Hepsinin aynı şekilde işaret etmesini sağlamak için bazı bağımlılıkları tersine çevirmemize gerek yok, çünkü saf işlevler bizi zaten yapmaya zorladı.

configBunu en üstte kontrol etmek yerine en alt katmana geçerek daha çok birleştirebileceğinizi unutmayın . FP bunu yapmanıza engel olmaz, ancak denerseniz çok daha can sıkıcı hale gelme eğilimindedir.


3
“Yan etkilerin kullanılması benzer zorluklar yaratır. Bazı işlevler için bir yan etki kullanırsanız, ancak bunun yerine geçebilmek istiyorsanız, bu bağımlılığı enjekte etmekten başka seçeneğiniz yoktur.” Yan etkilerin bununla bir ilgisi olduğunu sanmıyorum. Haskell'deki uygulamaları değiştirmek isterseniz, yine de bağımlılık enjeksiyonunu yapmanız gerekir . Type sınıflarını temizlediğinizde, her işlev için ilk argüman olarak bir arabirimden geçiyorsunuz.
Doval

2
Meselenin asıl amacı, hemen hemen her dilin sizi diğer kod modüllerine zor kod referanslarına zorlamasıdır, bu nedenle uygulamaları değiştirmenin tek yolu, her yerde dinamik gönderimi kullanmak ve ardından çalışma zamanındaki bağımlılıklarınızı çözmekte sıkıştığınızdır. Bir modül sistemi bağımlılık grafiğini tip kontrol zamanında bildirmenize izin verir.
Doval

@ Doval - Düşünceli ve düşündürücü yorumlarınız için teşekkürler. Sizi yanlış anlamış olabilirim, ancak yorumlarınıza göre, bir DI stili üzerinde (geleneksel C # anlamında) işlevsel bir programlama tarzı kullanacak olsaydım, o zaman çalışma zamanı ile ilgili olası hata ayıklama sıkıntılarını önleyeceğimi söyleyerek doğru çıkarım. bağımlılıkların çözümü?
Matt Cashatt

@MatthewPatrickCashatt Bu bir stil veya paradigma meselesi değil, dil özellikleri ile ilgilidir. Dil, birinci sınıf şeyler olarak modülleri desteklemiyorsa, uygulamaları takas etmek için bir miktar dinamik gönderme ve bağımlılık enjeksiyonu yapmanız gerekecektir, çünkü bağımlılıkları statik olarak ifade etmenin bir yolu yoktur. Biraz farklı koymak gerekirse, C # programınız dizeleri kullanıyorsa, üzerinde kodlanmış bir bağımlılık vardır System.String. Bir modül System.String, değişken uygulaması ile değiştirmenize izin verir , bu sayede string uygulama seçimi sabit kodlanmış değildir, fakat derleme zamanında çözümlenir.
Doval

8

Programlama bağımlılık enjeksiyon kalıplarına uygulanabilir bir alternatif nedir?

Bu beni garip bir soru olarak vurguluyor. İşlevsel Programlama yaklaşımları, bağımlılık enjeksiyonuna büyük ölçüde teğetdir.

Tabii ki, değişmeyen duruma sahip olmak sizi yan etkiler yaparak ya da sınıf durumunu işlevler arasında gizli bir sözleşme olarak kullanarak "aldatmaya" zorlayabilir. Verilerin aktarılmasını daha açık hale getirir, sanırım bağımlılık enjeksiyonunun en temel şeklidir. Ve etrafta dolaşan fonksiyonların işlevsel programlama konsepti bunu çok daha kolaylaştırır.

Ancak bağımlılıkları ortadan kaldırmaz. Operasyonlarınız hala durumunuz değiştiğinde ihtiyaç duydukları tüm veriye / işlemlere ihtiyaç duyuyor. Ve hala o bağımlılıkları bir şekilde oraya götürmen gerekiyor. Bu yüzden işlevsel programlama yaklaşımlarının DI'nin yerini aldığını söyleyemem , bu yüzden başka bir alternatif yok.

Herhangi bir şey varsa, size OO kodunun programcıların nadiren düşündüğünden daha fazla bağımlılık yaratabildiğini gösterdiler.


Sohbete katkıda bulunduğunuz için tekrar teşekkürler, Telastyn. Sizin de belirttiğiniz gibi, sorum çok iyi inşa edilmemiş (sözlerim), ama buradaki geri bildirim sayesinde, beynimde bu konuda neyin kıvılcım çıkardığını biraz daha iyi anlamaya başladım: Hepimiz aynı fikirdeyiz. (Bence) birim test DI dışında bir kabus olabilir. Ne yazık ki DI'nin, özellikle IoC kaplarıyla kullanılması, çalışma zamanındaki bağımlılıkları çözdüğü için yeni bir hata ayıklama kabusu oluşturabilir. DI'ye benzer şekilde FP birim testini kolaylaştırır, ancak çalışma zamanına bağımlılık sorunları yoktur.
Matt Cashatt

(yukarıdan devam etti). . Bu zaten benim mevcut anlayışım. Lütfen işareti kaçırırsam bana bildirin. Burada devler arasında ölümcül bir ölüm olduğumu itiraf etmiyorum!
Matt Cashatt

@MatthewPatrickCashatt - DI mutlaka belirttiğiniz gibi korkunç olan çalışma zamanı bağımlılığı sorunlarını gerektirmez.
Telastyn

7

Sorunuza hızlı cevap: Hayır .

Ancak başkalarının iddia ettiği gibi, soru iki, biraz alakasız kavramları ile evlenir.

Bu adımı adım adım yapalım.

DI, işlevsel olmayan stille sonuçlanır

İşlev programlamasının temelinde saf işlevler vardır - girdiyi çıktıya eşleyen işlevler, böylece belirli bir girdi için her zaman aynı çıktıyı elde edersiniz.

DI tipik olarak , birimin artık saf olmadığı anlamına gelir çünkü çıktı enjeksiyona bağlı olarak değişebilir. Örneğin, aşağıdaki işlevde:

const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }

getBookedSeatCount(bir işlev) aynı giriş için farklı sonuçlar verecek şekilde değişebilir. Bu da bookSeatssaf değildir.

Bunun için istisnalar vardır - farklı algoritmalar kullanıyor olsa da, aynı giriş-çıkış eşlemesini uygulayan iki sıralama algoritmasından birini enjekte edebilirsiniz. Ancak bunlar istisnalar.

Bir sistem saf olamaz

Bir sistemin saf olamaması gerçeği, işlevsel programlama kaynaklarında iddia edildiği gibi aynı şekilde göz ardı edilir.

Bir sistemin açık örnekleri ile yan etkileri olması gerekir:

  • UI
  • Veritabanı
  • API (istemci-sunucu mimarisinde)

Bu yüzden sisteminizin bir kısmı yan etkiler içermeli ve bu kısım da zorunlu stil veya OO stilini içerebilir.

Kabuk-çekirdek paradigması

Terimleri Gary Bernhardt'ın sınırlar üzerine yaptığı mükemmel konuşmadan ödünç alarak , iyi bir sistem (veya modül) mimarisi bu iki katmanı içerecektir:

  • çekirdek
    • Saf fonksiyonlar
    • Dallanma
    • Bağımlılık yok
  • Kabuk
    • Impure (yan etkiler)
    • Dallanma yok
    • Bağımlılıklar
    • Zorunlu olabilir, OO stili vb. İçerir.

Anahtar paket, sistemi 'saf parçaya (çekirdek) ve saf parçaya (kabuk)' ayırmaktır.

Hafif kusurlu bir çözüm (ve sonuç) sunmasına rağmen, bu Mark Seemann'ın makalesi aynı konsepti önermektedir. Haskell uygulaması, hepsinin FP kullanarak yapılabileceğini gösterdiği gibi, özellikle anlayışlı.

DI ve FP

DI'nin kullanılması, uygulamanızın büyük bir kısmı saf olsa bile tamamen mantıklıdır. Anahtar DI'yi saf olmayan kabuk içinde sınırlamaktır.

Bir örnek API saplamaları olacaktır - üretimde gerçek API'yi istiyorsunuz, ancak testlerde saplamalar kullanın. Kabuk-çekirdek modeline bağlı kalmak burada çok yardımcı olacaktır.

Sonuç

Yani FP ve DI tam olarak alternatif değil. Hem sisteminizde olması muhtemeldir hem de tavsiye, sırasıyla FP ve DI'nin bulunduğu sistemin saf ve saf olmayan kısmı arasında ayrım yapılmasını sağlamaktır.


Kabuk-çekirdek paradigmasına atıfta bulunduğunuzda, kabukta hiç dallanma olmaz mı? Bir uygulamanın bir değere dayalı saf olmayan bir şey veya başka bir şey yapması gereken birçok örnek düşünebilirim. Bu dallanmayan kural Java gibi dillerde uygulanabilir mi?
jrahhali

@jrahhali Lütfen ayrıntılar için Gary Bernhardt'ın Konuşmasına bakın (cevabın linkli).
Izhaki

başka bir relavent
jk.

1

OOP bakış açısından, fonksiyonların tek yöntemli arayüzler olduğu düşünülebilir.

Arayüz bir fonksiyondan daha güçlü bir sözleşmedir.

İşlevsel bir yaklaşım kullanıyorsanız ve çok fazla DI işlemi yapıyorsanız, bir OOP yaklaşımını kullanmaya kıyasla, her bağımlılık için daha fazla aday alacaksınız.

void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.

vs

void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.

3
Herhangi bir sınıf, arayüzü uygulamak için sarılabilir, böylece "daha güçlü sözleşme" çok daha güçlü olmaz. Daha da önemlisi, her bir işleve farklı bir tür vermek, işlevsellik yapmayı sınırda imkansız kılar.
Doval

İşlevsel programlama "Yüksek dereceli fonksiyonlarla programlama" anlamına gelmez, çok daha geniş bir konsept anlamına gelir, yüksek dereceli fonksiyonlar paradigmada yararlı olan tek tekniktir.
Jimmy Hoffa,
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.