Bagaimana pola penggunaan penangan perintah untuk menangani ketekunan cocok dengan bahasa murni fungsional, di mana kami ingin membuat kode terkait IO setipis mungkin?
Saat menerapkan Desain Berbasis Domain dalam bahasa berorientasi objek, biasanya menggunakan pola Command / Handler untuk mengeksekusi perubahan status. Dalam desain ini, penangan perintah duduk di atas objek domain Anda, dan bertanggung jawab atas logika terkait kegigihan yang membosankan seperti menggunakan repositori dan menerbitkan peristiwa domain. Penangan adalah wajah publik dari model domain Anda; kode aplikasi seperti UI memanggil penangan ketika perlu mengubah status objek domain.
Sebuah sketsa dalam C #:
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);
}
}
The document
domain objek bertanggung jawab untuk melaksanakan aturan bisnis (seperti "pengguna harus memiliki izin untuk membuang dokumen" atau "Anda tidak bisa membuang dokumen yang sudah dibuang") dan untuk menghasilkan peristiwa domain kita perlu untuk menerbitkan ( document.NewEvents
akan menjadi IEnumerable<Event>
dan mungkin akan berisi DocumentDiscarded
acara).
Ini adalah desain yang bagus - mudah untuk diperluas (Anda dapat menambahkan kasus penggunaan baru tanpa mengubah model domain Anda, dengan menambahkan penangan perintah baru) dan agnostik mengenai bagaimana objek bertahan (Anda dapat dengan mudah menukar repositori NHibernate untuk Mongo repositori, atau tukar penerbit RabbitMQ dengan penerbit EventStore) yang membuatnya mudah untuk diuji menggunakan tipuan dan cemoohan. Itu juga mematuhi pemisahan model / tampilan - penangan perintah tidak tahu apakah itu digunakan oleh pekerjaan batch, GUI, atau API REST.
Dalam bahasa yang murni fungsional seperti Haskell, Anda dapat memodelkan penangan perintah secara kasar seperti ini:
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"
Inilah bagian yang saya berjuang untuk mengerti. Biasanya, akan ada semacam kode 'presentasi' yang memanggil pemanggil perintah, seperti GUI atau API REST. Jadi sekarang kita memiliki dua lapisan dalam program kita yang perlu dilakukan IO - pengendali perintah dan tampilan - yang merupakan no-no besar di Haskell.
Sejauh yang saya bisa tahu, ada dua kekuatan yang berlawanan di sini: satu adalah model / pandangan pemisahan dan yang lainnya adalah kebutuhan untuk mempertahankan model. Perlu ada kode IO untuk mempertahankan model di suatu tempat , tetapi pemisahan model / view mengatakan bahwa kita tidak bisa meletakkannya di lapisan presentasi dengan semua kode IO lainnya.
Tentu saja, dalam bahasa "normal", IO dapat (dan memang) terjadi di mana saja. Desain yang baik menentukan bahwa berbagai jenis IO disimpan terpisah, tetapi kompiler tidak menegakkannya.
Jadi: bagaimana kita mendamaikan pemisahan model / tampilan dengan keinginan untuk mendorong kode IO ke tepi program, ketika model perlu dipertahankan? Bagaimana kita memisahkan dua jenis IO , tetapi masih jauh dari semua kode murni?
Pembaruan : Karunia berakhir dalam waktu kurang dari 24 jam. Saya tidak merasa bahwa salah satu jawaban saat ini telah menjawab pertanyaan saya sama sekali. Komentar @ Ptharien Flame tentang acid-state
tampaknya menjanjikan, tapi itu bukan jawaban dan kurang detail. Aku benci poin-poin ini sia-sia!
sumber
acid-state
tampaknya dekat dengan apa yang Anda gambarkan .acid-state
terlihat cukup bagus, terima kasih untuk tautannya. Dalam hal desain API tampaknya masih terikatIO
; pertanyaan saya adalah tentang bagaimana kerangka kegigihan cocok dengan arsitektur yang lebih besar. Apakah Anda tahu ada aplikasi open-source yang digunakan diacid-state
samping lapisan presentasi, dan berhasil memisahkan keduanya?Query
danUpdate
monad yang cukup jauh dariIO
, sebenarnya. Saya akan mencoba memberikan contoh sederhana dalam jawaban.Jawaban:
Cara umum untuk memisahkan komponen di Haskell adalah melalui tumpukan transformator monad. Saya jelaskan ini secara lebih rinci di bawah ini.
Bayangkan kita sedang membangun sistem yang memiliki beberapa komponen skala besar:
Kami memutuskan bahwa kami perlu menjaga komponen-komponen ini secara longgar digabungkan untuk mempertahankan gaya kode yang baik.
Oleh karena itu kami mengkodekan setiap komponen kami secara polimorfik, menggunakan berbagai kelas MTL untuk memandu kami:
MonadState DataState m => Foo -> Bar -> ... -> m Baz
DataState
adalah representasi murni dari potret keadaan database atau penyimpanan kamiMonadState UIState m => Foo -> Bar -> ... -> m Baz
UIState
adalah representasi murni dari potret keadaan antarmuka pengguna kamiMonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
main :: IO ()
yang melakukan pekerjaan yang hampir sepele menggabungkan komponen lain ke dalam satu sistemzoom
atau kombinator serupaStateT (DataState, UIState) IO
, yang kemudian dijalankan dengan konten aktual dari database atau penyimpanan untuk diproduksiIO
.sumber
DataState
adalah representasi murni dari snapshot keadaan database atau penyimpanan kami". Mungkin Anda tidak bermaksud memuat seluruh database ke dalam memori!Haruskah model ini perlu dipertahankan? Dalam banyak program, menyimpan model diperlukan karena keadaan tidak dapat diprediksi, operasi apa pun dapat mengubah model dengan cara apa pun, jadi satu-satunya cara untuk mengetahui keadaan model adalah dengan mengaksesnya secara langsung.
Jika, dalam skenario Anda, urutan peristiwa (perintah yang telah divalidasi dan diterima) selalu dapat menghasilkan negara, maka itu peristiwa yang perlu dipertahankan, tidak harus negara. Status selalu dapat dihasilkan dengan memutar ulang acara.
Karena itu, sering kali keadaan disimpan, tetapi hanya sebagai snapshot / cache untuk menghindari mengulang perintah, tidak sebagai data program penting.
Setelah perintah diterima, acara dikomunikasikan ke dua tujuan (penyimpanan acara, dan sistem pelaporan) tetapi pada lapisan yang sama dari program.
Lihat Juga
Turunkan Acara Dengan
Saksama
sumber
Anda mencoba menempatkan ruang pada aplikasi intensif IO Anda untuk semua aktivitas non-IO; sayangnya aplikasi CRUD tipikal seperti yang Anda bicarakan hanya sedikit melakukan IO.
Saya pikir Anda memahami denda pemisahan yang relevan, tetapi di mana Anda mencoba untuk menempatkan kode IO kegigihan di beberapa lapisan dari kode presentasi, fakta umum masalah ini ada di controller Anda di suatu tempat Anda harus memanggil Anda lapisan kegigihan, yang mungkin terasa terlalu dekat dengan presentasi Anda - tetapi itu hanya kebetulan bahwa jenis aplikasi itu memiliki sedikit hal lain untuk itu.
Presentasi dan kegigihan pada dasarnya merupakan keseluruhan dari jenis aplikasi yang saya pikir Anda uraikan di sini.
Jika Anda berpikir di kepala Anda tentang aplikasi serupa yang memiliki banyak logika bisnis yang kompleks dan pemrosesan data di dalamnya, saya pikir Anda akan dapat membayangkan bagaimana itu dipisahkan dengan baik dari IO presentasional dan kegigihan hal-hal IO sedemikian rupa sehingga perlu tahu apa-apa tentang baik. Masalah yang Anda miliki saat ini hanyalah masalah persepsi yang disebabkan oleh mencoba melihat solusi untuk masalah dalam jenis aplikasi yang tidak memiliki masalah untuk memulai.
sumber
Sejauh yang saya bisa mengerti pertanyaan Anda (yang mungkin tidak saya lakukan, tetapi saya pikir saya akan memasukkan 2 sen saya), karena Anda tidak harus memiliki akses ke objek itu sendiri, Anda perlu memiliki database objek Anda sendiri yang dapat berakhir seiring waktu).
Idealnya objek itu sendiri dapat ditingkatkan untuk menyimpan keadaan mereka sehingga ketika mereka "diedarkan", prosesor perintah yang berbeda akan tahu apa yang mereka kerjakan.
Jika itu tidak mungkin, (icky icky), satu-satunya cara adalah dengan memiliki kunci seperti-DB yang umum, yang dapat Anda gunakan untuk menyimpan info di toko yang disetel agar dapat dibagikan di antara berbagai perintah - dan semoga, "buka" antarmuka dan / atau kode sehingga penulis perintah lain juga akan mengadopsi antarmuka Anda untuk menyimpan dan memproses informasi-meta.
Di bidang server file samba memiliki berbagai cara untuk menyimpan hal-hal seperti daftar akses dan aliran data alternatif, tergantung pada apa yang disediakan oleh OS host. Idealnya, samba di-host pada sistem file memberikan atribut yang diperluas pada file. Contoh 'xfs' di 'linux' - lebih banyak perintah menyalin atribut yang diperluas bersama dengan file (secara default, sebagian besar utilitas di linux "tumbuh" tanpa berpikir seperti atribut yang diperluas).
Solusi alternatif - yang bekerja untuk banyak proses samba dari pengguna yang berbeda yang beroperasi pada file umum (objek), adalah bahwa jika sistem file tidak mendukung melampirkan sumber daya secara langsung ke file seperti dengan atribut yang diperluas, menggunakan modul yang mengimplementasikan lapisan sistem file virtual untuk meniru atribut diperluas untuk proses samba. Hanya samba yang tahu tentang itu, tetapi memiliki keuntungan bekerja ketika format objek tidak mendukungnya, tetapi masih bekerja dengan beragam pengguna samba (lih. Pemroses perintah) yang melakukan beberapa pekerjaan pada file berdasarkan kondisi sebelumnya. Ini akan menyimpan informasi meta dalam database umum untuk sistem file yang membantu dalam mengontrol ukuran database (dan tidak
Mungkin tidak berguna bagi Anda jika Anda membutuhkan lebih banyak informasi khusus untuk implementasi yang sedang Anda kerjakan, tetapi secara konseptual, teori yang sama dapat diterapkan pada kedua set masalah. Jadi jika Anda mencari algoritma dan metode untuk melakukan apa yang Anda inginkan, itu mungkin bisa membantu. Jika Anda membutuhkan pengetahuan yang lebih spesifik dalam beberapa kerangka kerja tertentu, maka mungkin tidak begitu membantu ... ;-)
BTW - alasan saya menyebutkan 'kedaluwarsa' - adalah bahwa tidak jelas jika Anda tahu benda apa yang ada di luar sana dan berapa lama mereka bertahan. Jika Anda tidak memiliki cara langsung untuk mengetahui kapan suatu objek dihapus, Anda harus memotong metaDB Anda sendiri untuk mencegahnya mengisi dengan informasi meta lama atau kuno yang telah lama dihapus oleh pengguna.
Jika Anda tahu kapan objek kedaluwarsa / dihapus, maka Anda berada di depan permainan, dan dapat kedaluwarsa dari metaDB Anda pada saat yang sama, tetapi tidak jelas apakah Anda memiliki opsi itu.
Bersulang!
sumber