Kalıcılık tamamen işlevsel bir dile nasıl uyuyor?


18

Kalıcılıkla başa çıkmak için komut işleyicilerini kullanma şekli, IO ile ilgili kodu olabildiğince inceltmek istediğimiz tamamen işlevsel bir dile nasıl sığar?


Etki Alanına Dayalı Tasarım'ı nesne yönelimli bir dilde uygularken, durum değişikliklerini yürütmek için Komut / İşleyici desenini kullanmak yaygındır . Bu tasarımda, komut işleyicileri etki alanı nesnelerinizin üstünde bulunur ve depoları kullanmak ve etki alanı olaylarını yayınlamak gibi sıkıcı kalıcılıkla ilgili mantıktan sorumludur. İşleyiciler, alan adı modelinizin genel yüzüdür; kullanıcı arayüzü gibi uygulama kodu, etki alanı nesnelerinin durumunu değiştirmesi gerektiğinde işleyicileri çağırır.

C # 'da bir eskiz:

public class DiscardDraftDocumentCommandHandler : CommandHandler<DiscardDraftDocument>
{
    IDraftDocumentRepository _repo;
    IEventPublisher _publisher;

    public DiscardDraftCommandHandler(IDraftDocumentRepository repo, IEventPublisher publisher)
    {
        _repo = repo;
        _publisher = publisher;
    }

    public override void Handle(DiscardDraftDocument command)
    {
        var document = _repo.Get(command.DocumentId);
        document.Discard(command.UserId);
        _publisher.Publish(document.NewEvents);
    }
}

documentEtki alanı nesnesi (( "Zaten atılır oldu bir belgeyi iptal edemez" veya "kullanıcı belgeyi atmak iznine sahip olmalıdır" gibi) iş kurallarını uygulamaktan ve yayınladığımız gereken alan olayları oluşturmaktan sorumlu document.NewEventsolur IEnumerable<Event>bir DocumentDiscardedetkinlik olabilir ve muhtemelen bir etkinlik içerir ).

Bu güzel bir tasarımdır - genişletmek kolaydır (etki alanı modelinizi değiştirmeden yeni komut işleyicileri ekleyerek yeni kullanım örnekleri ekleyebilirsiniz) ve nesnelerin nasıl kalıcı olduğuna dair agnostiktir (bir Moğol için NHibernate deposunu kolayca değiştirebilirsiniz) sahte veya alay kullanarak test etmeyi kolaylaştıran bir EventStore yayıncısı için bir RabbitMQ yayıncısını değiştirin). Ayrıca model / görünüm ayrımına da uyar - komut işleyicinin toplu iş, GUI veya REST API tarafından kullanılıp kullanılmadığı hakkında hiçbir fikri yoktur.


Haskell gibi tamamen işlevsel bir dilde, komut işleyicisini kabaca şu şekilde modelleyebilirsiniz:

newtype CommandHandler = CommandHandler {handleCommand :: Command -> IO Result)
data Result a = Success a | Failure Reason
type Reason = String

discardDraftDocumentCommandHandler = CommandHandler handle
    where handle (DiscardDraftDocument documentID userID) = do
              document <- loadDocument documentID
              let result = discard document userID :: Result [Event]
              case result of
                   Success events -> publishEvents events >> return result
                   -- in an event-sourced model, there's no extra step to save the document
                   Failure _ -> return result
          handle _ = return $ Failure "I expected a DiscardDraftDocument command"

İşte anlamaya çalıştığım kısım. Genellikle, GUI veya REST API gibi komut işleyicisini çağıran bir çeşit 'sunum' kodu olacaktır. Şimdi programımızda, Haskell'de büyük bir hayır olan IO - komut işleyicisi ve görünüm - yapması gereken iki katman var.

Yapabildiğim kadarıyla, burada iki karşıt güç var: biri model / görüş ayrımı ve diğeri de modeli devam ettirme ihtiyacı. Modeli bir yerde devam ettirmek için IO kodu olması gerekiyor , ancak model / görünüm ayrımı, onu diğer tüm IO koduyla birlikte sunum katmanına koyamayacağımızı söylüyor.

Tabii ki, "normal" bir dilde, IO her yerde olabilir (ve olur). İyi tasarım, farklı IO tiplerinin ayrı tutulmasını gerektirir, ancak derleyici bunu zorlamaz.

