Panduan untuk Pemrograman Berorientasi Proses dalam Elixir dan OTP

Diterbitkan: 2022-03-11

Orang suka mengkategorikan bahasa pemrograman ke dalam paradigma. Ada bahasa berorientasi objek (OO), bahasa imperatif, bahasa fungsional, dll. Ini dapat membantu dalam mencari tahu bahasa mana yang memecahkan masalah serupa, dan jenis masalah apa yang ingin dipecahkan oleh bahasa.

Dalam setiap kasus, sebuah paradigma umumnya memiliki satu fokus dan teknik "utama" yang merupakan kekuatan pendorong untuk rumpun bahasa tersebut:

  • Dalam bahasa OO, itu adalah kelas atau objek sebagai cara untuk merangkum keadaan (data) dengan manipulasi keadaan itu (metode).

  • Dalam bahasa fungsional, ini bisa berupa manipulasi fungsi itu sendiri atau data yang tidak dapat diubah yang diteruskan dari fungsi ke fungsi.

Sementara Elixir (dan Erlang sebelumnya) sering dikategorikan sebagai bahasa fungsional karena mereka menunjukkan data yang tidak dapat diubah yang umum untuk bahasa fungsional, saya akan menyampaikan bahwa mereka mewakili paradigma yang terpisah dari banyak bahasa fungsional . Mereka ada dan diadopsi karena keberadaan OTP, jadi saya akan mengkategorikannya sebagai bahasa berorientasi proses .

Dalam posting ini, kami akan menangkap arti dari apa itu pemrograman berorientasi proses ketika menggunakan bahasa-bahasa ini, mengeksplorasi perbedaan dan persamaan dengan paradigma lain, melihat implikasi untuk pelatihan dan adopsi, dan diakhiri dengan contoh pemrograman berorientasi proses singkat.

Apa itu Pemrograman Berorientasi Proses?

Mari kita mulai dengan definisi: Pemrograman berorientasi proses adalah sebuah paradigma yang didasarkan pada Communicating Sequential Processes, yang berasal dari makalah Tony Hoare pada tahun 1977. Ini juga populer disebut model aktor konkurensi. Bahasa lain yang berhubungan dengan karya asli ini termasuk Occam, Limbo, dan Go. Makalah formal hanya membahas komunikasi sinkron; kebanyakan model aktor (termasuk OTP) menggunakan komunikasi asinkron juga. Itu selalu memungkinkan untuk membangun komunikasi sinkron di atas komunikasi asinkron, dan OTP mendukung kedua bentuk.

Pada sejarah ini, OTP menciptakan sistem untuk komputasi yang toleran terhadap kesalahan dengan mengomunikasikan proses sekuensial. Fasilitas toleransi kesalahan berasal dari pendekatan "biarkan gagal" dengan pemulihan kesalahan yang solid dalam bentuk pengawas dan penggunaan pemrosesan terdistribusi yang diaktifkan oleh model aktor. "Biarkan gagal" dapat dikontraskan dengan "mencegahnya agar tidak gagal", karena yang pertama jauh lebih mudah untuk diakomodasi dan telah terbukti dalam OTP jauh lebih andal daripada yang terakhir. Alasannya adalah bahwa upaya pemrograman yang diperlukan untuk mencegah kegagalan (seperti yang ditunjukkan pada model pengecualian yang diperiksa Java) jauh lebih terlibat dan menuntut.

Jadi, pemrograman berorientasi proses dapat didefinisikan sebagai paradigma di mana struktur proses dan komunikasi antara proses dari suatu sistem menjadi perhatian utama .

Pemrograman Berorientasi Objek vs. Pemrograman Berorientasi Proses

Dalam pemrograman berorientasi objek, struktur statis data dan fungsi adalah perhatian utama. Metode apa yang diperlukan untuk memanipulasi data terlampir, dan apa yang harus menjadi koneksi antara objek atau kelas. Dengan demikian, diagram kelas UML adalah contoh utama dari fokus ini, seperti yang terlihat pada Gambar 1.

Pemrograman berorientasi proses: Contoh diagram kelas UML

