Komunikasi antara komponen yang dipisahkan menggunakan peristiwa


8

Kami memiliki Aplikasi Web di mana kami memiliki banyak (> 50) komponen Web kecil yang berinteraksi satu sama lain.

Agar semuanya dipisahkan, kita memiliki aturan bahwa tidak ada komponen yang dapat langsung merujuk yang lain. Alih-alih, komponen mengaktifkan peristiwa yang kemudian (dalam aplikasi "utama") terhubung untuk memanggil metode komponen lain.

Seiring berjalannya waktu, semakin banyak komponen yang ditambahkan dan file aplikasi "utama" menjadi berserakan dengan potongan kode yang terlihat seperti ini:

buttonsToolbar.addEventListener('request-toggle-contact-form-modal', () => {
  contactForm.toggle()
})

buttonsToolbar.addEventListener('request-toggle-bug-reporter-modal', () => {
  bugReporter.toggle()
})

// ... etc

Untuk memperbaiki ini kami mengelompokkan fungsi yang sama bersama-sama, dalam Class, beri nama itu sesuatu yang relevan, melewati elemen yang berpartisipasi ketika instantiating dan menangani "kabel" di dalam Class, seperti:

class Contact {
  constructor(contactForm, bugReporter, buttonsToolbar) {
    this.contactForm = contactForm
    this.bugReporterForm = bugReporterForm
    this.buttonsToolbar = buttonsToolbar

    this.buttonsToolbar
      .addEventListener('request-toggle-contact-form-modal', () => {
        this.toggleContactForm()
      })

    this.buttonsToolbar
      .addEventListener('request-toggle-bug-reporter-modal', () => {
        this.toggleBugReporterForm()
      })
  }

  toggleContactForm() {
    this.contactForm.toggle()
  }

  toggleBugReporterForm() {
    this.bugReporterForm.toggle()
  }
}

dan kami instantiate seperti:

<html>
  <contact-form></contact-form>
  <bug-reporter></bug-reporter>

  <script>
    const contact = new Contact(
      document.querySelector('contact-form'),
      document.querySelector('bug-form')
    )
  </script>
</html>

Saya benar-benar lelah memperkenalkan pola saya sendiri, terutama yang tidak benar-benar OOP-y karena saya gunakan Classessebagai wadah inisialisasi belaka, karena tidak ada kata yang lebih baik.

Apakah ada pola yang lebih baik / lebih dikenal untuk menangani jenis tugas yang saya lewatkan?


2
Ini sebenarnya terlihat agak luar biasa.
Robert Harvey

Ketika saya ingat dengan benar, dalam pertanyaan Anda sebelumnya, Anda telah menyebutnya mediator, yang juga merupakan nama pola dari buku GoF, jadi ini jelas merupakan pola OOP.
Doc Brown

@RobertHarvey Nah kata-kata Anda memiliki banyak bobot bagi saya; Apakah Anda akan melakukannya secara berbeda? Saya tidak yakin saya terlalu memikirkan ini.
nicholaswmin

2
Jangan terlalu memikirkan ini. Kelas "kabel" Anda terlihat SOLID bagi saya, jika berhasil dan Anda senang dengan namanya, seharusnya tidak masalah bagaimana namanya.
Doc Brown

1
@NicholasKyriakides: lebih baik tanyakan salah satu rekan kerja Anda (yang pasti tahu sistem lebih baik dari saya) untuk nama baik, bukan orang asing seperti saya dari internet.
Doc Brown

Jawaban:


6

Kode yang Anda miliki cukup bagus. Hal yang kelihatannya agak membingungkan adalah kode inisialisasi bukan bagian dari objek itu sendiri. Artinya, Anda dapat membuat instance objek, tetapi jika Anda lupa menyebut kelas kabelnya, itu tidak berguna.

Pertimbangkan Notification Center (alias Event Bus) yang mendefinisikan sesuatu seperti ini:

class NotificationCenter(){
    constructor(){
        this.dictionary = {}
    }
    register(message, callback){
        if not this.dictionary.contains(message){
            this.dictionary[message] = []
        }
        this.dictionary[message].append(callback)
    }
    notify(message, payload){
        if this.dictionary.contains(message){
            for each callback in this.dictionary[message]{
                callback(payload)
            }
        }
    }
}