Öyleyse: modelin kalıcı olması gerektiğinde, IO kodunu programın en uç noktasına itme arzusu ile model / görünüm ayrımını nasıl uzlaştırırız? İki farklı G / Ç türünü nasıl ayrı tutarız , ancak yine de tüm saf kodlardan nasıl uzak tutarız?


Güncelleme : Ödülün süresi 24 saatten az sürer. Şu anki cevapların hiçbirinin sorumu ele aldığını hissetmiyorum. @ Ptharien's Flame'ın hakkındaki yorumu acid-stateumut verici görünüyor, ancak bu bir cevap değil ve ayrıntılı olarak eksik. Bu noktaların boşa gitmesinden nefret ederim!


1
Belki de Haskell'deki çeşitli kalıcılık kütüphanelerinin tasarımına bakmak faydalı olabilir; özellikle, acid-statetanımladığınız şeye yakın görünüyor .
Ptharien'in Alevi

1
acid-stateharika görünüyor, bu bağlantı için teşekkürler. API tasarımı açısından hala buna bağlı gibi görünmektedir IO; sorum bir kalıcılık çerçevesinin daha büyük bir mimariye nasıl uyduğu ile ilgili. acid-stateBir sunum katmanının yanında kullanılan ve bu ikisini ayrı tutmayı başaran açık kaynaklı uygulamalar biliyor musunuz ?
Benjamin Hodgson

QueryVe Updatemonads oldukça uzak kaldırılır IOaslında. Bir cevapta basit bir örnek vermeye çalışacağım.
Ptharien'in Alevi

Konu dışı olma riskiyle Komut / İşleyici modelini bu şekilde kullanan okuyucular için Akka.NET'e göz atmanızı tavsiye ederim. Oyuncu modeli burada iyi bir uyum gibi hissediyor. Pluralsight'ta harika bir rota var. (Yemin ederim sadece bir hayranım, bir tanıtım botu değilim.)
RJB

Yanıtlar:


6

Haskell'deki bileşenleri ayırmanın genel yolu, monad transformatör yığınlarıdır. Bunu aşağıda daha ayrıntılı olarak açıklarım.

Birkaç büyük ölçekli bileşeni olan bir sistem oluşturduğumuzu düşünün:

  • disk veya veritabanı ile konuşan bir bileşen (alt model)
  • alanımızda dönüşümler yapan bir bileşen (model)
  • kullanıcıyla etkileşime giren bir bileşen (görünüm)
  • görünüm, model ve alt model ( bağlantı) arasındaki bağlantıyı tanımlayan bir bileşen
  • tüm sistemi başlatan bir bileşen (sürücü)

İyi kod stilini korumak için bu bileşenleri gevşek bir şekilde bağlı tutmamız gerektiğine karar veriyoruz.

Bu nedenle, bize yol gösterecek çeşitli MTL sınıflarını kullanarak bileşenlerin her birini polimorf olarak kodlarız:

  • alt modeldeki her fonksiyon tiptedir MonadState DataState m => Foo -> Bar -> ... -> m Baz
    • DataState veritabanımızın veya depolama alanımızın anlık görüntüsünün saf bir temsilidir
  • modeldeki her fonksiyon saf
  • görünümdeki her işlev türdedir MonadState UIState m => Foo -> Bar -> ... -> m Baz
    • UIState kullanıcı arayüzümüzün durumunun anlık bir gösterimidir
  • denetleyicideki her işlev tiptedir MonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
    • Denetleyicinin hem görünüm durumuna hem de alt modelin durumuna erişebildiğine dikkat edin
  • sürücünün sadece bir tanımı vardır main :: IO (), bu da diğer bileşenleri tek bir sistemde birleştirmek gibi önemsiz bir iş yapar
    • görünüm ve alt modelin denetleyiciyi zoomveya benzer bir birleştiriciyi kullanarak aynı durum türüne kaldırılması gerekir
    • model saf ve böylece kısıtlama olmadan kullanılabilir
    • Sonunda, her şey (uyumlu bir tür) içinde yaşar StateT (DataState, UIState) IO, bu da daha sonra üretilecek veritabanının veya depolamanın gerçek içeriği ile çalıştırılır IO.

1
Bu mükemmel tavsiye ve tam olarak ne aradığını. Teşekkürler!
Benjamin Hodgson

