Kami akan memberi kata tentang dekomposisi kode: pemrograman kontekstual

Tentu saja, idealnya, lebih baik tidak menulis terlalu banyak kode sama sekali . Dan jika Anda menulis, maka, seperti yang Anda tahu, Anda harus berpikir dengan baik sistem tulang arsitektur sistem dan implementasinya sistem daging logika sistem. Dalam catatan ini kami akan memberikan resep untuk implementasi yang mudah dari yang terakhir.


Kami akan memberikan contoh untuk bahasa Clojure, namun prinsip itu sendiri dapat diterapkan dalam bahasa pemrograman fungsional lainnya (misalnya, kami menggunakan ide yang persis sama di Erlang).


Ide


Idenya sendiri sederhana dan berdasarkan pada pernyataan berikut:


  • logika apa pun selalu terdiri dari langkah-langkah dasar;
  • setiap langkah membutuhkan data tertentu, yang menerapkan logika dan menghasilkan hasil yang berhasil atau tidak berhasil.

Pada level pseudo-code, ini dapat direpresentasikan sebagai berikut:


do-something-elementary(context) -> [:ok updated_context] | [:error reason]


Dimana:


  • do-something-elementary - nama fungsi;
  • context - argumen fungsi, struktur data dengan konteks awal dari mana fungsi mengambil semua data yang diperlukan;
  • updated_context - struktur data dengan konteks yang diperbarui, jika berhasil, di mana fungsi menambahkan hasil pelaksanaannya;
  • reason - struktur data, penyebab kegagalan, dengan kegagalan.

Itulah keseluruhan ide. Dan kemudian - masalah teknologi. Dengan 100.500 juta bagian.


Contoh: pengguna melakukan pembelian


Kami akan menuliskan detailnya pada contoh sederhana yang nyata, yang tersedia di GitHub di sini .
Katakanlah kita memiliki pengguna dengan uang dan banyak yang membutuhkan uang dan yang dapat dibeli oleh pengguna. Kami ingin menulis kode yang akan melakukan pembelian lot:


buy-lot(user_id, lot_id) -> [:ok updated_user] | [:error reason]


Untuk kesederhanaan, kami akan menyimpan jumlah uang dan banyak pengguna dalam struktur pengguna itu sendiri.


Untuk implementasi, kita memerlukan beberapa fungsi tambahan.


Berfungsi until-first-error


Dalam sebagian besar kasus, logika bisnis dapat direpresentasikan sebagai urutan langkah-langkah yang perlu diambil sampai kesalahan terjadi. Untuk melakukan ini, kami akan membuat fungsi:


until-first-error(fs, init_context) -> [:ok updated_context] | [:error reason]


Dimana:


  • fs adalah urutan fungsi (tindakan elementer);
  • init_context adalah konteks awal.

Implementasi fitur ini dapat dilihat di GitHub di sini .


Berfungsi with-result-or-error


Seringkali, tindakan elementer adalah Anda hanya perlu menjalankan beberapa fungsi dan, jika berhasil, tambahkan hasilnya ke konteks. Untuk melakukan ini, kami memiliki fungsi:


with-result-or-error(f, key, context) -> [:ok updated_context] | [:error reason]


Secara umum, satu-satunya tujuan fungsi ini adalah untuk mengurangi ukuran kode.


Dan akhirnya, "kecantikan" kami ...


