Contoh arsitektur Model-View-Update dalam F #

Seseorang tidak suka Redux dalam Bereaksi karena penerapannya pada JS?


Saya tidak suka sakelar sakelar yang kikuk di reduksi, ada bahasa dengan pencocokan pola yang lebih nyaman, dan mengetik acara dan model model yang lebih baik. Misalnya, F #.
Artikel ini adalah penjelasan tentang perangkat olahpesan di Elmish .


Saya akan memberikan contoh aplikasi konsol yang ditulis pada arsitektur ini, akan jelas pada contohnya bagaimana menggunakan pendekatan ini, dan kemudian kita akan memahami arsitektur Elmish.


Saya menulis aplikasi konsol sederhana untuk membaca puisi, di seed ada beberapa puisi, satu untuk setiap penulis, yang ditampilkan di konsol.


Jendela hanya berisi 4 baris teks, Anda dapat menggulir puisi dengan menekan tombol "Atas" dan "Turun", tombol numerik mengubah warna teks, dan tombol kiri dan kanan memungkinkan Anda menavigasi riwayat tindakan, misalnya, pengguna membaca puisi Pushkin, beralih ke puisi Yesenin, diubah ke puisi Yesenin, diubah warna teks, dan kemudian berpikir bahwa warnanya tidak terlalu bagus dan Yesenin tidak menyukainya, klik dua kali pada panah kiri dan kembali ke tempat di mana ia selesai membaca Pushkin.


Mukjizat ini terlihat seperti ini:



Pertimbangkan implementasinya.


Jika Anda memikirkan semua opsi, jelas bahwa semua yang dapat dilakukan pengguna adalah menekan tombol, dengan menekannya, Anda dapat menentukan apa yang diinginkan pengguna, dan ia dapat berharap:


  1. Ubah penulis
  2. Ubah warna
  3. Gulir (atas / bawah)
  4. Pergi ke versi sebelumnya / berikutnya

Karena pengguna harus dapat kembali ke versi kembali, Anda harus memperbaiki tindakannya dan mengingat modelnya , sebagai akibatnya, semua pesan yang mungkin dijelaskan sebagai berikut:


type Msg = | ConsoleEvent of ConsoleKey | ChangeAuthor of Author | ChangeColor of ConsoleColor | ChangePosition of ChangePosition | ChangeVersion of ChangeVersion | RememberModel | WaitUserAction | Exit type ChangeVersion = | Back | Forward type ChangePosition = | Up | Down type Author = | Pushkin | Lermontov | Blok | Esenin type Poem = Poem of string 

Dalam model, Anda perlu menyimpan informasi tentang teks yang sekarang ada di konsol, riwayat tindakan pengguna dan jumlah tindakan yang akan diputar pengguna untuk mengetahui model mana yang akan ditampilkan.


 type Model = { viewTextInfo: ViewTextInfo countVersionBack: int history: ViewTextInfo list } type ViewTextInfo = { text: string; formatText: string; countLines: int; positionY: int; color: ConsoleColor } 

Arsitektur Elmish - model-view-update, model sudah dipertimbangkan, mari kita lanjutkan untuk melihat:


 let SnowAndUserActionView (model: Model) (dispatch: Msg -> unit) = let { formatText = ft; color = clr } = model.viewTextInfo; clearConsoleAndPrintTextWithColor ft clr let key = Console.ReadKey().Key; Msg.ConsoleEvent key |> dispatch let clearConsoleAndPrintTextWithColor (text: string) (color: ConsoleColor) = Console.Clear(); Console.WriteLine() Console.ForegroundColor <- color Console.WriteLine(text) 

Ini adalah salah satu pandangan, ini dibuat berdasarkan viewTextInfo , menunggu reaksi pengguna, dan mengirimkan pesan ini ke fungsi pembaruan .
Nanti kita akan memeriksa secara rinci apa yang sebenarnya terjadi ketika pengiriman dipanggil, dan apa fungsinya.