Dapat dicatat bahwa kritik umum dari pemrograman berorientasi objek adalah bahwa tidak ada aliran kontrol yang terlihat. Karena sistem terdiri dari sejumlah besar kelas/objek yang didefinisikan secara terpisah, mungkin sulit bagi orang yang kurang berpengalaman untuk memvisualisasikan aliran kontrol suatu sistem. Hal ini terutama berlaku untuk sistem dengan banyak pewarisan, yang menggunakan antarmuka abstrak atau tidak memiliki pengetikan yang kuat. Dalam kebanyakan kasus, menjadi penting bagi pengembang untuk menghafal sejumlah besar struktur sistem agar efektif (kelas apa yang memiliki metode apa dan yang digunakan dengan cara apa).

Kekuatan pendekatan pengembangan berorientasi objek adalah bahwa sistem dapat diperluas untuk mendukung jenis objek baru dengan dampak terbatas pada kode yang ada, selama jenis objek baru sesuai dengan harapan kode yang ada.

Pemrograman Fungsional vs. Berorientasi Proses

Banyak bahasa pemrograman fungsional menangani konkurensi dengan berbagai cara, tetapi fokus utamanya adalah data yang tidak dapat diubah yang lewat di antara fungsi, atau pembuatan fungsi dari fungsi lain (fungsi tingkat tinggi yang menghasilkan fungsi). Untuk sebagian besar, fokus bahasa masih satu ruang alamat atau executable, dan komunikasi antara executable tersebut ditangani dengan cara khusus sistem operasi.

Misalnya, Scala adalah bahasa fungsional yang dibangun di atas Java Virtual Machine. Meskipun dapat mengakses fasilitas Java untuk komunikasi, itu bukan bagian yang melekat pada bahasa. Meskipun ini adalah bahasa umum yang digunakan dalam pemrograman Spark, sekali lagi ini adalah pustaka yang digunakan bersama dengan bahasa tersebut.

Kekuatan paradigma fungsional adalah kemampuan untuk memvisualisasikan aliran kontrol sistem yang diberikan fungsi tingkat atas. Alur kontrol secara eksplisit di mana setiap fungsi memanggil fungsi lain, dan meneruskan semua data dari satu ke yang berikutnya. Dalam paradigma fungsional tidak ada efek samping, yang membuat penentuan masalah lebih mudah. Tantangan dengan sistem fungsional murni adalah bahwa "efek samping" diperlukan untuk memiliki keadaan yang persisten. Dalam sistem yang dirancang dengan baik, status yang bertahan ditangani di tingkat atas aliran kontrol, memungkinkan sebagian besar sistem menjadi bebas efek samping.

Elixir/OTP dan Pemrograman Berorientasi Proses

Dalam Elixir/Erlang dan OTP, komunikasi primitif adalah bagian dari mesin virtual yang mengeksekusi bahasa. Kemampuan untuk berkomunikasi antar proses dan antar mesin dibangun di dalam dan merupakan pusat dari sistem bahasa. Ini menekankan pentingnya komunikasi dalam paradigma ini dan dalam sistem bahasa ini.

Sementara bahasa Elixir sebagian besar fungsional dalam hal logika yang diungkapkan dalam bahasa, penggunaannya berorientasi pada proses .

Apa Artinya Berorientasi Proses?

Berorientasi proses seperti yang didefinisikan dalam posting ini adalah merancang sistem terlebih dahulu dalam bentuk proses apa yang ada dan bagaimana mereka berkomunikasi. Salah satu pertanyaan utama adalah proses mana yang statis, dan mana yang dinamis, yang muncul berdasarkan permintaan ke permintaan, yang melayani tujuan jangka panjang, yang memegang status bersama atau bagian dari status bersama sistem, dan fitur mana dari sistem secara inheren bersamaan. Sama seperti OO memiliki jenis objek, dan fungsional memiliki jenis fungsi, pemrograman berorientasi proses memiliki jenis proses.

Dengan demikian, desain berorientasi proses adalah identifikasi set jenis proses yang diperlukan untuk memecahkan masalah atau memenuhi kebutuhan .

Aspek waktu masuk dengan cepat ke dalam upaya desain dan persyaratan. Apa siklus hidup sistem? Kebutuhan adat apa yang kadang-kadang dan mana yang konstan? Di mana beban dalam sistem dan berapa kecepatan dan volume yang diharapkan? Hanya setelah jenis pertimbangan ini dipahami bahwa desain berorientasi proses mulai mendefinisikan fungsi setiap proses atau logika yang akan dieksekusi.