Fungsi yang mengimplementasikan pembelian


 1. (defn buy-lot [user_id lot_id] 2. (let [with-lot-fn (partial 3. util/with-result-or-error 4. #(lot-db/find-by-id lot_id) 5. :lot) 6. 7. buy-lot-fn (fn [{:keys [lot] :as ctx}] 8. (util/with-result-or-error 9. #(user-db/update-by-id! 10. user_id 11. (fn [user] 12. (let [wallet_v (get-in user [:wallet :value]) 13. price_v (get-in lot [:price :value])] 14. (if (>= wallet_v price_v) 15. (let [updated_user (-> user 16. (update-in [:wallet :value] 17. - 18. price_v) 19. (update-in [:lots] 20. conj 21. {:lot_id lot_id 22. :price price_v}))] 23. [:ok updated_user]) 24. [:error {:type :invalid_wallet_value 25. :details {:code :not_enough 26. :provided wallet_v 27. :required price_v}}])))) 28. :user 29. ctx)) 30. 31. fs [with-lot-fn 32. buy-lot-fn]] 33. 34. (match (util/until-first-error fs {}) 35. 36. [:ok {:user updated_user}] 37. [:ok updated_user] 38. 39. [:error reason] 40. [:error reason]))) 

Mari kita lihat kodenya:


  • hal 34: match adalah makro untuk nilai-nilai pencocokan dari templat dari pustaka clojure.core.match ;
  • hlm 34-40: kita menerapkan fungsi dijanjikan until-first-error pada langkah-langkah dasar fs , mengambil data yang kita butuhkan dari konteks dan mengembalikannya, atau membuang kesalahan ke atas;
  • hal. 2-5: kami sedang membangun aksi elementer pertama (yang hanya konteks saat ini akan diterapkan), yang hanya menambahkan data pada kunci :lot ke konteks saat ini;
  • p. 7-29: di sini kita menggunakan fungsi yang akrab with-result-or-error , tetapi tindakan yang dibungkusnya ternyata sedikit lebih rumit: dalam satu transaksi, kami memeriksa bahwa pengguna memiliki cukup uang dan, jika berhasil, kami melakukan pembelian (karena , secara default aplikasi kami multithreaded (dan siapa di suatu tempat yang terakhir kali melihat aplikasi single-threaded?) dan kita harus siap untuk ini).

Dan beberapa kata tentang fungsi lain yang kami gunakan:


  • lot-db/find-by-id(id) - mengembalikan banyak, dengan id ;
  • user-db/update-by-id!(user_id, update-user-fn) - menerapkan fungsi update-user-fn ke pengguna user_id (dalam database imajiner).

Dan untuk menguji? ...


Uji aplikasi sampel ini dari clojure REPL. Kami memulai REPL dari konsol dari root proyek:


 lein repl 

Apa yang pengguna kami dengan keuangan:


 context-aware-app.core=> (context-aware-app.user.db/enumerate) [:ok ({:id "1", :name "Vasya", :wallet {:value 100}, :lots []} {:id "2", :name "Petya", :wallet {:value 100}, :lots []})] 

Apa yang kami punya banyak (barang):


 context-aware-app.core=> (context-aware-app.lot.db/enumerate) [:ok ({:id "1", :name "Apple", :price {:value 10}} {:id "2", :name "Banana", :price {:value 20}} {:id "3", :name "Nuts", :price {:value 80}})] 

Vasya membeli sebuah apel:


 context-aware-app.core=>(context-aware-app.processing/buy-lot "1" "1") [:ok {:id "1", :name "Vasya", :wallet {:value 90}, :lots [{:lot_id "1", :price 10}]}] 

Dan pisang:


 context-aware-app.core=> (context-aware-app.processing/buy-lot "1" "2") [:ok {:id "1", :name "Vasya", :wallet {:value 70}, :lots [{:lot_id "1", :price 10} {:lot_id "2", :price 20}]}] 

Dan "kacang":


 context-aware-app.core=> (context-aware-app.processing/buy-lot "1" "3") [:error {:type :invalid_wallet_value, :details {:code :not_enough, :provided 70, :required 80}}] 

Tidak ada cukup uang untuk kacang.


Total


Akibatnya, menggunakan pemrograman kontekstual, tidak akan ada lagi potongan kode besar (tidak masuk ke dalam satu layar), serta "metode panjang", "kelas besar" dan "daftar panjang parameter". Dan itu memberi:


  • menghemat waktu membaca dan memahami kode;
  • Pengujian kode yang disederhanakan
  • kemampuan untuk menggunakan kembali kode (termasuk menggunakan file copy-paste + dopilivanie);
  • penyederhanaan kode refactoring.

Yaitu semua yang kita sukai dan praktikkan.


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


All Articles