2
Bu cevabı sindiriyorum. Lütfen bu modeldeki 'alt modelin' rolünü açıklığa kavuşturabilir misiniz? GÇ gerçekleştirmeden "disk veya veritabanı ile nasıl konuşur"? Özellikle " DataStatebizim veritabanı veya depolama durumunun bir anlık görüntüsünün saf bir temsili " ile ne demek konusunda kafam karıştı . Muhtemelen tüm veritabanını belleğe yüklemek istemezsiniz!
Benjamin Hodgson

1
Kesinlikle bu mantığın C # uygulaması hakkındaki düşüncelerinizi görmek isterim. Sana rüşvet verebileceğimi düşünmüyor musun? ;-)
RJB

1
@RJB Maalesef, dilde daha yüksek türlere izin vermek için C # geliştirme ekibine rüşvet vermelisiniz, çünkü onlar olmadan bu mimari biraz düz düşer.
Ptharien's Flame

4

Öyleyse: modelin kalıcı olması gerektiğinde, IO kodunu programın en uç noktasına itme arzusu ile model / görünüm ayrımını nasıl uzlaştırırız?

Modelin kalıcı olması gerekir mi? Birçok programda, durumun öngörülemez olması nedeniyle modelin kaydedilmesi gerekir, herhangi bir işlem modeli herhangi bir şekilde değiştirebilir, bu nedenle modelin durumunu bilmenin tek yolu doğrudan erişmektir.

Senaryonuzda, olaylar dizisi (onaylanmış ve kabul edilmiş komutlar) her zaman durumu oluşturabilirse, durumun değil, kalıcı olması gereken olaylardır. Durum her zaman olaylar tekrarlanarak oluşturulabilir.

Bununla birlikte, çoğu zaman durum depolanır, ancak komutların tekrar edilmesini önlemek için bir anlık görüntü / önbellek olarak, temel program verileri olarak değil.

Şimdi programımızda, Haskell'de büyük bir hayır olan IO - komut işleyicisi ve görünüm - yapması gereken iki katman var.

Komut kabul edildikten sonra, olay iki hedefe (olay depolama ve raporlama sistemi) ancak programın aynı katmanına iletilir.

Ayrıca Bkz.
Olay Sağlama
İstekli Okuma Türevlemesi


2
Olay kaynağı oluşturma konusuna aşinayım (yukarıdaki örneğimde kullanıyorum!) Ve saçları bölmekten kaçınmak için yine de olay kaynaklarının kalıcılık sorununa bir yaklaşım olduğunu söyleyebilirim. Her durumda, olay kaynağı, etki alanı nesnelerinizi komut işleyicisine yükleme gereğini ortadan kaldırmaz . Komut işleyici, nesnelerin bir olay akışından mı, bir ORM'den mi, yoksa saklı bir yordamdan mı geldiğini bilmez - yalnızca depodan alır.
Benjamin Hodgson

1
Anlayışınız, birden çok IO oluşturmak için görünümü ve komut işleyiciyi birleştiriyor gibi görünüyor. Benim anlayışım, işleyicinin olayı oluşturması ve başka bir ilgisi olmamasıdır. Bu örnekte görünüm ayrı bir modül olarak işlev görür (teknik olarak aynı uygulamada olsa bile) ve komut işleyicisine bağlanmaz.
FMJaguar

1
Sanırım çapraz amaçlarla konuşuyor olabiliriz. 'Görünüm' dediğimde, bir REST API'sı veya bir model-görünüm denetleyici sistemi olabilecek tüm sunum katmanından bahsediyorum. (Görünümün MVC modelindeki modelden ayrılması gerektiğine katılıyorum.) Temel olarak "komut işleyiciye ne çağırırsa çağırmak" demek istiyorum.
Benjamin Hodgson

2

IO olmayan tüm aktiviteler için IO yoğun uygulamanıza yer açmaya çalışıyorsunuz; sizin gibi tipik CRUD uygulamaları maalesef IO'dan başka bir şey yapmıyor.

İlgili ayırma cezasını anladığınızı düşünüyorum, ancak kalıcılık IO kodunu sunum kodundan birkaç katmana yerleştirmeye çalıştığınız zaman, konunun genel gerçeği, kontrol cihazınızda, Sunumunuza çok yakın hissedebileceğiniz bir kalıcılık katmanı - ancak bu tür bir uygulamada sadece başka bir tesadüf yoktur.