Implikasi Pelatihan

Implikasi dari kategorisasi ini untuk pelatihan adalah bahwa pelatihan harus dimulai bukan dengan sintaks bahasa atau contoh "Hello World", tetapi dengan pemikiran rekayasa sistem dan fokus desain pada alokasi proses .

Masalah pengkodean adalah hal sekunder untuk desain dan alokasi proses yang paling baik ditangani di tingkat yang lebih tinggi, dan melibatkan pemikiran lintas fungsi tentang siklus hidup, QA, DevOps, dan persyaratan bisnis pelanggan. Kursus pelatihan apa pun di Elixir atau Erlang harus (dan umumnya memang demikian) menyertakan OTP, dan harus memiliki orientasi proses sejak awal, bukan sebagai pendekatan tipe "Sekarang Anda dapat membuat kode di Elixir, jadi mari kita lakukan konkurensi".

Implikasi Adopsi

Implikasi untuk adopsi adalah bahwa bahasa dan sistem lebih baik diterapkan pada masalah yang membutuhkan komunikasi dan/atau distribusi komputasi. Masalah beban kerja tunggal pada satu komputer kurang menarik di ruang ini, dan mungkin lebih baik ditangani dengan bahasa lain. Sistem pemrosesan berkelanjutan yang berumur panjang adalah target utama untuk bahasa ini karena memiliki toleransi kesalahan yang dibangun dari bawah ke atas.

Untuk dokumentasi dan pekerjaan desain, penggunaan notasi grafis akan sangat membantu (seperti gambar 1 untuk bahasa OO). Saran untuk Elixir dan pemrograman berorientasi proses dari UML adalah diagram urutan (contoh pada gambar 2) untuk menunjukkan hubungan temporal antara proses dan mengidentifikasi proses mana yang terlibat dalam melayani permintaan. Tidak ada tipe diagram UML untuk menangkap siklus hidup dan struktur proses, tetapi dapat direpresentasikan dengan diagram kotak dan panah sederhana untuk tipe proses dan hubungannya. Misalnya, Gambar 3:

Diagram urutan UML sampel pemrograman berorientasi proses

Diagram struktur sampel proses pemrograman berorientasi proses

Contoh Orientasi Proses

Terakhir, kita akan membahas contoh singkat penerapan orientasi proses pada suatu masalah. Misalkan kita ditugaskan untuk menyediakan sistem yang mendukung pemilu global. Masalah ini dipilih karena banyak aktivitas individu dilakukan secara berurutan, tetapi agregasi atau ringkasan hasil diinginkan secara real time dan mungkin melihat beban yang signifikan.

Desain dan Alokasi Proses Awal

Kita awalnya dapat melihat bahwa pemberian suara oleh masing-masing individu adalah ledakan lalu lintas ke sistem dari banyak input diskrit, tidak diatur waktu, dan dapat memiliki beban tinggi. Untuk mendukung kegiatan ini, kami ingin sejumlah besar proses mengumpulkan semua input ini dan meneruskannya ke proses tabulasi yang lebih sentral. Proses ini dapat ditempatkan di dekat populasi di setiap negara yang akan menghasilkan suara, dan dengan demikian memberikan latensi yang rendah. Mereka akan mempertahankan hasil lokal, mencatat input mereka segera, dan meneruskannya untuk tabulasi dalam batch untuk mengurangi bandwidth dan overhead.

Kami awalnya dapat melihat bahwa perlu ada proses yang melacak suara di setiap yurisdiksi di mana hasil harus disajikan. Mari kita asumsikan untuk contoh ini bahwa kita perlu melacak hasil untuk setiap negara, dan di setiap negara berdasarkan provinsi/negara bagian. Untuk mendukung aktivitas ini, kami ingin setidaknya satu proses per negara melakukan penghitungan, dan mempertahankan total saat ini, dan set lainnya untuk setiap negara bagian/provinsi di setiap negara. Ini mengasumsikan kita harus dapat menjawab total untuk negara dan negara bagian/provinsi secara real time atau latensi rendah. Jika hasil dapat diperoleh dari sistem database, kita mungkin memilih alokasi proses yang berbeda di mana total diperbarui oleh proses sementara. Keuntungan menggunakan proses khusus untuk perhitungan ini adalah bahwa hasilnya terjadi pada kecepatan memori dan dapat diperoleh dengan latensi rendah.