Perbarui :


 let update (msg: Msg) (model: Model) = match msg with | ConsoleEvent key -> model, updateConsoleEvent key | ChangeAuthor author -> updateChangeAuthor model author | ChangeColor color -> updateChangeColor model color | ChangePosition position -> updateChangePosition model position | ChangeVersion version -> updateChangeVersion model version | RememberModel -> updateAddEvent model | WaitUserAction -> model, [] 

Bergantung pada jenis pesan, ia memilih fungsi mana yang akan memproses pesan.


Ini adalah pembaruan pada tindakan pengguna, memetakan tombol ke pesan, kasus terakhir - mengembalikan acara WaitUserAction - mengabaikan klik dan menunggu tindakan pengguna lebih lanjut.


 let updateConsoleEvent (key: ConsoleKey) = let msg = match key with | ConsoleKey.D1 -> ChangeColor ConsoleColor.Red | ConsoleKey.D2 -> ChangeColor ConsoleColor.Green | ConsoleKey.D3 -> ChangeColor ConsoleColor.Blue | ConsoleKey.D4 -> ChangeColor ConsoleColor.Black | ConsoleKey.D5 -> ChangeColor ConsoleColor.Cyan | ConsoleKey.LeftArrow -> ChangeVersion Back | ConsoleKey.RightArrow -> ChangeVersion Forward | ConsoleKey.P -> ChangeAuthor Author.Pushkin | ConsoleKey.E -> ChangeAuthor Author.Esenin | ConsoleKey.B -> ChangeAuthor Author.Blok | ConsoleKey.L -> ChangeAuthor Author.Lermontov | ConsoleKey.UpArrow -> ChangePosition Up | ConsoleKey.DownArrow -> ChangePosition Down | ConsoleKey.X -> Exit | _ -> WaitUserAction msg |> Cmd.ofMsg 

Kami mengubah penulis, perhatikan bahwa countVersionBack segera diatur ulang ke 0, yang berarti bahwa jika pengguna memutar kembali kisahnya dan kemudian ingin mengubah warnanya, tindakan ini akan diperlakukan sebagai baru dan akan ditambahkan ke riwayat .


 let updateChangeAuthor (model: Model) (author: Author) = let (Poem updatedText) = seed.[author] let updatedFormatText = getlines updatedText 0 3 let updatedCountLines = (splitStr updatedText).Length let updatedViewTextInfo = {model.viewTextInfo with text = updatedText; formatText = updatedFormatText; countLines = updatedCountLines } { model with viewTextInfo = updatedViewTextInfo; countVersionBack = 0 }, Cmd.ofMsg RememberModel 

Kami juga mengirim pesan RememberModel , pawang yang memperbarui sejarah , menambahkan model saat ini.


 let updateModelHistory model = { model with history = model.history @ [ model.viewTextInfo ] }, Cmd.ofMsg WaitUserAction 

Sisa dari pembaruan dapat dilihat di sini , mereka mirip dengan yang dipertimbangkan.


Untuk menguji kinerja program, saya akan memberikan tes untuk beberapa skenario:


Tes

Metode jalankan mengambil struktur di mana daftar Pesan disimpan dan mengembalikan model setelah mereka diproses.


 [<Property(Verbose=true)>] let ``    `` (authors: Author list) = let state = (createProgram (authors |> List.map ChangeAuthor) |> run) match (authors |> List.tryLast) with | Some s -> let (Poem text) = seed.[s] state.viewTextInfo.text = text | None -> true [<Property(Verbose=true)>] let ``    `` changeColorMsg = let state = (createProgram (changeColorMsg|>List.map ChangeColor)|> run) match (changeColorMsg |> List.tryLast) with | Some s -> state.viewTextInfo.color = s | None -> true [<Property(Verbose=true,Arbitrary=[|typeof<ChangeColorAuthorPosition>|])>] let ``        `` msgs = let tryLastSomeList list = list |> List.filter (Option.isSome) |> List.map (Option.get) |> List.tryLast let lastAuthor = msgs |> List.map (fun x -> match x with | ChangeAuthor a -> Some a | _ -> None) |> tryLastSomeList let lastColor = msgs |> List.map (fun x -> match x with | ChangeColor a -> Some a | _ -> None) |> tryLastSomeList let state = (createProgram msgs |> run) let colorTest = match lastColor with | Some s -> state.viewTextInfo.color = s | None -> true let authorTest = match lastAuthor with | Some s -> let (Poem t) = seed.[s]; state.viewTextInfo.text = t | None -> true authorTest && colorTest 

