Mesin otomatis terhadap kode spageti


“Saya suka spageti barat, saya benci kode spageti”

"Kode spageti" adalah ungkapan yang ideal untuk menggambarkan perangkat lunak yang merupakan kekacauan dari sudut pandang kognitif dan estetika. Dalam artikel ini, saya akan berbicara tentang rencana tiga poin untuk menghancurkan kode spageti:

  • Kami membahas mengapa kode spageti tidak begitu enak.
  • Memperkenalkan tampilan baru pada apa kode sebenarnya.
  • Kami sedang mendiskusikan Frame Machine Notation (FMN) , yang membantu pengembang mengurai pasta.

Kita semua tahu betapa sulitnya membaca kode orang lain. Ini mungkin disebabkan oleh kenyataan bahwa tugas itu sendiri sulit atau karena struktur kode terlalu ... "kreatif". Seringkali kedua masalah ini berjalan beriringan.

Tantangan adalah tugas yang sulit, dan biasanya hanya penemuan revolusioner yang dapat menyederhanakannya. Namun, itu terjadi bahwa struktur perangkat lunak itu sendiri menambah kompleksitas yang tidak perlu, dan masalah ini patut dipecahkan.

Keburukan kode spageti terletak pada logika kondisional yang kompleks. Dan meskipun hidup bisa sulit untuk dibayangkan tanpa banyak konstruksi rumit jika-maka-lain, artikel ini akan menunjukkan kepada Anda solusi yang lebih baik.


Untuk mengilustrasikan situasi dengan kode spaghetti, kita perlu terlebih dahulu mengubah ini:


Pasta renyah

Dalam hal ini:


Al dente!

Ayo mulai memasak.

Keadaan tersirat


Untuk membuat pasta, kita pasti membutuhkan air untuk memasak. Namun, bahkan elemen sederhana yang melibatkan kode spaghetti bisa sangat membingungkan.

Ini adalah contoh sederhana:

(temp < 32) 

Apa yang sebenarnya dilakukan pemeriksaan ini? Jelas, itu membagi garis bilangan menjadi dua bagian, tetapi apa artinya bagian-bagian ini? Saya pikir Anda dapat membuat asumsi logis, tetapi masalahnya adalah bahwa kode tersebut tidak benar-benar berkomunikasi secara eksplisit .