Akhirnya, kita dapat melihat bahwa banyak orang akan melihat hasilnya. Proses-proses ini dapat dipartisi dengan banyak cara. Kami mungkin ingin mendistribusikan beban dengan menempatkan proses di setiap negara yang bertanggung jawab atas hasil negara tersebut. Proses dapat men-cache hasil dari proses komputasi untuk mengurangi beban kueri pada proses komputasi, dan/atau proses komputasi dapat mendorong hasilnya ke proses hasil yang tepat secara berkala, ketika hasil berubah dalam jumlah yang signifikan, atau pada saat proses komputasi menjadi idle yang menunjukkan tingkat perubahan yang melambat.

Dalam ketiga jenis proses, kita dapat menskalakan proses secara independen satu sama lain, mendistribusikannya secara geografis, dan memastikan hasil tidak pernah hilang melalui pengakuan aktif transfer data antar proses.

Seperti yang telah dibahas, kita telah memulai contoh dengan desain proses yang independen dari logika bisnis di setiap proses. Dalam kasus di mana logika bisnis memiliki persyaratan khusus untuk agregasi data atau geografi yang dapat memengaruhi alokasi proses secara berulang. Desain proses kami sejauh ini ditunjukkan pada Gambar 4.

Contoh pengembangan berorientasi proses: Desain proses awal

Penggunaan proses terpisah untuk menerima suara memungkinkan setiap suara diterima secara independen dari suara lainnya, dicatat setelah diterima, dan digabungkan ke rangkaian proses berikutnya, mengurangi beban pada sistem tersebut secara signifikan. Untuk sistem yang mengkonsumsi sejumlah besar data, mengurangi volume data dengan menggunakan lapisan proses adalah pola yang umum dan berguna.

Dengan melakukan komputasi dalam serangkaian proses yang terisolasi, kami dapat mengelola beban pada proses tersebut dan memastikan stabilitas dan kebutuhan sumber dayanya.

Dengan menempatkan presentasi hasil dalam rangkaian proses yang terisolasi, kami mengontrol beban ke seluruh sistem dan memungkinkan rangkaian proses diskalakan secara dinamis untuk beban.

Persyaratan tambahan

Sekarang, mari tambahkan beberapa persyaratan yang rumit. Anggaplah bahwa di setiap yurisdiksi (negara atau negara bagian), tabulasi suara dapat menghasilkan hasil yang proporsional, hasil pemenang-mengambil-semua, atau tidak ada hasil jika suara yang diberikan tidak mencukupi relatif terhadap populasi yurisdiksi itu. Setiap yurisdiksi memiliki kendali atas aspek-aspek ini. Dengan perubahan ini, maka hasil negara bukan merupakan agregasi sederhana dari hasil raw vote, tetapi merupakan agregasi dari hasil negara bagian/provinsi. Ini mengubah alokasi proses dari aslinya menjadi mengharuskan hasil dari proses negara bagian/provinsi dimasukkan ke dalam proses negara. Jika protokol yang digunakan antara pengumpulan suara dan proses negara bagian/provinsi dan provinsi ke negara adalah sama, maka logika agregasi dapat digunakan kembali, tetapi proses berbeda yang menyimpan hasil diperlukan dan jalur komunikasinya berbeda, seperti yang ditunjukkan pada Gambar 5.

Contoh pengembangan berorientasi proses: Desain proses yang dimodifikasi

Kode

Untuk melengkapi contoh, kami akan meninjau implementasi contoh di Elixir OTP. Untuk menyederhanakan, contoh ini mengasumsikan server web seperti Phoenix digunakan untuk memproses permintaan web yang sebenarnya, dan layanan web tersebut membuat permintaan ke proses yang disebutkan di atas. Ini memiliki keuntungan menyederhanakan contoh dan menjaga fokus pada Elixir/OTP. Dalam sistem produksi, memiliki proses yang terpisah ini memiliki beberapa keuntungan serta memisahkan masalah, memungkinkan penerapan yang fleksibel, mendistribusikan beban, dan mengurangi latensi. Kode sumber lengkap dengan tes dapat ditemukan di https://github.com/technomage/voting. Sumbernya disingkat dalam posting ini agar mudah dibaca. Setiap proses di bawah ini cocok dengan pohon pengawasan OTP untuk memastikan bahwa proses dimulai ulang pada kegagalan. Lihat sumber untuk informasi lebih lanjut tentang aspek contoh ini.