Untuk ini, perpustakaan FsCheck digunakan, yang menyediakan kemampuan untuk menghasilkan data.


Sekarang pertimbangkan inti dari program ini, kode dalam Elmish ditulis untuk semua kesempatan, saya menyederhanakannya ( kode asli) :


 type Dispatch<'msg> = 'msg -> unit type Sub<'msg> = Dispatch<'msg> -> unit type Cmd<'msg> = Sub<'msg> list type Program<'model, 'msg, 'view> = { init: unit ->'model * Cmd<'msg> update: 'msg -> 'model -> ('model * Cmd<'msg>) setState: 'model -> 'msg -> Dispatch<'msg> -> unit } let runWith<'arg, 'model, 'msg, 'view> (program: Program<'model, 'msg, 'view>) = let (initModel, initCmd) = program.init() //1 let mutable state = initModel //2 let mutable reentered = false //3 let buffer = RingBuffer 10 //4 let rec dispatch msg = let mutable nextMsg = Some msg; //5 if reentered //6 then buffer.Push msg //7 else while Option.isSome nextMsg do // 8 reentered <- true // 9 let (model, cmd) = program.update nextMsg.Value state // 9 program.setState model nextMsg.Value dispatch // 10 Cmd.exec dispatch cmd |> ignore //11 state <- model; // 12 nextMsg <- buffer.Pop() // 13 reentered <- false; // 14 Cmd.exec dispatch initCmd |> ignore // 15 state //16 let run program = runWith program 

Jenis Dispath <'msg> adalah pengiriman yang digunakan dalam tampilan , dibutuhkan pesan dan unit pengembalian
Sub <'msg> - fungsi pelanggan, menerima unit pengiriman dan pengembalian, kami menelurkan daftar Sub ketika kami menggunakanMsg:


 let ofMsg<'msg> (msg: 'msg): Cmd<'msg> = [ fun (dispatch: Dispatch<'msg>) -> dispatch msg ] 

Setelah memanggil ofMsg , seperti Cmd.ofMsg RememberModel di akhir metode updateChangeAuthor , setelah beberapa saat pelanggan dipanggil dan pesan masuk ke metode pembaruan
Cmd <'msg> - Sub Sheet <' msg>


Mari kita beralih ke tipe Program , ini adalah tipe generik, dibutuhkan tipe model, pesan dan tampilan , dalam aplikasi konsol tidak perlu mengembalikan sesuatu dari tampilan , tetapi dalam Elmish. Bereaksi tampilan mengembalikan struktur F # dari pohon DOM.


Bidang init - dipanggil pada awal elmish, fungsi ini mengembalikan model awal dan pesan pertama, dalam kasus saya, saya mengembalikan Cmd.ofMsg RememberModel
Pembaruan adalah fungsi utama pembaruan , Anda sudah terbiasa dengannya.


SetState - di Elmish standar, ia hanya menerima model dan tampilan pengiriman dan panggilan, tetapi saya harus meneruskan pesan untuk mengganti tampilan tergantung pada pesan, saya akan menunjukkan implementasinya setelah kami mempertimbangkan pengiriman pesan.


Fungsi runWith menerima konfigurasi, kemudian memanggil init , model dan pesan pertama dikembalikan, pada baris 2,3 dua objek yang dapat diubah dideklarasikan, yang pertama - di mana keadaan akan disimpan, yang kedua diperlukan oleh fungsi pengiriman .


Pada baris ke-4, buffer dinyatakan - Anda dapat mengambilnya sebagai antrian, yang pertama datang - yang pertama keluar (sebenarnya, implementasi RingBuffer sangat menarik, saya mengambilnya dari perpustakaan, saya menyarankan Anda untuk membacanya di github )