Sunum ve kalıcılık, temelde burada açıkladığınızı düşündüğünüz uygulamanın türünün tamamını oluşturur.

Kafanızda çok fazla karmaşık iş mantığı ve veri işleme olan benzer bir uygulama hakkında düşünürseniz, bunun sunumsal IO ve kalıcılık IO şeylerinden nasıl güzelce ayrıldığını hayal edebileceğinizi düşünüyorum. hakkında da hiçbir şey bilmem gerekiyor. Şu anda sahip olduğunuz sorun, başlamak için bu sorunu olmayan bir uygulama türünde bir soruna bir çözüm görmeye çalışmanın neden olduğu algısal bir sorundur.


1
CRUD sistemlerinin kalıcılığı ve sunumu birleştirmesinin uygun olduğunu söylüyorsunuz. Bu benim için makul görünüyor; ancak ben CRUD'den bahsetmedim. Ben özellikle karmaşık nesneler, bir kalıcılık katmanı (komut işleyicileri) ve bunun üzerine bir sunum katmanı cadı iş nesneleri var DDD hakkında soruyorum. İnce bir IO sargısını korurken iki IO katmanını nasıl ayrı tutarsınız ?
Benjamin Hodgson

1
Dikkat, soruda tarif ettiğim alan adı çok karmaşık olabilir . Bir taslak belgeyi atmak, bazı ilgili izin kontrollerine tabidir veya aynı taslağın birden fazla sürümünün ele alınması gerekebilir veya bildirimlerin gönderilmesi gerekebilir veya eylemin başka bir kullanıcı tarafından onaylanması gerekiyor veya taslaklar birkaç kesinleşmeden önceki yaşam döngüsü aşamaları ...
Benjamin Hodgson

2
@BenjaminHodgson DDD veya diğer doğal olarak OO tasarım metodolojilerini kafanızdaki bu duruma karıştırmamanızı şiddetle tavsiye ederim, sadece kafa karıştırıcı olacak. Evet, saf FP'de bitler ve bobbles gibi nesneler oluşturabilirsiniz, ancak bunlara dayalı tasarım yaklaşımları ilk erişiminiz olmamalıdır. Açıkladığınız senaryoda, yukarıda bahsettiğim gibi, iki IO ve saf kod arasında iletişim kuran bir denetleyici hayal ediyorum: Sunum IO giriyor ve denetleyiciden isteniyor, denetleyici şeyleri saf bölümlere ve kalıcılık bölümlerine aktarıyor.
Jimmy Hoffa

1
Hodgson, takdir ettiğiniz her tasarımda isteyebileceğiniz tüm katmanlar ve düşkünlük ile tüm saf kodunuzun yaşadığı bir balonu hayal edebilirsiniz. Bu balon için giriş noktası, sunum, kalıcılık ve saf parçalar arasındaki iletişimi yapan bir "denetleyici" (belki de yanlış) olarak adlandırdığım küçük bir parça olacak. Bu şekilde sebatınız hiçbir şey sunmaz ya da saftır ve tam tersi - bu da IO eşyalarınızı saf sisteminizin balonunun üstünde bu ince tabakada tutar.
Jimmy Hoffa

2
Bahsettiğiniz bu "akıllı nesneler" yaklaşımı doğal olarak FP için kötü bir yaklaşımdır, FP'deki akıllı nesneler ile ilgili sorun çok fazla çiftleşmeleri ve çok az genelleme yapmalarıdır. Buna bağlı veri ve işlevsellik ile sonuçlanırsınız; burada FP, verilerinizin genelleştirilmek üzere işlevlerinizi uygulayabileceğiniz işlevselliğe gevşek bir bağlamaya sahip olmasını tercih eder ve daha sonra birden çok veri türü üzerinde çalışacaktır. Cevabımı burada oku: programmers.stackexchange.com/questions/203077/203082#203082
Jimmy Hoffa

1

Sorunuzu anlayabildiğim kadar yakın (ki olmayabilirim, ama 2 sentime atacağımı düşündüm), nesnelerin kendilerine mutlaka erişiminiz olmadığından, kendi nesne veritabanınıza sahip olmanız gerekir. zaman içinde sona eriyor).

İdeal olarak nesnelerin kendileri durumlarını depolayacak şekilde geliştirilebilirler, böylece "etrafından geçtiklerinde" farklı komut işlemcileri ne ile çalıştıklarını bilirler.

