Nikolai Ryzhikov mengusulkan versi jawabannya untuk pertanyaan mengapa sangat sulit untuk mengembangkan antarmuka pengguna. Pada contoh proyeknya, ia akan menunjukkan bahwa aplikasi di frontend dari beberapa ide dari backend mempengaruhi pengurangan kompleksitas pengembangan dan testability dari frontend.
Materi disusun berdasarkan laporan oleh Nikolai Ryzhikov pada musim semi
HolyJS 2018 Piter conference.
Saat ini, Nikolai Ryzhikov bekerja di sektor Kesehatan-TI untuk membuat sistem informasi medis. Anggota komunitas programmer fungsional St. Petersburg FPROG. Anggota aktif dari komunitas Clojure Online, anggota dari standar pertukaran informasi medis HL7 FHIR. Telah pemrograman selama 15 tahun.
- Saya selalu tersiksa oleh pertanyaan: mengapa grafis UI selalu sulit dilakukan? Mengapa ini selalu menimbulkan banyak pertanyaan?
Hari ini saya akan mencoba untuk berspekulasi tentang apakah mungkin untuk mengembangkan antarmuka pengguna secara efektif. Bisakah kita mengurangi kompleksitas perkembangannya.
Apa itu efisiensi?
Mari kita tentukan apa itu efisiensi. Dari sudut pandang pengembangan antarmuka pengguna, efisiensi berarti:
- kecepatan pengembangan
- jumlah bug
- jumlah uang yang dihabiskan ...
Ada definisi yang sangat bagus:
Efisiensi adalah melakukan lebih banyak dengan lebih sedikit
Setelah penentuan ini, Anda dapat meletakkan apa pun yang Anda inginkan - menghabiskan lebih sedikit waktu, lebih sedikit usaha. Misalnya, "jika Anda menulis lebih sedikit kode, izinkan lebih sedikit bug" dan mencapai tujuan yang sama. Secara umum, kami menghabiskan banyak usaha dengan sia-sia. Dan efisiensi adalah tujuan yang agak tinggi - untuk menghilangkan kerugian ini dan hanya melakukan apa yang dibutuhkan.
Apa itu kompleksitas?
Menurut saya, kompleksitas adalah masalah utama dalam pembangunan.
Fred Brooks menulis sebuah artikel pada tahun 1986 berjudul No silver bullet. Di dalamnya, ia merenungkan perangkat lunak. Dalam perangkat keras, kemajuan sangat cepat, dan dengan perangkat lunak semuanya jauh lebih buruk. Pertanyaan utama Fred Brooks - dapatkah ada teknologi yang mempercepat kita dengan urutan besarnya? Dan dia sendiri memberikan jawaban pesimistis, menyatakan bahwa dalam perangkat lunak tidak mungkin untuk mencapai ini, menjelaskan posisinya. Saya sangat merekomendasikan membaca artikel ini.
Seorang teman saya mengatakan pemrograman UI adalah "masalah kotor". Anda tidak dapat duduk sekali dan menghasilkan opsi yang tepat sehingga masalah terpecahkan selamanya. Selain itu, selama 10 tahun terakhir, kompleksitas pembangunan hanya meningkat.
12 tahun yang lalu ...
Kami mulai mengembangkan sistem informasi medis 12 tahun yang lalu. Pertama dengan flash. Lalu kami melihat apa yang mulai dilakukan Gmail. Kami menyukainya dan kami ingin beralih ke JavaScript dengan HTML.
Faktanya, saat itu kami jauh di depan. Kami mengambil dojo, dan sebenarnya kami memiliki semua yang sama seperti yang kami miliki sekarang. Ada beberapa komponen yang cukup bagus di widget dojo, ada sistem modular build dan mengharuskan Google Clojure Compiler dibangun dan dikecilkan (RequireJS dan CommonJS bahkan tidak tercium saat itu).
Semuanya berhasil. Kami melihat Gmail, terinspirasi, berpikir bahwa semuanya baik-baik saja. Pada awalnya, kami hanya menulis pembaca kartu pasien. Kemudian mereka secara bertahap beralih ke otomatisasi alur kerja lain di rumah sakit. Dan semuanya menjadi rumit. Tim tampaknya profesional - tetapi setiap fitur mulai berderit. Sensasi ini muncul 12 tahun yang lalu - dan masih tidak meninggalkan saya.
Cara rel + jQuery
Kami melakukan sertifikasi sistem, dan itu perlu untuk menulis portal pasien. Ini adalah sistem di mana pasien dapat pergi dan melihat data medis mereka.
Backend kami kemudian ditulis dalam Ruby on Rails. Meskipun komunitas Ruby on Rails tidak terlalu besar, itu memiliki dampak besar pada industri. Dari komunitas kecil Anda yang penuh gairah, semua manajer paket Anda, GitHub, Git, make-up otomatis, dll. Telah datang.
Inti dari tantangan yang kami hadapi adalah bahwa kami harus mengimplementasikan portal pasien dalam dua minggu. Dan kami memutuskan untuk mencoba cara Rails - untuk melakukan segalanya di server. Web 2.0 yang klasik. Dan mereka melakukannya - mereka benar-benar melakukannya dalam dua minggu.
Kami berada di depan seluruh planet: kami melakukan SPA, kami memiliki API REST, tetapi untuk beberapa alasan itu tidak efektif. Beberapa fitur sudah dapat membuat unit, karena hanya mereka yang mampu mengakomodasi semua kompleksitas komponen ini, hubungan backend dengan frontend. Dan ketika kami mengambil Rails way - sedikit ketinggalan jaman oleh standar kami, fitur tiba-tiba mulai memukau. Pengembang rata-rata mulai meluncurkan fitur dalam beberapa hari. Dan kami bahkan mulai menulis tes sederhana.
Atas dasar ini, saya sebenarnya masih mengalami cedera: ada pertanyaan. Ketika kami beralih dari Java ke Rails di backend, efisiensi pengembangan meningkat sekitar 10 kali lipat. Tetapi ketika kami mencetak skor di SPA, efisiensi pengembangan juga meningkat secara signifikan. Bagaimana bisa begitu?
Mengapa Web 2.0 efektif?
Mari kita mulai dengan pertanyaan lain: mengapa kita membuat satu aplikasi halaman, mengapa kita meyakininya?
Mereka hanya memberi tahu kami: kami perlu melakukan ini - dan kami melakukannya. Dan sangat jarang mempertanyakannya. Apakah arsitektur REST API dan SPA benar? Apakah ini benar-benar cocok untuk kasing tempat kami menggunakannya? Kami tidak berpikir.
Di sisi lain, ada contoh-contoh sebaliknya yang luar biasa. Semua orang menggunakan GitHub. Apakah Anda tahu bahwa GitHub bukan aplikasi satu halaman? GitHub adalah aplikasi "rail" reguler yang diberikan di server dan di mana ada beberapa widget. Adakah yang mengalami tepung dari ini? Saya pikir ada tiga orang. Sisanya bahkan tidak memperhatikan. Ini tidak mempengaruhi pengguna dengan cara apa pun, tetapi pada saat yang sama, untuk beberapa alasan, kami harus membayar 10 kali lebih banyak untuk pengembangan aplikasi lain (baik kekuatan, kompleksitas, dll.). Contoh lain adalah Basecamp. Twitter dulunya hanya aplikasi Rails.
Bahkan, ada begitu banyak aplikasi Rails. Ini sebagian ditentukan oleh jenius DHH (David Heinemeier Hansson, pencipta Ruby on Rails). Ia mampu menciptakan alat yang berfokus pada bisnis, yang memungkinkan Anda untuk segera melakukan apa yang Anda butuhkan, tanpa terganggu oleh masalah teknis.
Ketika kami menggunakan cara Rails, tentu saja, ada banyak ilmu hitam. Ketika kami secara bertahap berkembang, kami beralih dari Ruby ke Clojure, praktis mempertahankan efisiensi yang sama, tetapi membuat semuanya menjadi lebih mudah. Dan itu luar biasa.
12 tahun telah berlalu
Seiring waktu, tren baru mulai muncul di frontend.
Kami sepenuhnya mengabaikan Backbone, karena aplikasi dojo yang kami tulis sebelumnya bahkan lebih canggih daripada yang ditawarkan Backbone.
Lalu datanglah Angular. Itu adalah "sinar cahaya" yang agak menarik - dari sudut pandang efisiensi, Angular sangat bagus. Anda mengambil pengembang rata-rata, dan dia memusatkan fitur. Namun dari sudut pandang kesederhanaan, Angular membawa banyak masalah - itu buram, rumit, ada arloji, pengoptimalan, dll.
Bereaksi muncul, yang membawa sedikit kesederhanaan (setidaknya keterusterangan render, yang, karena Virtual DOM, memungkinkan kita setiap saat seolah-olah hanya menggambar ulang, hanya memahami dan hanya menulis). Tetapi dalam hal efisiensi, jujur saja, Bereaksi secara signifikan mendorong kami kembali.
Yang terburuk adalah bahwa tidak ada yang berubah dalam 12 tahun. Kami masih melakukan hal yang sama seperti itu. Sudah waktunya untuk berpikir - ada yang salah di sini.
Fred Brooks mengatakan ada dua masalah dengan pengembangan perangkat lunak. Tentu saja, ia melihat masalah utama dalam kompleksitas, tetapi ia membaginya menjadi dua kelompok:
- kompleksitas signifikan yang berasal dari tugas itu sendiri. Itu tidak bisa dibuang begitu saja, karena itu adalah bagian dari tugas.
- kompleksitas acak adalah yang kami bawa untuk menyelesaikan masalah ini.
Pertanyaannya adalah, apa keseimbangan di antara mereka. Inilah tepatnya yang sedang kita diskusikan.
Mengapa begitu menyakitkan untuk melakukan User Interface?