Selanjutnya datang fungsi pengiriman rekursif itu sendiri, yang sama yang disebut dalam tampilan , ketika kita pertama kali memanggil, kita memotong jika jika pada saluran 6 dan segera masuk ke loop, atur reented ke true sehingga panggilan rekursif berikutnya tidak kembali ke loop ini, tetapi tambahkan pesan baru di buffer .


Pada baris 9, kami menjalankan metode pembaruan , dari mana kami mengambil model yang diubah dan pesan baru (untuk pertama kalinya ini adalah pesan RememberModel )
Baris 10 menggambarkan model, metode SetState terlihat seperti ini:



Seperti yang Anda lihat, posting berbeda menyebabkan tampilan berbeda
Ini adalah ukuran yang diperlukan untuk tidak memblokir aliran, karena memanggil Konsol. ReadLine memblokir aliran program, dan peristiwa seperti RememberModel, ChangeColor (yang dipicu di dalam program, bukan oleh pengguna) akan menunggu setiap kali sampai pengguna mengklik tombol, meskipun mereka hanya perlu mengubah warna.


Untuk pertama kalinya, fungsi OnlyShowView akan dipanggil, yang hanya akan menggambar model.
Jika pesan WaitUserAction datang ke metode alih-alih RememberModel, fungsi ShowAndUserActionView akan dipanggil , yang akan menggambar model dan memblokir aliran, menunggu tombol untuk ditekan, segera setelah tombol ditekan, metode pengiriman akan dipanggil lagi dan pesan akan dikirim ke buffer (karena reenvited = false )


Selanjutnya, Anda perlu memproses semua pesan yang datang dari metode pembaruan , jika tidak kita akan kehilangan mereka, panggilan rekursif hanya akan masuk ke loop jika reents menjadi false. Baris 11 terlihat rumit, tetapi sebenarnya itu hanya dorongan dari semua pesan di buffer :


 let exec<'msg> (dispatch: Dispatch<'msg>) (cmd: Cmd<'msg>) = cmd |> List.map (fun sub -> sub dispatch) 

Untuk semua pelanggan yang dikembalikan oleh metode pembaruan , pengiriman akan dipanggil, sehingga menambahkan pesan ini ke buffer .


Pada baris 12 kami memperbarui model, mendapatkan pesan baru dan kembali ke false ketika buffer tidak kosong itu tidak perlu, tetapi jika tidak ada elemen yang tersisa dan pengiriman hanya dapat dipanggil dari tampilan , ini masuk akal. Sekali lagi, dalam kasus kami, ketika semuanya sinkron, ini tidak masuk akal, karena kami mengharapkan panggilan pengiriman sinkron pada saluran 10, tetapi jika ada panggilan tidak sinkron dalam kode, pengiriman dapat dipanggil dari panggilan balik dan Anda harus dapat melanjutkan program.


Nah, itulah keseluruhan deskripsi fungsi pengiriman , pada saluran 15 disebut dan status dikembalikan pada saluran 16.


Dalam aplikasi konsol, keluar terjadi ketika buffer menjadi kosong. Di versi asli, runWith tidak mengembalikan apa-apa, tetapi pengujian tidak mungkin tanpa ini.


Program untuk pengujian berbeda, fungsi createProgram menerima daftar pesan yang akan diinisiasi pengguna dan di SetState mereka mengganti klik yang biasa:


Perbedaan lain antara versi saya yang berubah dan yang asli adalah bahwa fungsi pembaruan disebut pertama, dan kemudian hanya setState , dalam versi asli, sebaliknya, rendering pertama dan kemudian pemrosesan pesan, saya harus melakukan ini karena panggilan pemblokiran ke Console.ReadKey (perlu mengubah lihat )


Saya harap saya berhasil menjelaskan bagaimana Elmish dan sistem serupa bekerja, cukup banyak fungsi Elmish tertinggal, jika Anda tertarik dengan topik ini, saya menyarankan Anda untuk melihat situs web mereka.


Terima kasih atas perhatian anda!

Source: https://habr.com/ru/post/id459870/


All Articles