Jika saya benar-benar mengkonfirmasi bahwa dia memeriksa apakah airnya SOLID [kira-kira. jalur: menurut skala Fahrenheit, air membeku pada +32 derajat] , apa yang secara logis berarti pengembalian salah?

 if (temp < 32) { // SOLID water } else { // not SOLID water. is (LIQUID | GAS) } 

Meskipun cek membagi angka menjadi dua kelompok, sebenarnya ada tiga keadaan logis - padat, cair dan gas (SOLID, LIQUID, GAS)!

Yaitu, garis angka ini:


dibagi dengan kondisi periksa sebagai berikut:

 if (temp < 32) { 


 } else { 


 } 

Perhatikan apa yang terjadi karena sangat penting untuk memahami sifat kode spageti. Pemeriksaan Boolean membagi ruang angka menjadi dua bagian, tetapi TIDAK mengkategorikan sistem sebagai struktur logis nyata dari (SOLID, LIQUID, GAS). Sebagai gantinya, cek membagi ruang menjadi (SOLID, yang lainnya).

Ini cek serupa:

 if (temp > 212) { // GAS water } else { // not GAS water. is (SOLID | LIQUID) } 

Secara visual, akan terlihat seperti ini:

 if (temp > 212) { 


 } else { 


 } 

Perhatikan bahwa:

  1. set lengkap negara yang mungkin tidak diumumkan di mana pun
  2. tidak ada dalam konstruksi bersyarat yang menyatakan negara logis atau kelompok negara dinyatakan
  3. beberapa negara secara tidak langsung dikelompokkan berdasarkan struktur logika kondisional dan percabangan

Kode seperti itu rapuh, tetapi sangat umum, dan tidak terlalu besar untuk menyebabkan masalah dengan dukungannya. Jadi mari kita buat situasinya lebih buruk.


Saya tidak pernah menyukai kode Anda

Kode yang ditunjukkan di atas menyiratkan adanya tiga keadaan materi - SOLID, LIQUID, GAS. Namun, menurut data ilmiah, pada kenyataannya, ada empat keadaan yang dapat diamati di mana plasma (PLASMA) dimasukkan (sebenarnya, ada banyak yang lain, tetapi ini akan cukup bagi kita). Meskipun tidak ada yang menyiapkan pasta dari plasma, jika kode ini diterbitkan di Github, dan kemudian beberapa mahasiswa pascasarjana yang mempelajari fisika energi tinggi akan membayarnya, kita juga harus mempertahankan keadaan ini.

Namun, ketika plasma ditambahkan, kode yang ditunjukkan di atas secara naif akan melakukan hal berikut:

 if (temp < 32) { // SOLID water } else { // not SOLID water. is (LIQUID | GAS) + (PLASMA?) // how did PLASMA get in here?? } if (temp > 212) { // GAS water + (PLASMA) // again with the PLASMA!! } else { // not GAS water. is (SOLID | LIQUID) } 

Sangat mungkin bahwa kode lama, ketika ditambahkan ke banyak negara plasma, akan pecah di cabang lain. Sayangnya, tidak ada dalam struktur kode yang membantu melaporkan keberadaan negara baru atau memengaruhi perubahan. Selain itu, bug apa pun cenderung tidak mencolok, yaitu, menemukan mereka akan menjadi yang paling sulit. Katakan saja tidak pada serangga di spageti.

Singkatnya, masalahnya adalah ini: Pemeriksaan Boolean digunakan untuk menentukan status secara tidak langsung . Keadaan logis sering tidak dinyatakan dan tidak terlihat dalam kode. Seperti yang kita lihat di atas, ketika sistem menambahkan keadaan logis baru, kode yang ada dapat rusak. Untuk menghindari hal ini, pengembang harus memeriksa ulang setiap pemeriksaan bersyarat dan cabang individu untuk memastikan bahwa jalur kode masih valid untuk semua keadaan logis mereka! Ini adalah alasan utama degradasi fragmen kode besar karena menjadi lebih kompleks.

Meskipun tidak ada cara untuk sepenuhnya menyingkirkan pemeriksaan data bersyarat, teknik apa pun yang meminimalkannya akan mengurangi kompleksitas kode.

Sekarang mari kita lihat implementasi kelas berorientasi objek yang khas yang menciptakan model volume air yang sangat sederhana. Kelas akan mengelola perubahan dalam keadaan substansi air. Setelah mempelajari masalah solusi klasik untuk masalah ini, kami kemudian membahas notasi baru bernama Frame dan menunjukkan bagaimana hal itu dapat mengatasi kesulitan yang telah kami temukan.

Pertama-tama didihkan airnya ...


Ilmu pengetahuan memberi nama pada semua transisi yang mungkin dilakukan suatu zat ketika suhu berubah.


Kelas kami sangat sederhana (dan tidak terlalu berguna). Ini menjawab tantangan melakukan transisi antara negara dan mengubah suhu sampai menjadi cocok untuk negara target yang diinginkan:

(Catatan: Saya menulis pseudo-code ini. Gunakan dalam pekerjaan Anda hanya dengan risiko dan risiko Anda sendiri.)

 class WaterSample { temp:int Water(temp:int) { this.temp = temp } // gas -> solid func depose() { // If not in GAS state, throw an error if (temp < WATER_GAS_TEMP) throw new IllegalStateError() // do depose while (temp > WATER_SOLID_TEMP) decreaseTemp(1) } // gas -> liquid func condense() { // If not in GAS state, throw an error if (temp < WATER_GAS_TEMP) throw new IllegalStateError() // do condense while (temp > WATER_GAS_TEMP) decreaseTemp(1) } // liquid -> gas func vaporize() { // If not in LIQUID state, throw an error if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() // do vaporize while (temp < WATER_GAS_TEMP) increaseTemp(1) } // liquid -> solid func freeze() { // If not in LIQUID state, throw an error if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() // do freeze while (temp > WATER_SOLID_TEMP) decreaseTemp(1) } // solid -> liquid func melt() { // If not in SOLID state, throw an error if (temp > WATER_SOLID_TEMP) throw new IllegalStateError() // do melt while (temp < WATER_SOLID_TEMP) increaseTemp(1) } // solid -> gas func sublimate() { // If not in SOLID state, throw an error if (temp > WATER_SOLID_TEMP) throw new IllegalStateError() // do sublimate while (temp < WATER_GAS_TEMP) increaseTemp(1) } func getState():string { if (temp < WATER_SOLID_TEMP) return "SOLID" if (temp > WATER_GAS_TEMP) return "GAS" return "LIQUID" } } 

Dibandingkan dengan contoh pertama, kode ini memiliki peningkatan tertentu. Pertama, angka "ajaib" hard-code (32, 212) digantikan oleh konstanta batas suhu negara (WATER_ LAPORAN_TEMP, WATER_GAS_TEMP). Perubahan ini mulai membuat negara lebih eksplisit, meskipun secara tidak langsung.

Pemeriksaan untuk "pemrograman defensif" juga muncul dalam kode ini, yang membatasi pemanggilan metode jika ia dalam kondisi tidak cocok untuk operasi. Misalnya, air tidak dapat membeku jika bukan cairan - ini melanggar hukum (dari alam). Tetapi menambahkan kondisi anjing penjaga membuat memahami tujuan kode lebih sulit. Sebagai contoh:

 // liquid -> solid if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() 

Pemeriksaan bersyarat ini melakukan hal berikut:

  1. Periksa apakah temp lebih rendah dari suhu batas GAS
  2. Cek apakah temp melebihi suhu batas SOLID
  3. Mengembalikan kesalahan jika salah satu dari pemeriksaan ini tidak benar

Logika ini membingungkan. Pertama, berada dalam keadaan cair ditentukan oleh apa yang bukan zat - padat atau gas.

 (temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // is liquid? 

Kedua, kode memeriksa untuk melihat apakah air itu cair untuk mengetahui apakah kesalahan perlu dikembalikan.

 !(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // Seriously? 

Pertama kali memahami negasi ganda negara ini tidaklah mudah. Berikut adalah penyederhanaan yang sedikit mengurangi kompleksitas ekspresi:

 bool isLiquidWater = (temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) if (!isLiquidWater) throw new IllegalStateError() 

Kode ini lebih mudah dipahami karena kondisi isLiquidWater eksplisit .

Sekarang kami sedang mengeksplorasi teknik yang memperbaiki keadaan eksplisit sebagai cara terbaik untuk menyelesaikan masalah. Dengan pendekatan ini, keadaan logis sistem menjadi struktur fisik perangkat lunak, yang meningkatkan kode dan menyederhanakan pemahamannya.

Notasi mesin bingkai


Frame Machine Notation (FMN) adalah bahasa khusus domain (Domain Specific Language, DSL) yang mendefinisikan pendekatan kategorikal, metodologis, dan sederhana untuk mendefinisikan dan mengimplementasikan berbagai jenis mesin . Untuk kesederhanaan, saya akan memanggil Frame automata hanya "mesin," karena notasi ini dapat menentukan kriteria teoritis untuk berbagai jenis (mesin negara, toko automata, dan evolusi teratas automata - mesin Turing). Untuk mengetahui tentang berbagai jenis mesin dan aplikasinya, saya sarankan untuk mempelajari halaman di Wikipedia .

Meskipun teori automata mungkin menarik (pernyataan yang SANGAT meragukan), dalam artikel ini kita akan fokus pada aplikasi praktis dari konsep-konsep kuat ini untuk membangun sistem dan menulis kode.

Untuk mengatasi masalah ini, Frame memperkenalkan notasi standar yang berfungsi pada tiga level terintegrasi:

  1. Teks DSL untuk mendefinisikan pengontrol Frame dengan sintaks yang elegan dan ringkas
  2. Satu set pola pengkodean referensi untuk menerapkan kelas berorientasi objek dalam bentuk mesin yang Frame sebut "pengontrol"
  3. Notasi visual di mana FMN digunakan untuk mengekspresikan operasi kompleks yang sulit untuk diwakili secara grafis - Frame Visual Notation (FVN)

Pada artikel ini, saya akan mempertimbangkan dua poin pertama: FMN dan pola referensi, dan saya akan meninggalkan diskusi FVN untuk artikel selanjutnya.

Frame adalah notasi yang memiliki beberapa aspek penting:

  1. FMN memiliki objek tingkat pertama yang terkait dengan konsep automata, yang tidak tersedia dalam bahasa berorientasi objek.
  2. Spesifikasi FMN mendefinisikan pola implementasi standar dalam kode semu yang menunjukkan bagaimana notasi FMN dapat diimplementasikan.
  3. FMN akan segera dapat mengkompilasi (bekerja dalam proses) dalam bahasa berorientasi objek apa pun

Catatan: implementasi referensi digunakan untuk menunjukkan kesetaraan absolut dari notasi FMN dan cara sederhana untuk mengimplementasikannya dalam bahasa berorientasi objek apa pun. Anda dapat memilih metode apa pun.

Sekarang saya akan memperkenalkan Anda ke dua objek tingkat pertama yang paling penting dalam Frame - Frame Events dan Frame Controllers .

Bingkai acara


FrameEvents adalah bagian integral dari kesederhanaan notasi FMN. FrameEvent diimplementasikan sebagai struktur atau kelas yang setidaknya memiliki variabel anggota berikut:

  • id pesan
  • kamus atau daftar parameter
  • mengembalikan objek

Inilah pseudocode dari kelas FrameEvent:

 class FrameEvent { var _msg:String var _params:Object var _return:Object FrameEvent(msg:String, params:Object = null) { _msg = msg _params = params } } 

Notasi bingkai menggunakan simbol @ , yang mengidentifikasi objek FrameEvent. Setiap atribut FrameEvent yang diperlukan memiliki token khusus untuk mengaksesnya:

 @|message| :  -    _msg @[param1] :  []      @^ :              _return 

Seringkali kita tidak perlu menentukan dengan apa FrameEvent bekerja. Karena sebagian besar konteks bekerja dengan hanya satu FrameEvent pada suatu waktu, notasi pasti dapat disederhanakan sehingga hanya menggunakan pemilih atribut. Karena itu, kami dapat menyederhanakan akses:

 |buttonClick| // Select for a "buttonClick" event _msg [firstName] = "Mark" // Set firstName _params property to "Mark" ^ = "YES" // Set the _return object to "YES" 

Notasi seperti itu mungkin tampak aneh pada awalnya, tetapi segera kita akan melihat bagaimana sintaksis yang sederhana untuk peristiwa sangat menyederhanakan pemahaman kode FMN.

Pengontrol bingkai


Frame Controller adalah kelas berorientasi objek, yang dipesan dengan cara yang jelas untuk mengimplementasikan mesin Frame. Jenis pengontrol diidentifikasi oleh awalan # :

 #MyController 

ini setara dengan pseudocode berorientasi objek berikut:

 class MyController {} 

Jelas kelas ini tidak terlalu berguna. Agar ia dapat melakukan sesuatu, pengontrol memerlukan setidaknya satu keadaan untuk merespons peristiwa.

Pengontrol disusun sedemikian rupa sehingga berisi blok berbagai jenis, yang diidentifikasi oleh tanda hubung di sekitar nama jenis blok:

 #MyController<br> -block 1- -block 2- -block 3- 

Kontroler dapat memiliki tidak lebih dari satu instance dari setiap blok, dan tipe blok hanya dapat berisi tipe subkomponen tertentu. Dalam artikel ini, kami hanya memeriksa blok -machine , yang hanya dapat berisi status. Negara diidentifikasi oleh token $ awalan.

Di sini kita melihat FMN untuk pengontrol yang berisi mesin dengan hanya satu keadaan:

 #MyController // controller declaration -machine- // machine block $S1 // state declaration 

Berikut ini adalah implementasi kode FMN di atas:

 class MyController { // -machine- var _state(e:FrameEvent) = S1 // initialize state variable // to $S1 func S1(e:FrameEvent) { // state $S1 does nothing } } 

Implementasi blok mesin terdiri dari elemen-elemen berikut:

  1. variabel _state , yang mengacu pada fungsi dari status saat ini. Ini diinisialisasi dengan fungsi status pertama di controller.
  2. satu atau lebih metode negara

Metode Frame state didefinisikan sebagai fungsi dengan tanda tangan berikut:

 func MyState(e:FrameEvent); 

Setelah mendefinisikan dasar-dasar implementasi blok mesin ini, kita dapat melihat seberapa baik FrameEvent berinteraksi dengan mesin.

Unit antarmuka


Interaksi FrameEvents yang mengontrol operasi mesin adalah inti dari kesederhanaan dan kekuatan notasi Frame. Namun, kami belum menjawab pertanyaan, dari mana FrameEvents berasal - bagaimana mereka masuk ke controller untuk mengendalikannya? Satu opsi: klien eksternal sendiri dapat membuat dan menginisialisasi FrameEvents, dan kemudian langsung memanggil metode yang ditunjukkan oleh variabel anggota _state:

 myController._state(new FrameEvent("buttonClick")) 

Alternatif yang jauh lebih baik adalah membuat antarmuka umum yang membungkus panggilan langsung ke variabel anggota _state:

 myController.sendEvent(new FrameEvent("buttonClick")) 

Namun, cara yang paling mudah, sesuai dengan cara biasa membuat perangkat lunak berorientasi objek, adalah menciptakan metode umum yang mengirim acara atas nama klien ke mesin internal:

 class MyController { func buttonClick() { FrameEvent e = new FrameEvent("buttonClick") _state(e) return e._return } } 

Frame mendefinisikan sintaks untuk blok antarmuka yang berisi metode yang mengubah panggilan menjadi antarmuka umum untuk FrameEvents.

 #MyController -interface- buttonClick ... 

Blok interface memiliki banyak fitur lain, tetapi contoh ini memberi kita gambaran umum tentang cara kerjanya. Saya akan memberikan penjelasan lebih lanjut dalam artikel seri berikut.

Sekarang mari kita terus mempelajari operasi otomat Frame.

Penangan acara


Meskipun kami telah menunjukkan cara mendefinisikan mobil, kami belum memiliki notasi untuk melakukan apa pun. Untuk memproses peristiwa, kita perlu 1) untuk dapat memilih acara yang perlu diproses dan 2) untuk melampirkannya ke perilaku yang sedang dilakukan.

Berikut adalah pengontrol Frame sederhana yang menyediakan infrastruktur untuk menangani acara:

 #MyController // controller declaration -machine- // machine block $S1 // state declaration |e1| ^ // e1 event handler and return 

Seperti yang dinyatakan di atas, untuk mengakses atribut _msg dari acara _msg , notasi FMN menggunakan tanda kurung dari garis vertikal:

 |messageName| 

FMN juga menggunakan token eksponen yang mewakili pernyataan pengembalian. Kontroler yang ditunjukkan di atas akan diimplementasikan sebagai berikut:

 class MyController { // #MyController // -machine- var _state(e:FrameEvent) = S1 func S1(e:FrameEvent) { // $S1 if (e._msg == "e1") { // |e1| return // ^ } } } 

Di sini kita melihat betapa jelas notasi FMN sesuai dengan pola implementasi yang mudah dipahami dan dikodekan.

Setelah mengatur aspek-aspek dasar peristiwa, pengontrol, mesin, status, dan penangan peristiwa, kita dapat melanjutkan untuk memecahkan masalah nyata dengan bantuan mereka.

Mesin fokus tunggal


Di atas kami melihat pengontrol stateless yang sangat tidak berguna.

 #MyController 

Satu langkah lebih tinggi dalam rantai makanan utilitas adalah kelas dengan satu negara, yang, meskipun tidak sia-sia, hanya membosankan. Tapi setidaknya dia setidaknya melakukan sesuatu .

Pertama, mari kita lihat bagaimana kelas dengan hanya satu negara (tersirat) akan diimplementasikan:

 class Mono { String status() { return "OFF" } } 

Tidak ada status yang dideklarasikan atau bahkan tersirat di sini, tetapi mari kita asumsikan bahwa jika kode melakukan sesuatu, sistem dalam keadaan "Bekerja".

Kami juga akan memperkenalkan ide penting: panggilan antarmuka akan dianggap sama dengan mengirim acara ke objek. Oleh karena itu, kode di atas dapat dianggap sebagai metode transmisi | status | kelas Mono, selalu dalam status $ Working.

Situasi ini dapat divisualisasikan menggunakan tabel acara pengikatan:


Sekarang mari kita lihat FMN, yang menunjukkan fungsi yang sama dan cocok dengan tabel penjilidan yang sama:

 #Mono -machine- $Working |status| ^("OFF") 

Begini tampilannya:

 class Mono { // #Mono // -machine- var _state(e:FrameEvent) = Working // initialize start state func Working(e:FrameEvent) { // $Working if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } } 

Anda dapat melihat bahwa kami juga memperkenalkan notasi baru untuk pernyataan pengembalian , yang berarti mengevaluasi ekspresi dan mengembalikan hasilnya ke antarmuka:

 ^(return_expr) 

Operator ini setara

 @^ = return_expr 

atau adil

 ^ = return_expr 

Semua operator ini setara secara fungsional dan Anda dapat menggunakan salah satu dari mereka, tetapi ^(return_expr) terlihat paling ekspresif.

Nyalakan kompor


Sejauh ini kita telah melihat controller dengan 0 state dan controller dengan 1 state. Mereka belum sangat berguna, tetapi kita sudah di ambang sesuatu yang menarik.

Untuk memasak pasta kami, pertama-tama Anda harus menyalakan kompor. Berikut ini adalah kelas Switch sederhana dengan variabel boolean tunggal:

 class Switch { boolean _isOn; func status() { if (_isOn) { return "ON"; } else { return "OFF"; } } } 

Meskipun pada pandangan pertama ini tidak jelas, kode yang ditunjukkan di atas mengimplementasikan tabel pengikatan acara berikut:


Sebagai perbandingan, ini adalah FMN untuk perilaku yang sama:

 #Switch1 -machine- $Off |status| ^("OFF") $On |status| ^("ON") 

Sekarang kita melihat bagaimana tepatnya notasi Frame cocok dengan tujuan kode kita - melampirkan peristiwa (pemanggilan metode) ke perilaku berdasarkan pada keadaan di mana controller berada. Selain itu, struktur implementasi juga sesuai dengan tabel penjilidan:

 class Switch1 { // #Switch1 // -machine- var _state(e:FrameEvent) = Off func Off(e:FrameEvent) { // $Off if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } func On(e:FrameEvent) { // $On if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } } 

Tabel ini memungkinkan Anda untuk dengan cepat memahami tujuan pengontrol di berbagai statusnya. Struktur notasi Frame dan pola implementasi keduanya memiliki keunggulan yang sama.

Namun, sakelar kami memiliki masalah fungsional yang nyata. Ini diinisialisasi dalam status $ Tidak Aktif, tetapi tidak dapat beralih ke status $ Aktif! Untuk melakukan ini, kita perlu memasukkan operator perubahan status .

Ubah status


Pernyataan perubahan negara adalah sebagai berikut:

 ->> $NewState 

Sekarang kita bisa menggunakan operator ini untuk beralih antara $ Off dan $ On:

 #Switch2 -machine- $Off |toggle| ->> $On ^ |status| ^("OFF") $On |toggle| ->> $Off ^ |status| ^("ON") 

Dan di sini adalah tabel pengikatan acara yang sesuai:


Acara Baru | beralih | sekarang memicu perubahan yang cukup siklus melalui dua negara. Bagaimana operasi perubahan negara dapat diimplementasikan?

Tidak ada tempat yang lebih mudah. Berikut ini adalah implementasi Switch2:

 class Switch2 { // #Switch2 // -machine- var _state(e:FrameEvent) = Off func Off(e:FrameEvent) { if (e._msg == "toggle") { // |toggle| _state = On // ->> $On return // ^ } if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } func On(e:FrameEvent) { if (e._msg == "toggle") { // |toggle| _state = Off // ->> $Off return // ^("OFF") } if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } } 

Anda juga dapat membuat peningkatan terakhir di Switch2 sehingga tidak hanya memungkinkan Anda untuk beralih antar negara, tetapi juga secara eksplisit mengatur negara:

 #Switch3 -machine- $Off |turnOn| ->> $On ^ |toggle| ->> $On ^ |status| ^("OFF") $On |turnOff| ->> $Off ^ |toggle| ->> $Off ^ |status| ^("ON") 

Berbeda dengan | toggle | event, if | turnOn | ditransmisikan ketika Switch3 sudah aktif atau | turnOff | ketika sudah mati, pesan diabaikan dan tidak ada yang terjadi. Peningkatan kecil ini memberi klien kemampuan untuk secara eksplisit menunjukkan keadaan di mana saklar harus:

 class Switch3 { // #Switch3 // -machine- var _state(e:FrameEvent) = Off /********************************** $Off |turnOn| ->> $On ^ |toggle| ->> $On ^ |status| ^("OFF") ***********************************/ func Off(e:FrameEvent) { if (e._msg == "turnOn") { // |turnOn| _state = On // ->> $On return // ^ } if (e._msg == "toggle") { // |toggle| _state = On // ->> $On return // ^ } if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } /********************************** $On |turnOff| ->> $Off ^ |toggle| ->> $Off ^ |status| ^("ON") ***********************************/ func On(e:FrameEvent) { if (e._msg == "turnOff") { // |turnOff| _state = Off // ->> $Off return // ^ } if (e._msg == "toggle") { // |toggle| _state = Off // ->> $Off return // ^ } if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } } 

Langkah terakhir dalam evolusi saklar kami menunjukkan betapa mudahnya memahami tujuan pengontrol FMN. Kode yang relevan menunjukkan betapa mudahnya mengimplementasikan menggunakan mekanisme Frame.

Setelah menciptakan mesin Switch, kita dapat menyalakan api dan mulai memasak!

Keadaan suara


Kunci, walaupun halus, aspek automata adalah keadaan mesin saat ini adalah hasil dari situasi (misalnya, menghidupkan) atau semacam analisis data atau lingkungan. Ketika mesin telah beralih ke kondisi yang diinginkan, itu tersirat. bahwa situasinya tidak akan berubah tanpa sepengetahuan mobil.

Namun, anggapan ini tidak selalu benar. Dalam beberapa situasi, verifikasi (atau "penginderaan") data diperlukan untuk menentukan keadaan logis saat ini:

  1. kondisi awal yang dipulihkan - saat mesin dipulihkan dari kondisi konstan
  2. keadaan eksternal - mendefinisikan "situasi aktual" yang ada di lingkungan pada saat pembuatan, pemulihan atau pengoperasian mesin
  3. keadaan internal yang mudah berubah - ketika bagian dari data internal yang dikelola oleh mesin yang sedang berjalan dapat berubah di luar kendali mesin

Dalam semua kasus ini, data, lingkungan, atau keduanya harus "diselidiki" untuk menentukan situasi dan mengatur keadaan mesin yang sesuai. Idealnya, logika Boolean ini dapat diimplementasikan dalam satu fungsi yang mendefinisikan keadaan logis yang benar. Untuk mendukung pola ini, Notasi bingkai memiliki jenis fungsi khusus yang menyelidiki alam semesta dan menentukan situasi saat ini. Fungsi-fungsi tersebut ditunjukkan oleh $ awalan sebelum nama metode yang mengembalikan tautan ke status :

 $probeForState() 

Dalam situasi kami, metode seperti itu dapat diimplementasikan sebagai berikut:

 func probeForState():FrameState { if (temp < 32) return Solid if (temp < 212) return Liquid return Gas } 

Seperti yang dapat kita lihat, metode hanya mengembalikan referensi ke fungsi negara yang sesuai dengan keadaan logis yang benar. Fungsi penginderaan ini kemudian dapat digunakan untuk memasukkan kondisi yang benar:

 ->> $probeForState() 

Mekanisme implementasi terlihat seperti ini:

 _state = probeForState() 

Metode penginderaan negara adalah contoh notasi Frame untuk mengelola keadaan dengan cara tertentu. Selanjutnya, kita juga akan mempelajari notasi penting untuk mengelola FrameEvents.

Warisan dan dispatcher perilaku


Warisan perilaku dan dispatcher adalah paradigma pemrograman yang kuat dan topik terakhir tentang notasi Frame dalam artikel ini.

Penggunaan frame menggunakan pewarisan perilaku , bukan pewarisan data atau atribut lainnya. Untuk keadaan ini, FrameEvents dikirim ke negara-negara lain jika keadaan awal tidak menangani acara (atau, seperti yang akan kita lihat di artikel berikutnya, hanya ingin meneruskannya). Rantai peristiwa ini dapat mencapai kedalaman yang diinginkan.

Untuk ini, mesin dapat diimplementasikan menggunakan teknik yang disebut metode chaining . Notasi FMN untuk mengirim acara dari satu negara ke negara lain adalah operator => :

 $S1 => $S2 

Pernyataan FMN ini dapat diimplementasikan sebagai berikut:

 func S1(e:FrameEvent) { S2(e) // $S1 => $S2 } 

Sekarang kita melihat betapa mudahnya untuk metode rantai negara. Mari kita terapkan teknik ini pada situasi yang agak sulit:

 #Movement -machine- $Walking => $Moving |getSpeed| ^(3) |isStanding| ^(true) $Running => $Moving |getSpeed| ^(6) |isStanding| ^(true) $Crawling => $Moving |getSpeed| ^(.5) |isStanding| ^(false) $AtAttention => $Motionless |isStanding| ^(true) $LyingDown => $Motionless |isStanding| ^(false) $Moving |isMoving| ^(true) $Motionless |getSpeed| ^(0) |isMoving| ^(false) 

Dalam kode di atas, kita melihat bahwa ada dua status dasar - $ Moving dan $ Motionless - dan lima status lainnya mewarisi fungsi penting dari mereka. Penjilidan acara dengan jelas menunjukkan kepada kita bagaimana binding secara umum akan terlihat:


Berkat teknik yang telah kami pelajari, implementasinya akan sangat sederhana:

 class Movement { // #Movement // -machine- /********************************** $Walking => $Moving |getSpeed| ^(3) |isStanding| ^(true) ***********************************/ func Walking(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 3 return } if (e._msg == "isStanding") { e._return = true return } Moving(e) // $Walking => $Moving } /********************************** $Running => $Moving |getSpeed| ^(6) |isStanding| ^(true) ***********************************/ func Running(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 6 return } if (e._msg == "isStanding") { e._return = true return } Moving(e) // $Running => $Moving } /********************************** $Crawling => $Moving |getSpeed| ^(.5) |isStanding| ^(false) ***********************************/ func Crawling(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = .5 return } if (e._msg == "isStanding") { e._return = false return } Moving(e) // $Crawling => $Moving } /********************************** $AtAttention => $Motionless |isStanding| ^(true) ***********************************/ func AtAttention(e:FrameEvent) { if (e._msg == "isStanding") { e._return = true return } Motionless(e) // $AtAttention => $Motionless } /********************************** $LyingDown => $Motionless |isStanding| ^(false) ***********************************/ func LyingDown(e:FrameEvent) { if (e._msg == "isStanding") { e._return = false return } Motionless(e) // $AtAttention => $Motionless } /********************************** $Moving |isMoving| ^(true) ***********************************/ func Moving(e:FrameEvent) { if (e._msg == "isMoving") { e._return = true return } } /********************************** $Motionless |getSpeed| ^(0) |isMoving| ^(false) ***********************************/ func Motionless(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 0 return } if (e._msg == "isMoving") { e._return = false return } } } 

Mesin air


Sekarang kita memiliki dasar-dasar pengetahuan tentang FMN, memungkinkan kita untuk memahami bagaimana menerapkan kembali kelas WaterSample dengan negara dan dengan cara yang jauh lebih cerdas. Kami juga akan membuatnya berguna bagi fisikawan mahasiswa pascasarjana kami dan menambahkan status $ Plasma baru ke dalamnya:


Seperti apa bentuk implementasi FMN lengkapnya:

 #WaterSample -machine- $Begin |create| // set temp to the event param value setTemp(@[temp]) // probe for temp state and change to it ->> $probeForState() ^ $Solid => $Default |melt| doMelt() ->> $Liquid ^ |sublimate| doSublimate() ->> $Gas ^ |getState| ^("SOLID") $Liquid => $Default |freeze| doFreeze() ->> $Solid ^ |vaporize| doVaporize() ->> $Gas ^ |getState| ^("LIQUID") $Gas => $Default |condense| doCondense() ->> $Liquid ^ |depose| doDepose() ->> $Solid ^ |ionize| doIonize() ->> $Plasma ^ |getState| ^("GAS") $Plasma => $Default |recombine| doRecombine() ->> $Gas ^ |getState| ^("PLASMA") $Default |melt| throw new InvalidStateError() |sublimate| throw new InvalidStateError() |freeze| throw new InvalidStateError() |vaporize| throw new InvalidStateError() |condense| throw InvalidStateError() |depose| throw InvalidStateError() |ionize| throw InvalidStateError() |recombine| throw InvalidStateError() |getState| throw InvalidStateError() 

Seperti yang Anda lihat, kita memiliki status awal $ Begin, yang merespons pesan | create | dan mempertahankan nilai temp. Fungsi penginderaan pertama memeriksa nilai awal tempuntuk menentukan keadaan logis, dan kemudian melakukan transisi mesin ke keadaan ini.

Semua status fisik ($ Solid, $ Liquid, $ Gas, $ Plasma) mewarisi perilaku protektif dari keadaan $ Default. Semua peristiwa yang tidak valid untuk keadaan saat ini diteruskan ke keadaan $ Default, yang melempar kesalahan InvalidStateError. Ini menunjukkan bagaimana pemrograman defensif sederhana dapat diimplementasikan menggunakan pewarisan perilaku.

Dan sekarang implementasinya:

 class WaterSample { // -machine- var _state(e:FrameEvent) = Begin /********************************** $Begin |create| // set temp to the event param value setTemp(@[temp]) // probe for temp state and change to it ->> $probeForState() ^ ***********************************/ func Begin(e:FrameEvent) { if (e._msg == "create") { setTemp(e["temp"]) _state = probeForState() return } } /********************************** $Solid => $Default |melt| doMelt() ->> $Liquid ^ |sublimate| doSublimate() ->> $Gas ^ |sublimate| ^("SOLID") ***********************************/ func Solid(e:FrameEvent) { if (e._msg == "melt") { doMelt() _state = Liquid return } if (e._msg == "sublimate") { doSublimate() _state = Gas return } if (e._msg == "getState") { e._return = "SOLID" return } Default(e) } /********************************** $Liquid => $Default |freeze| doFreeze() ->> $Solid ^ |vaporize| doVaporize() ->> $Gas ^ |getState| ^("LIQUID") ***********************************/ func Liquid(e:FrameEvent) { if (e._msg == "freeze") { doFreeze() _state = Solid return } if (e._msg == "vaporize") { doVaporize() _state = Gas return } if (e._msg == "getState") { e._return = "LIQUID" return } Default(e) } /********************************** $Gas => $Default |condense| doCondense() ->> $Liquid ^ |depose| doDepose() ->> $Solid ^ |ionize| doIonize() ->> $Plasma ^ |getState| ^("GAS") ***********************************/ func Gas(e:FrameEvent) { if (e._msg == "condense") { doCondense() _state = Liquid return } if (e._msg == "depose") { doDepose() _state = Solid return } if (e._msg == "ionize") { doIonize() _state = Plasma return } if (e._msg == "getState") { e._return = "GAS" return } Default(e) } /********************************** $Plasma => $Default |recombine| doRecombine() ->> $Gas ^ |getState| ^("PLASMA") ***********************************/ func Plasma(e:FrameEvent) { if (e._msg == "recombine") { doRecombine() _state = Gas return } if (e._msg == "getState") { e._return = "PLASMA" return } Default(e) } /********************************** $Default |melt| throw new InvalidStateError() |sublimate| throw new InvalidStateError() |freeze| throw new InvalidStateError() |vaporize| throw new InvalidStateError() |condense| throw InvalidStateError() |depose| throw InvalidStateError() |ionize| throw InvalidStateError() |recombine| throw InvalidStateError() |getState| throw InvalidStateError() ***********************************/ func Default(e:FrameEvent) { if (e._msg == "melt") { throw new InvalidStateError() } if (e._msg == "sublimate") { throw new InvalidStateError() } if (e._msg == "freeze") { throw new InvalidStateError() } if (e._msg == "vaporize") { throw new InvalidStateError() } if (e._msg == "condense") { throw new InvalidStateError() } if (e._msg == "depose") { throw new InvalidStateError() } if (e._msg == "ionize") { throw new InvalidStateError() } if (e._msg == "recombine") { throw new InvalidStateError() } if (e._msg == "getState") { throw new InvalidStateError() } } } 

Kesimpulan


Automata adalah konsep dasar ilmu komputer yang telah digunakan terlalu lama hanya di bidang khusus pengembangan perangkat lunak dan perangkat keras. Tugas utama Frame adalah membuat notasi untuk mendeskripsikan automata dan menetapkan pola sederhana untuk menulis kode atau "mekanisme" untuk implementasinya. Saya berharap notasi Frame akan mengubah cara programmer memandang mesin, menyediakan cara mudah untuk mempraktikkannya dalam tugas pemrograman sehari-hari dan, tentu saja, menyelamatkan mereka dari spaghetti dalam kode.


Terminator makan pasta (foto oleh Suzuki san)
Dalam artikel mendatang, berdasarkan konsep yang telah kita pelajari, kita akan menciptakan kekuatan dan ekspresi yang lebih besar dari notasi FMN. Seiring waktu, saya akan memperluas diskusi ke studi pemodelan visual, yang mencakup FMN dan memecahkan masalah perilaku yang tidak pasti dalam pendekatan modern untuk pemodelan perangkat lunak.

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


All Articles