Menurut saya alasan pertama adalah model aplikasi mental kita. Komponen yang bereaksi adalah pendekatan OOP murni. Sistem kami adalah grafik dinamis dari objek yang bisa berubah yang saling berhubungan. Jenis yang menyelesaikan Turing secara konstan menghasilkan simpul-simpul pada grafik ini, beberapa simpul hilang. Pernahkah Anda mencoba membayangkan aplikasi Anda di kepala Anda? Ini menakutkan! Saya biasanya menyajikan aplikasi OOP seperti ini:

Saya merekomendasikan membaca tesis Roy Fielding (penulis arsitektur REST). Disertasinya berjudul "Gaya Arsitektur dan Desain Perangkat Lunak Berbasis Jaringan." Pada awalnya ada pengantar yang sangat baik, di mana ia berbicara tentang bagaimana untuk sampai ke arsitektur secara umum, dan memperkenalkan konsep-konsep: memecah sistem menjadi komponen dan hubungan antara komponen-komponen ini. Ini memiliki arsitektur "nol", di mana semua komponen berpotensi dikaitkan dengan semua. Ini adalah kekacauan arsitektur. Ini adalah representasi objek kami dari antarmuka pengguna.
Roy Fielding merekomendasikan mencari dan memaksakan serangkaian kendala, karena itu adalah serangkaian kendala yang menentukan arsitektur Anda.
Mungkin yang paling penting adalah bahwa pembatasan adalah teman arsitek. Cari batasan nyata ini dan rancang sebuah sistem darinya. Karena kebebasan itu jahat. Kebebasan berarti Anda memiliki sejuta opsi yang dapat Anda pilih, dan bukan kriteria tunggal yang dengannya Anda dapat menentukan apakah pilihan itu benar. Cari kendala dan bangunlah.
Ada artikel bagus yang disebut OUT OF THE TAR PIT ("Lebih mudah daripada lubang tar"), di mana orang-orang setelah Brooks memutuskan untuk menganalisis apa yang sebenarnya berkontribusi pada kompleksitas aplikasi. Mereka sampai pada kesimpulan yang mengecewakan bahwa sistem yang dapat berubah dan menyebar negara adalah sumber utama kompleksitas. Di sini dimungkinkan untuk menjelaskan secara murni kombinasi - jika Anda memiliki dua sel, dan di masing-masingnya sebuah bola bisa berbohong (atau tidak berbohong), berapa banyak keadaan yang mungkin? - Empat.
Jika tiga sel - 2
3 , jika 100 sel - 2
100 . Jika Anda mempresentasikan aplikasi Anda dan memahami seberapa banyak status kabur, Anda menyadari bahwa ada sejumlah kemungkinan kondisi sistem Anda. Jika pada saat yang sama Anda tidak dibatasi oleh apa pun, itu terlalu sulit. Dan otak manusia lemah, ini sudah dibuktikan oleh berbagai penelitian. Kita dapat memegang hingga tiga elemen di kepala kita sekaligus. Ada yang mengatakan tujuh, tetapi bahkan untuk ini otak menggunakan peretasan. Karena itu, kompleksitas adalah masalah bagi kami.
Saya sarankan membaca artikel ini, di mana orang-orang sampai pada kesimpulan bahwa sesuatu harus dilakukan dengan keadaan yang bisa berubah ini. Misalnya, ada database relasional, Anda dapat menghapus seluruh status yang bisa berubah di sana. Dan sisanya dilakukan dengan gaya fungsional murni. Dan mereka hanya datang dengan ide pemrograman fungsional-relasional.
Jadi masalahnya berasal dari fakta bahwa:
- pertama, kami tidak memiliki model antarmuka pengguna tetap yang baik. Pendekatan komponen membawa kita ke neraka yang ada. Kami tidak memaksakan batasan apa pun, kami menyebarkan keadaan yang bisa berubah, akibatnya, kompleksitas sistem pada titik tertentu hanya menghancurkan kami;
- kedua, jika kita menulis aplikasi backend - frontend klasik, itu sudah merupakan sistem terdistribusi. Dan aturan pertama dari sistem terdistribusi adalah jangan membuat sistem terdistribusi (Hukum Pertama Desain Objek Terdistribusi: Jangan mendistribusikan objek Anda - oleh Martin Fowler), karena Anda segera meningkatkan kompleksitas dengan urutan besarnya. Siapa pun yang menulis integrasi memahami bahwa segera setelah Anda memasuki interaksi antar sistem, semua perkiraan proyek dapat dikalikan dengan 10. Tetapi kami hanya melupakannya dan beralih ke sistem terdistribusi. Ini mungkin pertimbangan utama ketika kami beralih ke Rails, mengembalikan semua kontrol ke server.
Semua ini terlalu keras bagi otak manusia yang buruk. Mari kita pikirkan apa yang dapat kita lakukan dengan dua masalah ini - kurangnya batasan dalam arsitektur (grafik objek yang bisa berubah) dan transisi ke sistem terdistribusi yang begitu rumit sehingga para akademisi masih bingung bagaimana melakukannya dengan benar (pada saat yang sama, kami bunuh diri kita sendiri terhadap siksaan-siksaan ini dalam aplikasi bisnis yang paling sederhana)?
Bagaimana backend berevolusi?
Jika kita menulis backend dengan gaya yang sama seperti kita menciptakan UI sekarang, akan ada "kekacauan berdarah" yang sama. Kami akan menghabiskan banyak waktu untuk itu. Jadi benar-benar pernah mencoba melakukannya. Kemudian secara bertahap mereka mulai memberlakukan batasan.
Penemuan backend besar pertama adalah database.