Perekam Suara

Proses ini menerima suara, mencatatnya ke penyimpanan persisten, dan mengelompokkan hasilnya ke agregator. Modul VoteRecoder menggunakan Task.Supervisor untuk mengelola tugas singkat untuk merekam setiap suara.

 defmodule Voting.VoteRecorder do @moduledoc """ This module receives votes and sends them to the proper aggregator. This module uses supervised tasks to ensure that any failure is recovered from and the vote is not lost. """ @doc """ Start a task to track the submittal of a vote to an aggregator. This is a supervised task to ensure completion. """ def cast_vote where, who do Task.Supervisor.async_nolink(Voting.VoteTaskSupervisor, fn -> Voting.Aggregator.submit_vote where, who end) |> Task.await end end

Pengumpul Suara

Proses ini mengumpulkan suara dalam yurisdiksi, menghitung hasil untuk yurisdiksi itu, dan meneruskan ringkasan suara ke proses berikutnya yang lebih tinggi (yurisdiksi tingkat yang lebih tinggi, atau penyaji hasil).

 defmodule Voting.Aggregator do use GenStage ... @doc """ Submit a single vote to an aggregator """ def submit_vote id, candidate do pid = __MODULE__.via_tuple(id) :ok = GenStage.call pid, {:submit_vote, candidate} end @doc """ Respond to requests """ def handle_call {:submit_vote, candidate}, _from, state do n = state.votes[candidate] || 0 state = %{state | votes: Map.put(state.votes, candidate, n+1)} {:reply, :ok, [%{state.id => state.votes}], state} end @doc """ Handle events from subordinate aggregators """ def handle_events events, _from, state do votes = Enum.reduce events, state.votes, fn e, votes -> Enum.reduce e, votes, fn {k,v}, votes -> Map.put(votes, k, v) # replace any entries for subordinates end end # Any jurisdiction specific policy would go here # Sum the votes by candidate for the published event merged = Enum.reduce votes, %{}, fn {j, jv}, votes -> # Each jourisdiction is summed for each candidate Enum.reduce jv, votes, fn {candidate, tot}, votes -> Logger.debug "@@@@ Votes in #{inspect j} for #{inspect candidate}: #{inspect tot}" n = votes[candidate] || 0 Map.put(votes, candidate, n + tot) end end # Return the published event and the state which retains # Votes by jourisdiction {:noreply, [%{state.id => merged}], %{state | votes: votes}} end end

Hasil Presenter

Proses ini menerima suara dari agregator dan menyimpan hasil tersebut ke permintaan layanan untuk menyajikan hasil.

 defmodule Voting.ResultPresenter do use GenStage … @doc """ Handle requests for results """ def handle_call :get_votes, _from, state do {:reply, {:ok, state.votes}, [], state} end @doc """ Obtain the results from this presenter """ def get_votes id do pid = Voting.ResultPresenter.via_tuple(id) {:ok, votes} = GenStage.call pid, :get_votes votes end @doc """ Receive votes from aggregator """ def handle_events events, _from, state do Logger.debug "@@@@ Presenter received: #{inspect events}" votes = Enum.reduce events, state.votes, fn v, votes -> Enum.reduce v, votes, fn {k,v}, votes -> Map.put(votes, k, v) end end {:noreply, [], %{state | votes: votes}} end end

Bawa pulang

Posting ini mengeksplorasi Elixir/OTP dari potensinya sebagai bahasa berorientasi proses, membandingkannya dengan paradigma berorientasi objek dan fungsional, dan meninjau implikasinya terhadap pelatihan dan adopsi.

Posting ini juga menyertakan contoh singkat penerapan orientasi ini pada contoh masalah. Jika Anda ingin meninjau semua kode, berikut adalah tautan ke contoh kami di GitHub lagi, sehingga Anda tidak perlu menggulir ke belakang untuk mencarinya.

Kuncinya adalah melihat sistem sebagai kumpulan proses komunikasi. Rencanakan sistem dari sudut pandang desain proses terlebih dahulu, dan sudut pandang pengkodean logika kedua.