Ini adalah pengendali acara multi-pengiriman DIY. Anda kemudian dapat melakukan perkabelan Anda sendiri dengan hanya memerlukan NotificationCenter sebagai argumen konstruktor. Mengirim pesan ke dalamnya dan menunggunya memberi Anda muatan adalah satu-satunya kontak yang Anda miliki dengan sistem, jadi sangat SOLID.

class Toolbar{
    constructor(notificationCenter){
        this.NC = notificationCenter
        this.NC.register('request-toggle-contact-form-modal', (payload) => {
            this.toggleContactForm(payload)
          }
    }
    toolbarButtonClicked(e){
        this.NC.notify('toolbar-button-click-event', e)
    }
}

Catatan: Saya menggunakan string literal di tempat untuk kunci agar konsisten dengan gaya yang digunakan dalam pertanyaan dan untuk kesederhanaan. Ini tidak dianjurkan karena risiko kesalahan ketik. Sebagai gantinya, pertimbangkan untuk menggunakan konstanta enumerasi atau string.

Dalam kode di atas, Bilah Alat bertanggung jawab untuk membuat NotificationCenter tahu jenis acara apa yang diminati, dan menerbitkan semua interaksi eksternal melalui metode notify. Kelas lain yang tertarik dengan toolbar-button-click-eventitu hanya akan mendaftar untuk itu di konstruktornya.

Variasi yang menarik pada pola ini meliputi:

  • Menggunakan banyak NCs untuk menangani berbagai bagian sistem
  • Memiliki metode Notify memunculkan utas untuk setiap notifikasi, daripada mencekal secara serial
  • Menggunakan daftar prioritas daripada daftar biasa di dalam NC untuk menjamin pemesanan sebagian komponen mana yang mendapat pemberitahuan terlebih dahulu
  • Registrasi mengembalikan ID yang dapat digunakan untuk Tidak Mendaftar nanti
  • Lewati argumen pesan dan kirimkan berdasarkan kelas / jenis pesan

Fitur menarik termasuk:

  • Menginstruksikan NC semudah mendaftarkan penebang untuk mencetak muatan
  • Menguji satu atau lebih komponen yang berinteraksi hanyalah masalah instantiate mereka, menambahkan pendengar untuk hasil yang diharapkan, dan mengirim pesan
  • Menambahkan komponen baru dengan mendengarkan pesan lama itu sepele
  • Menambahkan komponen baru mengirim pesan ke yang lama itu sepele

Gotcha yang menarik dan kemungkinan solusi termasuk:

  • Peristiwa yang memicu peristiwa lain bisa membingungkan.
    • Masukkan ID pengirim dalam acara tersebut untuk menunjukkan dengan tepat sumber kejadian yang tidak terduga.
  • Setiap komponen tidak tahu apakah bagian tertentu dari sistem sudah aktif dan berjalan sebelum menerima suatu peristiwa, sehingga pesan awal dapat dihapus.
    • Ini dapat ditangani oleh kode yang membuat komponen yang mengirim pesan 'sistem siap', yang tentu saja komponen yang berminat perlu mendaftar.
  • Bus peristiwa membuat antarmuka tersirat antara komponen, artinya tidak ada cara bagi kompilator untuk memastikan Anda telah menerapkan semua yang Anda harus.
    • Argumen standar antara statis dan dinamis berlaku di sini.
  • Pendekatan ini mengelompokkan berbagai komponen, tidak harus perilaku. Menelusuri peristiwa melalui sistem mungkin memerlukan lebih banyak pekerjaan di sini daripada pendekatan OP. Misalnya, OP dapat mengatur semua pendengar yang berhubungan dengan tabungan diatur dan pendengar yang berhubungan dengan penghapusan diatur bersama di tempat lain.
    • Ini dapat dikurangi dengan penamaan acara yang baik dan dokumentasi seperti bagan alur. (Ya, dokumentasinya terkenal tidak sesuai dengan kode). Anda juga bisa menambahkan daftar penangan pra dan pasca penangkapan yang mendapatkan semua pesan dan mencetak siapa yang mengirim apa dalam urutan apa.

Pendekatan ini sepertinya mengingatkan kita pada arsitektur Event Bus. Satu-satunya perbedaan adalah bahwa pendaftaran Anda adalah string topik daripada jenis pesan. Kelemahan utama menggunakan string adalah bahwa mereka dapat salah ketik - artinya notifikasi atau pendengar bisa salah eja dan akan sulit untuk di-debug.
Berin Loritsch

Sejauh yang saya tahu ini adalah contoh klasik dari Mediator . Masalah dengan pendekatan ini adalah bahwa ia memasangkan komponen dengan Event Bus / Mediator. Bagaimana jika saya ingin memindahkan komponen, misalnya buttonsToolbarke proyek lain yang tidak menggunakan Bus Acara?
nicholaswmin

+1 Manfaat dari mediator adalah memungkinkan Anda untuk mendaftar dengan string / enum dan memiliki sambungan longgar di dalam kelas. Jika Anda memindahkan kabel di luar objek ke kelas utama / pengaturan Anda, maka ia tahu tentang semua objek dan bisa menghubungkannya langsung ke acara / fungsi tanpa khawatir tentang kopling. @NicholasKyriakides Pilih satu atau yang lain daripada mencoba menggunakan keduanya
Ewan

Dengan arsitektur bus kejadian klasik, satu-satunya sambungan adalah pesan itu sendiri. Pesan tersebut biasanya merupakan objek yang tidak dapat diubah. Objek yang mengirim pesan hanya membutuhkan antarmuka penerbit untuk mengirim pesan. Jika Anda menggunakan jenis objek pesan maka Anda hanya perlu mempublikasikan objek pesan. Jika Anda menggunakan string, Anda harus menyediakan string topik dan payload pesan (kecuali stringnya payload). Menggunakan string berarti Anda hanya harus teliti tentang nilai-nilai di kedua sisi.
Berin Loritsch

@NicholasKyriakides Apa yang terjadi jika Anda memindahkan kode asli ke solusi baru? Anda harus membawa kelas pengaturan Anda dan mengubahnya untuk konteksnya yang baru. Hal yang sama berlaku untuk pola ini.
Joel Harmon

3

Saya dulu memperkenalkan semacam "bus acara", dan di tahun-tahun berikutnya saya mulai semakin mengandalkan Model Objek Dokumen itu sendiri untuk mengkomunikasikan acara untuk kode UI.

Di browser, DOM adalah satu-satunya ketergantungan yang selalu ada - bahkan selama pemuatan halaman. Kuncinya adalah memanfaatkan Acara Kustom dalam JavaScript, dan mengandalkan gelembung acara untuk mengomunikasikan peristiwa itu.

Sebelum orang-orang mulai berteriak tentang "menunggu dokumen siap" sebelum melampirkan pelanggan, document.documentElementproperti mereferensikan <html>elemen dari saat JavaScript mulai dieksekusi, tidak peduli di mana skrip diimpor atau urutan yang muncul di markup Anda.

Di sinilah Anda dapat mulai mendengarkan acara.

Sangat umum untuk memiliki komponen JavaScript (atau widget) langsung di dalam tag HTML tertentu pada halaman. Elemen "root" dari komponen adalah tempat Anda dapat memicu peristiwa menggelegak Anda. Pelanggan pada <html>elemen akan menerima notifikasi ini sama seperti acara yang dibuat pengguna lain.

Hanya beberapa contoh kode pelat ketel:

(function (window, document, html) {
    html.addEventListener("custom-event-1", function (event) {
        // ...
    });
    html.addEventListener("custom-event-2", function (event) {
        // ...
    });

    function someOperation() {
        var customData = { ... };
        var event = new CustomEvent("custom-event-3", { detail : customData });

        event.dispatchEvent(componentRootElement);
    }
})(this, this.document, this.document.documentElement);

Jadi polanya menjadi:

  1. Gunakan Acara Kustom
  2. Berlangganan ke acara ini di document.documentElementproperti (tidak perlu menunggu dokumen siap)
  3. Publikasikan acara pada elemen root untuk komponen Anda, atau document.documentElement.

Ini harus bekerja untuk basis kode fungsional dan berorientasi objek.


1

Saya menggunakan gaya yang sama dengan pengembangan video game saya dengan Unity 3D. Saya membuat komponen seperti Kesehatan, Input, Statistik, Suara, dll. Dan menambahkannya ke Obyek Game untuk membangun objek objek permainan itu. Unity sudah memiliki mekanik untuk menambahkan komponen ke objek game. Namun, apa yang saya temukan adalah kebanyakan orang meminta komponen atau merujuk langsung komponen di dalam komponen lain (bahkan jika mereka menggunakan antarmuka itu masih lebih digabungkan, terima kasih saya lebih suka). Saya ingin komponen dapat dibuat dalam isolasi dengan nol dependensi komponen lainnya. Jadi saya memiliki komponen peristiwa kebakaran ketika data berubah (khusus untuk komponen) dan menyatakan metode untuk mengubah data pada dasarnya. Kemudian objek game yang saya buat kelas untuk dan menempelkan semua peristiwa komponen ke metode komponen lainnya.

Hal yang saya sukai dari ini adalah bahwa untuk melihat semua interaksi komponen untuk objek game, saya dapat melihat kelas 1 ini. Kedengarannya seperti kelas Kontak Anda sangat mirip dengan kelas Objek Game saya (saya beri nama objek game ke objek yang seharusnya seperti MainPlayer, Orc, dll).

Kelas-kelas ini adalah semacam kelas manajer. Mereka sendiri tidak benar-benar memiliki apa pun kecuali komponen dan kode untuk menghubungkan mereka. Saya tidak yakin mengapa Anda membuat metode di sini yang hanya memanggil metode komponen lain ketika Anda bisa menghubungkannya secara langsung. Maksud dari kelas ini adalah benar-benar hanya untuk mengatur acara yang terhubung.

Sebagai catatan untuk pendaftaran acara saya, saya menambahkan panggilan balik filter dan panggilan balik args. Ketika acara dipicu (saya membuat kelas acara khusus saya sendiri) itu akan memanggil panggilan balik filter jika ada dan jika kembali benar maka akan pindah ke panggilan balik args. Tujuan dari callback filter adalah untuk memberikan fleksibilitas. Suatu peristiwa dapat memicu karena berbagai alasan tetapi saya hanya ingin memanggil acara yang terhubung saya jika cek benar. Contohnya mungkin komponen Input memiliki acara OnKeyHit. Jika saya memiliki komponen Gerakan yang memiliki metode seperti MoveForward () MoveBackward (), dll. Saya dapat menghubungkan OnKeyHit + = MoveForward tapi jelas saya tidak ingin bergerak maju dengan tombol apa pun yang terkena. Saya hanya ingin melakukannya jika kuncinya adalah kunci 'w'. Karena OnKeyHit sedang mengisi argumen untuk diteruskan dan salah satunya adalah kunci yang dipukul,

Bagi saya, langganan untuk kelas pengelola objek permainan tertentu lebih mirip:

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' });

Karena komponen dapat dikembangkan secara terpisah, beberapa pemrogram dapat mengkodekannya. Dengan contoh di atas, coder input memberikan objek argumen variabel bernama Key. Namun, pengembang komponen Gerakan mungkin tidak menggunakan Kunci (jika perlu melihat argumen, dalam hal ini mungkin tidak tetapi dalam kasus lain mereka menggunakan nilai argumen yang diteruskan). Untuk menghapus persyaratan komunikasi ini, callback args bertindak sebagai pemetaan untuk argumen antar komponen. Jadi orang yang membuat game object manager class ini adalah orang yang perlu hanya tahu nama variabel arg antara 2 klien ketika mereka pergi untuk menghubungkan mereka dan melakukan pemetaan pada saat itu. Metode ini dipanggil setelah fungsi filter.

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' }, (args) => { args.keyPressed = args.Key });

Jadi dalam situasi di atas orang Input memberi nama variabel di dalam objek args 'Key' tetapi Gerakan menamakannya 'keyPressed'. Ini membantu lebih lanjut isolasi antara komponen itu sendiri ketika mereka sedang dikembangkan dan menempatkannya pada implementer kelas manajer untuk terhubung dengan benar.


0

Untuk apa nilainya, saya melakukan sesuatu sebagai bagian dari proyek backend dan telah mengambil pendekatan serupa:

  • Sistem saya tidak melibatkan widget (komponen web) tetapi abstrak 'adaptor', implementasi konkret yang menangani berbagai protokol.
  • Protokol dimodelkan sebagai seperangkat 'percakapan' yang memungkinkan. Adaptor protokol memicu percakapan ini tergantung pada acara yang masuk.
  • Ada Bus Acara yang pada dasarnya adalah Subjek Rx.
  • Event Bus berlangganan ke output dari semua adapter, dan semua adapter berlangganan ke output dari Event Bus.
  • 'Adaptor' dimodelkan sebagai aliran agregat dari semua 'percakapannya.' Percakapan adalah aliran yang berlangganan output dari bus peristiwa, menghasilkan pesan ke bus peristiwa, didorong oleh Mesin Negara.

Bagaimana saya menangani tantangan konstruksi / perkabelan Anda:

  • Protokol (diimplementasikan oleh adaptor) mendefinisikan kriteria yang memulai percakapan sebagai filter atas aliran input yang dilangganinya. Dalam C # ini adalah kueri LINQ atas aliran. Dalam ReactJS ini akan menjadi. Dimana atau. Operator filter.
  • Percakapan memutuskan pesan apa yang relevan menggunakan filternya sendiri.
  • Secara umum, apa pun yang berlangganan bus adalah stream, dan bus berlangganan ke stream tersebut.

Analogi dengan bilah alat Anda:

  • Kelas toolbar adalah .Map dari input yang dapat diamati (bus), yang dapat diamati dari kejadian toolbar, di mana bus berlangganan
  • Toolbar yang dapat diamati (jika Anda menggunakan banyak sub-toolbar) berarti Anda mungkin memiliki banyak yang bisa diamati, sehingga toolbar Anda dapat diamati dari yang bisa diamati. Ini akan menjadi RxJs. Merge'd menjadi output tunggal ke bus.

Masalah yang mungkin Anda hadapi:

  • Memastikan bahwa acara tidak siklus dan menggantung prosesnya.
  • Konkurensi (tidak tahu apakah ini relevan untuk komponen Web): untuk operasi asinkron atau operasi yang mungkin berjalan lama, event handler Anda dapat memblokir utas yang dapat diamati jika tidak dijalankan sebagai tugas latar belakang. Penjadwal RxJS dapat mengatasi hal ini (secara default Anda dapat. Mengawasi penjadwal default untuk semua langganan bus, misalnya)
  • Skenario yang lebih kompleks yang tidak dapat dimodelkan tanpa gagasan percakapan (misalnya: menangani suatu peristiwa dengan mengirim pesan dan menunggu respons, yang merupakan peristiwa itu sendiri). Dalam hal ini, mesin keadaan berguna untuk secara dinamis menentukan acara mana yang ingin Anda tangani (percakapan dalam model saya). Saya melakukan ini dengan memiliki aliran percakapan .filtermenurut negara (sebenarnya, implementasinya lebih fungsional - percakapan adalah peta datar yang dapat diamati dari pengamatan peristiwa perubahan negara).

Jadi, secara ringkas, Anda dapat melihat seluruh domain masalah Anda sebagai yang dapat diobservasi, atau 'secara fungsional' / 'secara deklaratif' dan menganggap komponen web Anda sebagai aliran peristiwa, sebagai yang dapat diamati, berasal dari bus (dapat diamati), ke mana bus (sebuah pengamat) juga berlangganan. Instansiasi yang dapat diobservasi (mis: toolbar baru) bersifat deklaratif, di mana keseluruhan proses dapat dilihat sebagai yang dapat diamati dari yang .mapd dapat diobservasi dari aliran input.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.