Bu mümkün değilse (icky icky), tek yol, ortak komutlara benzer bazı anahtarlara sahip olmaktır, bilgileri farklı komutlar arasında paylaşılabilecek şekilde ayarlanmış bir mağazada saklamak için kullanabilirsiniz - ve umarım, diğer komut yazarlarının da meta bilgileri kaydetme ve işleme konusundaki arayüzünüzü benimsemesi için arayüzü ve / veya kodu "açın".

Dosya sunucuları alanında, samba, ana bilgisayar işletim sisteminin sağladığı verilere bağlı olarak erişim listeleri ve alternatif veri akışları gibi şeyleri depolamanın farklı yollarına sahiptir. İdeal olarak, samba bir dosya sisteminde barındırılıyor dosyalarda genişletilmiş öznitelikler sağlar. Örnek 'linux' üzerindeki 'xfs' - daha fazla komut genişletilmiş öznitelikleri bir dosyayla birlikte kopyalamaktır (varsayılan olarak, linux'daki çoğu araç genişletilmiş öznitelikler gibi düşünerek "büyüdü").

Alternatif bir çözüm - ortak dosyalarda (nesnelerde) çalışan farklı kullanıcıların birden fazla samba işlemi için çalışan, dosya sistemi kaynağın genişletilmiş özniteliklerde olduğu gibi doğrudan dosyaya eklenmesini desteklemiyorsa, uygulayan bir modül kullanıyor olmasıdır. samba işlemleri için genişletilmiş öznitelikleri taklit eden sanal bir dosya sistemi katmanı. Yalnızca samba bunu bilir, ancak nesne biçimi desteklemediğinde çalışma avantajına sahiptir, ancak yine de önceki durumuna göre dosya üzerinde bazı çalışmalar yapan çeşitli samba kullanıcılarıyla (cf. komut işlemcileri) çalışır. Meta bilgisini, veritabanı boyutunu kontrol etmeye yardımcı olan dosya sistemi için ortak bir veritabanında depolar (ve

Çalıştığınız uygulamaya özgü daha fazla bilgiye ihtiyacınız varsa sizin için yararlı olmayabilir, ancak kavramsal olarak her iki sorun kümesine de aynı teori uygulanabilir. Eğer istediğinizi yapmak için algoritmalar ve yöntemler arıyorsanız, bu yardımcı olabilir. Belirli bir çerçevede daha spesifik bilgiye ihtiyacınız varsa, o zaman belki çok yararlı değil ... ;-)

BTW - 'kendiliğinden sona ermekten' bahsetmemin nedeni - orada hangi nesnelerin olduğunu ve ne kadar sürdüğünü biliyorsanız, bunun net olmamasıdır. Bir nesnenin ne zaman silindiğini bilmenin doğrudan bir yolu yoksa, kullanıcıların nesneleri uzun süredir sildikleri eski veya eski meta bilgileriyle doldurmasını önlemek için kendi metaDB'nizi kırpmanız gerekir.

Nesnelerin ne zaman sona erdiğini / silindiğini biliyorsanız, oyunun ilerisindesiniz ve aynı anda metaDB'nizden sona erebilir, ancak bu seçeneğe sahipseniz net değildi.

Şerefe!


1
Bana göre bu tamamen farklı bir sorunun cevabı gibi görünüyor. Etki alanı odaklı tasarım bağlamında, tamamen işlevsel programlamada mimarlıkla ilgili tavsiye arıyordum. Puanlarınızı açıklığa kavuşturabilir misiniz lütfen?
Benjamin Hodgson

Tamamen işlevsel bir programlama paradigmasında veri kalıcılığını soruyorsunuz. Vikipedi: "Tamamen işlevsel, programın çalışma ortamındaki varlıkların yıkıcı değişikliklerini (güncellemelerini) hariç tutan algoritmaları, veri yapılarını veya programlama dillerini tanımlamak için kullanılan hesaplama terimidir." ==== Tanım olarak, veri kalıcılığı önemsizdir ve hiçbir veriyi değiştirmeyen bir şey için hiçbir faydası yoktur. Açıkçası sorunuzun cevabı yoktur. Yazdıklarınız hakkında daha gevşek bir yorum yapmaya çalışıyordum.
Astara
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.