Pada awalnya, dalam program itu, seluruh negara bagian tergantung secara tidak dapat dijelaskan di mana, dan sulit untuk mengelolanya. Seiring waktu, para pengembang datang dengan database dan menghapus seluruh status di sana.
Perbedaan menarik pertama antara database adalah bahwa data tidak ada beberapa objek dengan perilaku mereka sendiri, ini adalah informasi murni. Ada tabel atau beberapa struktur data lainnya (misalnya, JSON). Mereka tidak memiliki perilaku, dan ini juga sangat penting. Karena perilaku adalah interpretasi informasi, dan mungkin ada banyak interpretasi. Dan fakta dasar - mereka tetap mendasar.
Poin penting lainnya adalah bahwa pada database ini kami memiliki bahasa query seperti SQL. Dari sudut pandang keterbatasan, dalam kebanyakan kasus SQL bukan bahasa Turing-lengkap, itu lebih sederhana. Di sisi lain, ini deklaratif - lebih ekspresif, karena dalam SQL Anda mengatakan "apa", bukan "bagaimana". Misalnya, ketika Anda menggabungkan dua label dalam SQL, SQL memutuskan bagaimana melakukan operasi ini secara efisien. Ketika Anda mencari sesuatu, dia mengambil indeks untuk Anda. Anda tidak pernah secara eksplisit menyatakan ini. Jika Anda mencoba menggabungkan sesuatu dalam JavaScript, Anda harus menulis banyak kode untuk ini.
Di sini, sekali lagi, penting bahwa kita telah memberlakukan batasan dan sekarang kita pergi ke pangkalan ini melalui bahasa yang lebih sederhana dan lebih ekspresif. Kompleksitas yang didistribusikan kembali.
Setelah backend memasuki pangkalan, aplikasi menjadi stateless. Ini mengarah ke efek menarik - sekarang, misalnya, kita mungkin tidak takut untuk memperbarui aplikasi (keadaan tidak menggantung di lapisan aplikasi dalam memori, yang akan hilang jika aplikasi restart). Untuk lapisan aplikasi, stateless adalah fitur yang bagus dan kendala yang sangat baik. Kenakan jika Anda bisa. Selain itu, aplikasi baru dapat ditarik ke pangkalan lama, karena fakta dan interpretasi mereka bukan hal yang terkait.
Dari sudut pandang ini, objek dan kelas mengerikan karena merekatkan perilaku dan informasi. Informasi lebih kaya, ia hidup lebih lama. Basis data dan fakta bertahan kode ditulis dalam Delphi, Perl, atau JavaScript.
Ketika backend datang ke arsitektur seperti itu, semuanya menjadi lebih sederhana. Era emas Web 2.0 telah tiba. Dimungkinkan untuk mendapatkan sesuatu dari basis data, mengarahkan data ke templating (fungsi murni) dan mengembalikan HTML-ku, yang dikirim ke browser.
Kami belajar cara menulis aplikasi yang cukup kompleks di backend. Dan sebagian besar aplikasi ditulis dengan gaya ini. Tapi begitu backend mengambil langkah ke samping - ke dalam ketidakpastian - masalah mulai lagi.
Orang-orang mulai memikirkannya dan muncul dengan ide membuang PLO dan ritual.
Apa yang sebenarnya dilakukan sistem kita? Mereka mengambil informasi dari suatu tempat - dari pengguna, dari sistem lain dan sejenisnya - memasukkannya ke dalam basis data, mengubahnya, entah bagaimana memeriksanya. Dari pangkalan mereka mengeluarkannya dengan pertanyaan licik (analitik atau sintetik) dan mengembalikannya. Itu saja. Dan ini penting untuk dipahami. Dari sudut pandang ini, simulasi adalah konsep yang sangat salah dan buruk.
Tampak bagi saya bahwa secara umum seluruh OOP sebenarnya lahir dari UI. Orang-orang mencoba mensimulasikan dan mensimulasikan antarmuka pengguna. Mereka melihat objek grafis tertentu pada monitor dan berpikir: akan menyenangkan untuk merangsangnya dalam runtime kita, beserta propertinya, dll. Seluruh cerita ini sangat terkait erat dengan OOP. Tetapi simulasi adalah cara yang paling mudah dan naif untuk menyelesaikan tugas. Hal-hal menarik dilakukan ketika Anda minggir. Dari sudut pandang ini, lebih penting untuk memisahkan informasi dari perilaku, menyingkirkan benda-benda aneh ini, dan semuanya akan menjadi lebih mudah: server web Anda menerima string HTTP, mengembalikan string respons HTTP. Jika Anda menambahkan basis ke persamaan, Anda mendapatkan fungsi umumnya murni: server menerima basis dan permintaan, mengembalikan basis dan respons baru (data yang dimasukkan - data tersisa).
Sepanjang penyederhanaan ini, fungsionaris membuang ⅔ bagasi yang telah menumpuk di backend. Dia tidak dibutuhkan, itu hanya ritual. Kami masih bukan game dev - kami tidak membutuhkan pasien dan dokter untuk hidup dalam runtime, bergerak dan melacak koordinat mereka. Model informasi kami adalah sesuatu yang lain. Kami tidak berpura-pura menjadi obat, penjualan, atau apa pun. Kami menciptakan sesuatu yang baru di persimpangan. Misalnya, Uber tidak mensimulasikan perilaku operator dan mesin - ia memperkenalkan model informasi baru. Di bidang kami, kami juga menciptakan sesuatu yang baru, sehingga Anda dapat merasakan kebebasan.
Tidak perlu mencoba untuk mensimulasikan sepenuhnya - buat.
Clojure = JS--
Saatnya untuk memberi tahu Anda dengan tepat bagaimana Anda bisa membuang semuanya. Dan di sini saya ingin menyebutkan Clojure Script. Bahkan, jika Anda tahu JavaScript, Anda tahu Clojure. Di Clojure, kami tidak menambahkan fitur ke JavaScript, tetapi menghapusnya.
- Kami membuang sintaks - di Clojure (dalam Lisp) tidak ada sintaks. Dalam bahasa biasa, kami menulis beberapa kode, yang kemudian diuraikan dan AST diperoleh, yang dikompilasi dan dieksekusi. Di Lisp, kami segera menulis AST yang dapat dieksekusi - ditafsirkan atau dikompilasi.
- Kami membuang sifat berubah-ubah. Tidak ada objek atau array yang dapat berubah di Clojure. Setiap operasi menghasilkan seolah-olah salinan baru. Apalagi salinan ini sangat murah. Itu cerdik dibuat menjadi murah. Dan ini memungkinkan kita untuk bekerja, seperti dalam matematika, dengan nilai-nilai. Kami tidak mengubah apa pun - kami menciptakan sesuatu yang baru. Aman, mudah.
- Kami melempar kelas, game dengan prototipe, dll. Ini tidak ada.
Akibatnya, kami masih memiliki fungsi dan struktur data yang kami operasikan, serta primitif. Ini seluruh Clojure. Dan di atasnya Anda dapat melakukan hal yang sama dengan yang Anda lakukan dalam bahasa lain, di mana ada banyak alat tambahan yang tidak ada yang tahu cara menggunakannya.
Contohnya
Bagaimana kita bisa sampai ke Lisp melalui AST? Berikut ini adalah ungkapan klasik:
(1 + 2) - 3
Jika kita mencoba menulis AST-nya, misalnya, dalam bentuk array, di mana head adalah tipe node, dan apa yang berikutnya adalah parameter, kita akan mendapatkan sesuatu yang serupa (kita mencoba menulis ini dalam Java Script):
['minus', ['plus', 1, 2], 3]
Sekarang buang tanda kutip ekstra, kita dapat mengganti tanda minus dengan
-
, dan tanda tambah dengan
+
. Buang koma yang merupakan spasi putih di Lisp. Kami akan mendapatkan AST yang sama:
(- (+ 1 2) 3)
Dan di Lisp, kita semua menulis seperti ini. Kita dapat memeriksa - ini adalah fungsi matematika murni (emacs saya terhubung ke browser; Saya letakkan script di sana, itu mengevaluasi perintah di sana dan mengirimkannya kembali ke emacs - Anda melihat nilai setelah simbol
=>
):
(- (+ 1 2) 3) => 0
Kami juga dapat mendeklarasikan fungsi:
(defn xplus [ab] (+ ab)) ((fn [xy] (* xy)) 1 2) => 2
Atau fungsi anonim. Mungkin ini terlihat sedikit menakutkan:
(type xplus)
Jenisnya adalah fungsi JavaScript:
(type xplus) => #object[Function]
Kita bisa menyebutnya dengan mengirimkan parameter:
(xplus 1 2)
Artinya, semua yang kita lakukan adalah menulis AST, yang kemudian dikompilasi ke dalam JS atau bytecode, atau ditafsirkan.
(defn mymin [ab] (if (a > b) ba))
Clojure adalah bahasa yang di-host. Oleh karena itu, diperlukan primitif dari runtime induk, yaitu, dalam kasus Clojure Script, kita akan memiliki tipe JavaScript:
(type 1) => #object[Number]
(type "string") => #object[String]
Jadi regexp ditulis:
(type #"^Cl.*$") => #object[RegExp]
Fungsi yang kami miliki adalah fungsi:
(type (fn [x] x)) => #object[Function]
Selanjutnya kita membutuhkan beberapa jenis komposit.
(def user {:name "niquola" :address {:city "SPb"} :profiles [{:type "github" :link "https://….."} {:type "twitter" :link "https://….."}] :age 37} (type user)
Ini dapat dibaca seolah-olah Anda sedang membuat objek dalam JavaScript:
(def user {name: "niquola" …
Di Clojure, ini disebut hashmap. Ini adalah wadah di mana nilai-nilai terletak. Jika tanda kurung digunakan - maka ini disebut vektor - ini adalah array Anda:
(def user {:name "niquola" :address {:city "SPb"} :profiles [{:type "github" :link "https://….."} {:type "twitter" :link "https://….."}] :age 37} => #'intro/user (type user)
Kami mencatat informasi apa pun dengan hashmaps dan vektor.
Nama titik dua yang aneh (
:name
) adalah karakter yang disebut: string konstan yang dibuat untuk digunakan sebagai kunci dalam hashmaps. Dalam bahasa yang berbeda mereka disebut berbeda - simbol, sesuatu yang lain. Tapi ini bisa diambil hanya sebagai string konstan. Mereka cukup efektif - Anda dapat menulis nama panjang dan tidak menghabiskan banyak sumber daya untuk itu, karena mereka terhubung (mis. Mereka tidak diulang).
Clojure menyediakan ratusan fungsi untuk menangani struktur data generik dan primitif ini. Kami dapat menambahkan, menambahkan kunci baru. Selain itu, kami selalu memiliki salinan semantik, yaitu, setiap kali kami mendapatkan salinan baru. Pertama, Anda harus terbiasa dengannya, karena Anda tidak lagi dapat menyimpan sesuatu, seperti sebelumnya, di suatu tempat dalam variabel, dan kemudian mengubah nilai ini. Perhitungan Anda harus selalu langsung - semua argumen harus diteruskan ke fungsi secara eksplisit.
Ini mengarah pada hal yang penting. Dalam bahasa fungsional, fungsi adalah komponen yang ideal karena ia menerima semuanya secara eksplisit pada input. Tidak ada tautan tersembunyi yang menyimpang dalam sistem. Anda dapat mengambil fungsi dari satu tempat, memindahkannya ke tempat lain, dan menggunakannya di sana.
Di Clojure, kami memiliki operasi kesetaraan yang sangat baik dalam nilai bahkan untuk tipe komposit kompleks:
(= {:a 1} {:a 1}) => true
Dan operasi ini murah karena fakta bahwa struktur tidak berujung yang licik dapat dibandingkan hanya dengan referensi. Karena itu, bahkan sebuah hashmap dengan jutaan kunci dapat kita bandingkan dalam satu operasi.
Ngomong-ngomong, orang-orang dari React hanya menyalin implementasi Clojure dan membuat JS abadi.
Clojure juga memiliki banyak operasi, misalnya, mendapatkan sesuatu dari jalan bersarang di hashmap:
(get-in user [:address :city])
Masukkan sesuatu di sepanjang jalan bersarang di hashmap:
(assoc-in user [:address :city] "LA") => {:name "niquola", :address {:city "LA"}, :profiles [{:type "github", :link "https://….."} {:type "twitter", :link "https://….."}], :age 37}
Perbarui beberapa nilai:
(update-in user [:profiles 0 :link] (fn [old] (str old "+++++")))
Pilih hanya kunci tertentu:
(select-keys user [:name :address])
Hal yang sama dengan vektor:
(def clojurists [{:name "Rich"} {:name "Micael"}]) (first clojurists) (second clojurists) => {:name "Michael"}
Ada ratusan operasi dari pustaka dasar yang memungkinkan Anda untuk beroperasi pada struktur data ini. Ada interop dengan tuan rumah. Anda harus terbiasa sedikit:
(js/alert "Hello!") => nil </csource> "". location window: <source lang="clojure"> (.-location js/window)
Ada setiap gula yang harus dikonsumsi:
(.. js/window -location -href) => "http://localhost:3000/#/billing/dashboard"
(.. js/window -location -host) => "localhost:3000"
Saya dapat mengambil tanggal JS dan mengembalikan tahun darinya:
(let [d (js/Date.)] (.getFullYear d)) => 2018
Rich Hickey, pencipta Clojure, telah sangat membatasi kami. Kami benar-benar tidak memiliki hal lain, jadi kami melakukan semuanya melalui struktur data umum. Misalnya, ketika kita menulis SQL, kita biasanya menulisnya dengan struktur data. Jika Anda perhatikan dengan teliti, Anda akan melihat bahwa ini hanyalah sebuah hashmap di mana sesuatu tertanam. Lalu ada beberapa fungsi yang menerjemahkan semua ini menjadi string SQL:
{select [:*] :from [:users] :where [:= :id "user-1"]} => {:select [:*], :from [:users], :where [:= :id "user-1"]}
Kami juga menulis routings dengan struktur data dan struktur data typeset:
{"users" {:get {:handler :users-list}} :get {:handler :welcome-page}}
[:div.row [:div {:on-click #(.log js/console "Hello")} "User "]]
DB di UI
Jadi, kami membahas Clojure. Tapi saya sebutkan sebelumnya bahwa pencapaian besar di backend adalah database. Jika Anda melihat apa yang terjadi di frontend sekarang, kita akan melihat bahwa orang-orang menggunakan pola yang sama - mereka memasukkan basis data di Antarmuka Pengguna (dalam satu aplikasi halaman).
Database diperkenalkan dalam elm-architecture, dalam Clojure-scripted re-frame, dan bahkan dalam beberapa bentuk terbatas dalam fluks dan redux (plugin tambahan harus diatur di sini untuk mengajukan permintaan). Arsitektur Elm, bingkai ulang dan fluks diluncurkan pada waktu yang hampir bersamaan dan dipinjam dari satu sama lain. Kami menulis di bingkai ulang. Selanjutnya, saya akan berbicara sedikit tentang cara kerjanya.

Event (ini mirip redux) terbang keluar dari view-chi, yang ditangkap oleh pengontrol tertentu. Kontroler yang kami sebut event-handler. Event-handler memancarkan efek, yang juga seseorang ditafsirkan oleh struktur data.
Salah satu jenis efek adalah memperbarui basis data. Artinya, dibutuhkan nilai database saat ini dan mengembalikan yang baru. Kami juga memiliki langganan - analog permintaan di backend. Yaitu, ini adalah beberapa pertanyaan reaktif yang bisa kita lemparkan ke database ini. Permintaan-permintaan reaktif ini, kami kemudian menyatukan pandangan. Dalam kasus reaksi, kami tampaknya sepenuhnya menggambar ulang, dan jika hasil dari permintaan ini telah berubah - ini nyaman.
Bereaksi hadir dengan kita hanya di suatu tempat di bagian paling akhir, dan secara umum arsitektur tidak terhubung dengan itu. Itu terlihat seperti ini:

Di sini ditambahkan apa yang hilang, misalnya, di redux-s.
Pertama, kami memisahkan efeknya. Aplikasi frontend tidak mandiri. Dia memiliki backend tertentu - sejenis, "sumber kebenaran". Aplikasi harus terus menulis sesuatu di sana dan membaca sesuatu dari sana. Lebih buruk lagi, jika ia memiliki beberapa backend yang seharusnya. Dalam implementasi paling sederhana, ini bisa dilakukan langsung di action creater - di controller Anda, tetapi ini buruk. Oleh karena itu, orang-orang dari bingkai ulang memperkenalkan tingkat tipuan tambahan: struktur data tertentu terbang keluar dari controller, yang mengatakan apa yang perlu dilakukan. Dan posting ini memiliki penangan sendiri yang melakukan pekerjaan kotor. Ini adalah pengantar yang sangat penting, yang akan kita bahas nanti.
Ini juga penting (kadang-kadang mereka melupakannya) - beberapa fakta dasar harus ada di pangkalan. Segala sesuatu yang lain dapat dihapus dari database - dan permintaan biasanya melakukan ini, mereka mengubah data - mereka tidak menambahkan informasi baru, tetapi dengan benar menyusun yang sudah ada. Kami membutuhkan pertanyaan ini. Dalam redux, menurut pendapat saya, ini sekarang menyediakan pilihan kembali, dan dalam bingkai kami memilikinya di luar kotak (built-in).
Lihatlah diagram arsitektur kami. Kami mereproduksi backend kecil (dalam gaya Web 2.0) dengan basis, pengontrol, tampilan. Satu-satunya hal yang ditambahkan adalah reaktivitas. Ini sangat mirip dengan MVC, kecuali bahwa semuanya ada di satu tempat. Setelah MVC awal untuk setiap widget dibuat model mereka sendiri, tetapi di sini semuanya dilipat menjadi satu basis. Pada prinsipnya, Anda dapat menyinkronkan dengan backend dari controller melalui efek, Anda bisa tampil dengan tampilan yang lebih umum sehingga database berfungsi seperti proxy ke backend. Bahkan ada beberapa jenis algoritma generik: Anda menulis ke database lokal Anda, dan menyinkronkannya dengan yang utama.
Sekarang dalam kebanyakan kasus, basis hanyalah beberapa jenis objek di mana kita menulis sesuatu dalam redux. Tetapi pada prinsipnya, orang dapat membayangkan bahwa lebih lanjut ia akan berkembang menjadi basis data lengkap dengan bahasa query yang kaya. Mungkin dengan semacam sinkronisasi umum. Sebagai contoh, ada datomik - database logis tiga-toko yang berjalan langsung di browser. Anda mengambilnya dan menempatkan seluruh negara Anda di sana. Datomic memiliki bahasa query yang cukup kaya, sebanding dengan kekuatan SQL, dan bahkan menang di suatu tempat. Contoh lain adalah Google menulis lovefield. Semuanya akan pindah ke suatu tempat di sana.
Selanjutnya saya akan menjelaskan mengapa kita perlu berlangganan reaktif.
Sekarang kita mendapatkan persepsi naif pertama - kita mendapatkan pengguna dari backend, menaruhnya di database, dan kemudian kita perlu menggambarnya. Pada saat rendering, banyak logika tertentu terjadi, tetapi kami mencampurnya dengan rendering, dengan view. Jika kami segera mulai merender pengguna ini, kami mendapatkan bagian besar yang rumit yang melakukan sesuatu dengan Virtual DOM dan yang lainnya. Dan itu dicampur dengan model logis dari pandangan kita.
Konsep yang sangat penting yang perlu dipahami: karena kompleksitas UI, itu juga perlu dimodelkan. Adalah perlu untuk memisahkan bagaimana itu ditarik (seperti yang terlihat) dari model logisnya. Maka model logis akan lebih stabil. Anda tidak dapat membebani dengan ketergantungan pada kerangka kerja tertentu - Angular, React atau VueJS. Model adalah warga negara kelas satu yang biasa di runtime Anda. Idealnya, jika itu hanya beberapa data dan satu set fungsi di atasnya.

Artinya, dari model backend (objek), kita bisa mendapatkan model tampilan di mana, tanpa menggunakan rendering apa pun, kita dapat membuat kembali model logis. Jika ada semacam menu atau yang serupa - semua ini dapat dilakukan dalam model tampilan.
Mengapa
Kenapa kita semua melakukan ini?
Saya telah melihat tes UI yang baik hanya di mana ada staf 10 penguji.
Biasanya tidak ada pengujian UI. Oleh karena itu, kami mencoba untuk mendorong logika ini keluar dari komponen dalam model tampilan. Kurangnya tes adalah pertanda sangat buruk, menunjukkan bahwa ada sesuatu yang salah di sana, entah bagaimana semuanya terstruktur dengan buruk.
Mengapa UI sulit diuji? Mengapa orang-orang di backend belajar cara menguji kode mereka, memberikan cakupan yang sangat besar dan sangat membantu untuk hidup dengan kode backend? Mengapa UI salah? Kemungkinan besar, kami melakukan sesuatu yang salah. Dan semua yang saya jelaskan di atas benar-benar menggerakkan kami ke arah testabilitas.
Bagaimana cara kita melakukan tes?
Jika Anda melihat lebih dekat, bagian dari arsitektur kami, yang berisi pengontrol, berlangganan, dan basis data, bahkan tidak terkait dengan JS. Artinya, ini adalah semacam model yang beroperasi hanya pada struktur data: kami menambahkannya di suatu tempat, entah bagaimana mengubah, mengeluarkan kueri. Melalui efek, kita terputus dari interaksi dengan dunia luar. Dan bagian ini sepenuhnya portabel. Ini dapat ditulis dalam apa yang disebut cljc - ini adalah subset umum antara Clojure Script dan Clojure, yang berperilaku dengan cara yang sama di sana-sini. Kita bisa memotong bagian ini dari frontend dan meletakkannya di JVM - di mana backend tinggal. Kemudian kita dapat menulis efek lain di JVM, yang langsung mengenai titik akhir - itu menarik router tanpa konversi http-string, parsing, dll.

Sebagai hasilnya, kita dapat menulis tes yang sangat sederhana - tes integral fungsional yang sama dengan yang orang-orang tulis di backend. Kami melempar acara tertentu, itu melempar efek yang langsung menyentuh titik akhir di backend. Dia mengembalikan sesuatu kepada kami, meletakkannya di database, menghitung langganan, dan di langganan terletak tampilan logis (kami menempatkan logika antarmuka pengguna di sana secara maksimal). Kami menegaskan pandangan ini.
Dengan demikian, kita dapat menguji 80% dari kode di backend, sementara semua alat pengembangan backend tersedia untuk kita. Menggunakan perlengkapan atau beberapa pabrik, kita dapat menciptakan kembali situasi tertentu dalam database.
Misalnya, kami memiliki pasien baru atau ada sesuatu yang tidak dibayar, dll. Kita bisa melalui banyak kombinasi yang mungkin.
Dengan demikian, kita dapat menangani masalah kedua - dengan sistem terdistribusi. Karena kontrak antara sistem justru merupakan titik sakit utama, karena ini adalah dua runtime yang berbeda, dua sistem yang berbeda: backend mengubah sesuatu, dan sesuatu pecah pada frontend kami (Anda tidak dapat memastikan bahwa ini tidak akan terjadi).
Demonstrasi
Beginilah tampilannya dalam praktik. Ini adalah pembantu backend yang membersihkan pangkalan dan menulis sedikit dunia ke dalamnya:

Selanjutnya kita langganan:

Biasanya URL sepenuhnya mendefinisikan halaman dan beberapa peristiwa dilemparkan - Anda sekarang pada halaman ini dan itu dengan seperangkat parameter. Di sini kami masuk ke alur kerja baru dan langganan kami dikembalikan:

Di belakang layar, dia pergi ke pangkalan, mengambil sesuatu, memasukkannya ke pangkalan UI kami. Berlangganan di atasnya berhasil dan disimpulkan dari itu Lihat model logis.
Kami menginisialisasi itu. Dan ini adalah model logis kami:

Bahkan tanpa melihat antarmuka Pengguna, kita dapat menebak apa yang akan ditarik sesuai dengan model ini: beberapa peringatan akan datang, beberapa informasi tentang pasien, pertemuan dan serangkaian tautan akan berbohong (ini adalah widget alur kerja yang mengarah di meja depan) dalam langkah-langkah tertentu ketika pasien tiba).
Di sini kita datang dengan dunia yang lebih kompleks. Mereka melakukan beberapa pembayaran dan juga diuji setelah inisialisasi:

Jika dia sudah membayar untuk kunjungan tersebut, dia akan melihat ini di antarmuka pengguna:

Jalankan tes, setel ke CI. Sinkronisasi antara backend dan frontend akan dijamin oleh tes, dan tidak jujur.
Kembali ke backend?
Kami memperkenalkan tes enam bulan lalu, dan kami benar-benar menyukainya. Masalah logika kabur masih ada. Semakin cerdas aplikasi bisnis, semakin banyak informasi yang diperlukan untuk beberapa langkah. Jika Anda mencoba menjalankan semacam alur kerja dari dunia nyata di sana, dependensi akan muncul pada segalanya: untuk setiap antarmuka pengguna Anda perlu mendapatkan sesuatu dari berbagai bagian database di backend. Jika kita menulis sistem akuntansi, ini tidak dapat dihindari. Akibatnya, seperti yang saya katakan, semua logika dioleskan.
Dengan bantuan tes seperti itu, kita dapat membuat ilusi setidaknya di dev-time - pada saat pengembangan - bahwa kita, seperti di masa lalu web 2.0, duduk di server dalam satu runtime dan semuanya nyaman.
Gagasan gila lainnya muncul (belum diimplementasikan). Mengapa tidak menurunkan bagian ini ke backend? Mengapa tidak pergi sepenuhnya dari aplikasi yang didistribusikan sekarang? Biarkan langganan ini dan model tampilan kami dihasilkan di backend? Di sana basis tersedia, semuanya sinkron. Semuanya sederhana dan jelas.
Nilai tambah pertama yang saya lihat di sini adalah bahwa kita akan memiliki kendali di satu tempat. Kami hanya menyederhanakan semuanya segera dibandingkan dengan aplikasi terdistribusi kami. Tes menjadi sederhana, validasi ganda menghilang. Dunia modis dari sistem multi-user interaktif terbuka (jika dua pengguna pergi ke bentuk yang sama, kami memberi tahu mereka tentang hal itu; mereka dapat mengeditnya pada saat yang sama).
Sebuah fitur menarik muncul: dengan membuka backend dan prospek sesi, kita dapat memahami siapa yang saat ini ada dalam sistem dan apa yang sedang dia lakukan. Ini agak mirip game dev, di mana server bekerja seperti ini. Di sana dunia tinggal di server, dan front-end hanya merender. Hasilnya, kita bisa mendapatkan klien tipis tertentu.
Di sisi lain, ini menciptakan tantangan. Kita harus memiliki server statefull di mana sesi ini hidup. Jika kami memiliki beberapa server aplikasi, akan perlu untuk entah bagaimana menyeimbangkan beban atau mereplikasi sesi dengan benar. Namun, ada kecurigaan bahwa masalah ini kurang dari jumlah plus yang kita dapatkan.
Oleh karena itu, saya kembali ke slogan utama: ada banyak jenis aplikasi yang dapat ditulis tidak didistribusikan, untuk membuang kerumitan dari mereka. Dan Anda bisa mendapatkan beberapa peningkatan dalam efisiensi jika Anda sekali lagi merevisi postulat dasar yang menjadi andalan kami dalam pengembangan.
Jika Anda menyukai laporan ini, perhatikan: pada 24-25 November, HolyJS baru akan diadakan di Moskow, dan juga akan ada banyak hal menarik di sana. Informasi yang sudah diketahui tentang program ini ada di situs , dan tiket dapat dibeli di sana.