Mode pengamat , juga dikenal sebagai mode publikasi/berlangganan, diusulkan oleh kelompok empat orang (GOF, yaitu Erich Gamma, Richard Helm, Ralph Johnson dan John Vlissides) dalam "Pola Desain: Dasar-dasar Perangkat Lunak Berorientasi Objek yang Dapat Digunakan kembali" (lihat halaman 293-313 dalam detail). Meskipun pola ini memiliki sejarah yang cukup besar, masih banyak berlaku untuk berbagai skenario dan bahkan telah menjadi bagian integral dari Perpustakaan Java standar. Meskipun sudah ada banyak artikel tentang pola pengamat, mereka semua fokus pada implementasi di Java, tetapi mengabaikan berbagai masalah yang dihadapi oleh pengembang saat menggunakan pola pengamat di Java.
Tujuan asli menulis artikel ini adalah untuk mengisi kesenjangan ini: Artikel ini terutama memperkenalkan implementasi pola pengamat dengan menggunakan arsitektur Java8, dan lebih lanjut mengeksplorasi masalah kompleks tentang pola klasik atas dasar ini, termasuk kelas internal anonim, ekspresi Lambda, keselamatan utas, dan implementasi pengamat waktu-konsumen yang tidak sepele. Meskipun konten artikel ini tidak komprehensif, banyak masalah kompleks yang terlibat dalam model ini tidak dapat dijelaskan hanya dalam satu artikel. Tetapi setelah membaca artikel ini, pembaca dapat memahami apa pola pengamat, universalitasnya di Java, dan bagaimana menangani beberapa masalah umum ketika menerapkan pola pengamat di Java.
Mode pengamat
Menurut definisi klasik yang diusulkan oleh GOF, tema pola pengamat adalah:
Mendefinisikan ketergantungan satu-ke-banyak antara objek. Ketika keadaan suatu objek berubah, semua objek yang bergantung padanya diberitahu dan diperbarui secara otomatis.
Apa artinya? Dalam banyak aplikasi perangkat lunak, status antar objek saling bergantung. Misalnya, jika aplikasi berfokus pada pemrosesan data numerik, data ini dapat ditampilkan melalui tabel atau bagan antarmuka pengguna grafis (GUI) atau digunakan pada saat yang sama, yaitu, ketika data yang mendasarinya diperbarui, komponen GUI yang sesuai juga harus diperbarui. Kunci masalah ini adalah bagaimana memperbarui data yang mendasari ketika komponen GUI diperbarui, dan pada saat yang sama meminimalkan kopling antara komponen GUI dan data yang mendasarinya.
Solusi yang sederhana dan tidak dapat ditentukan adalah merujuk pada tabel dan komponen GUI gambar dari objek yang mengelola data yang mendasarinya, sehingga objek dapat memberi tahu komponen GUI ketika data yang mendasarinya berubah. Jelas, solusi sederhana ini dengan cepat menunjukkan kekurangannya untuk aplikasi kompleks yang menangani lebih banyak komponen GUI. Misalnya, ada 20 komponen GUI yang semuanya bergantung pada data yang mendasari, sehingga objek yang mengelola data yang mendasari perlu mempertahankan referensi ke 20 komponen ini. Dengan jumlah objek yang bergantung pada data terkait meningkat, tingkat kopling antara manajemen data dan objek menjadi sulit dikendalikan.
Solusi lain yang lebih baik adalah untuk mengizinkan objek untuk mendaftar untuk mendapatkan izin untuk memperbarui data yang menarik, yang akan diberitahukan oleh manajer data ketika data berubah. Dalam istilah awam, biarkan objek data yang menarik memberi tahu manajer: "Harap beri tahu saya ketika data berubah." Selain itu, objek -objek ini tidak hanya dapat mendaftar untuk mendapatkan pemberitahuan pembaruan, tetapi juga membatalkan pendaftaran untuk memastikan bahwa manajer data tidak lagi memberi tahu objek ketika data berubah. Dalam definisi asli GOF, objek yang terdaftar untuk mendapatkan pembaruan disebut "pengamat", manajer data yang sesuai disebut "subjek", data yang diminati pengamat disebut "status target", proses pendaftaran disebut "Tambah" dan proses pengamatan yang membatalkan disebut "melepaskan". Seperti disebutkan di atas, mode pengamat juga disebut Mode Penerbitan-Subjikan. Dapat dipahami bahwa pelanggan berlangganan pengamat tentang target. Ketika status target diperbarui, target menerbitkan pembaruan ini kepada pelanggan (pola desain ini diperluas ke arsitektur umum, yang disebut arsitektur berlangganan publikasi). Konsep -konsep ini dapat diwakili oleh diagram kelas berikut:
ConcereteObserver menggunakannya untuk menerima perubahan status pembaruan dan memberikan referensi ke ConcereteSubject ke konstruktornya. Ini memberikan referensi ke subjek tertentu untuk pengamat tertentu, dari mana pembaruan dapat diperoleh ketika keadaan berubah. Sederhananya, pengamat spesifik akan diberitahu untuk memperbarui topik, dan pada saat yang sama menggunakan referensi dalam konstruktornya untuk mendapatkan keadaan topik tertentu, dan akhirnya menyimpan objek negara pencarian ini di bawah properti Observerstate dari pengamat tertentu. Proses ini ditampilkan dalam diagram urutan berikut:
Spesialisasi model klasik <br /> Meskipun model pengamat bersifat universal, ada juga banyak model khusus, yang paling umum adalah dua berikut:
1. Memberikan parameter ke objek negara dan meneruskannya ke metode pembaruan yang dipanggil oleh pengamat. Dalam mode klasik, ketika pengamat diberitahu bahwa keadaan subjek telah berubah, keadaan yang diperbarui akan diperoleh langsung dari subjek. Ini mengharuskan pengamat untuk menyimpan referensi objek ke keadaan yang diambil. Ini membentuk referensi melingkar, referensi titik -titik concretesubject ke daftar pengamatnya, dan referensi poin ConcreteObserver ke Concretesubject yang dapat memperoleh keadaan subjek. Selain mendapatkan status yang diperbarui, tidak ada hubungan antara pengamat dan subjek yang didengarkannya. Pengamat peduli dengan objek negara, bukan subjek itu sendiri. Dengan kata lain, dalam banyak kasus, ConcreteObserver dan Concretesubject secara paksa dihubungkan bersama. Sebaliknya, ketika Concretesubject memanggil fungsi pembaruan, objek negara diteruskan ke ConcreteObserver, dan keduanya tidak perlu dikaitkan. Hubungan antara concreteobserver dan objek negara mengurangi tingkat ketergantungan antara pengamat dan negara (lihat artikel Martin Fowler untuk lebih banyak perbedaan dalam hubungan dan ketergantungan).
2. Gabungkan Kelas Abstrak Subjek dan Concretesubject menjadi kelas Singlesubject. Dalam kebanyakan kasus, penggunaan kelas abstrak dalam subjek tidak meningkatkan fleksibilitas dan skalabilitas program, sehingga menggabungkan kelas abstrak dan kelas beton ini menyederhanakan desain.
Setelah dua model khusus ini digabungkan, diagram kelas yang disederhanakan adalah sebagai berikut:
Dalam model khusus ini, struktur kelas statis sangat disederhanakan dan interaksi antar kelas juga disederhanakan. Diagram urutan saat ini adalah sebagai berikut:
Fitur lain dari mode spesialisasi adalah penghapusan pengamat variabel anggota dari ConcreteObserver. Kadang -kadang pengamat spesifik tidak perlu menyimpan keadaan terbaru dari subjek, tetapi hanya perlu memantau status subjek ketika status diperbarui. Misalnya, jika pengamat memperbarui nilai variabel anggota ke output standar, ia dapat menghapus Observerstate, yang menghapus hubungan antara konkreteobserver dan kelas negara.
Aturan penamaan yang lebih umum <BR /> mode klasik dan bahkan mode profesional yang disebutkan di atas menggunakan istilah seperti lampirkan, melepaskan dan pengamat, sementara banyak implementasi Java menggunakan kamus yang berbeda, termasuk register, tidak mendaftar, pendengar, dll. Perlu disebutkan bahwa negara adalah istilah umum untuk semua objek yang perlu dipantau oleh pendengar untuk memantau perubahan. Nama spesifik objek negara tergantung pada skenario yang digunakan dalam mode pengamat. Misalnya, dalam mode pengamat dalam adegan di mana pendengar mendengarkan kejadian peristiwa, pendengar terdaftar akan menerima pemberitahuan ketika peristiwa terjadi. Objek status saat ini adalah peristiwa, yaitu, apakah peristiwa telah terjadi.
Dalam aplikasi yang sebenarnya, penamaan target jarang termasuk subjek. Misalnya, buat aplikasi tentang kebun binatang, daftarkan banyak pendengar untuk mengamati kelas kebun binatang, dan terima pemberitahuan ketika hewan baru memasuki kebun binatang. Tujuan dalam kasus ini adalah kelas kebun binatang. Untuk menjaga terminologi konsisten dengan domain masalah yang diberikan, istilah "subjek" tidak akan digunakan, yang berarti bahwa kelas kebun binatang tidak akan dinamai zoosubject.
Penamaan pendengar umumnya diikuti oleh akhiran pendengar. Misalnya, pendengar yang disebutkan di atas untuk memantau hewan baru akan dinamai AnimalAddedListener. Demikian pula, penamaan fungsi seperti register, lepas dan beri tahu sering dimusnahkan dengan nama pendengar yang sesuai. Misalnya, fungsi register, lepaskan, dan beri tahu tentang animaladdedlistener akan dinamai registeranimaladdedlistener, unregisteranimaladdedlistener dan notifyanimaladdedlistener. Perlu dicatat bahwa nama fungsi notify digunakan, karena fungsi notify menangani banyak pendengar daripada satu pendengar.
Metode penamaan ini akan tampak panjang, dan biasanya subjek akan mendaftarkan beberapa jenis pendengar. Misalnya, dalam contoh kebun binatang yang disebutkan di atas, di kebun binatang, selain mendaftarkan pendengar baru untuk memantau hewan, ia juga perlu mendaftarkan pendengar hewan untuk mengurangi pendengar. Pada saat ini, akan ada dua fungsi register: (Registeranimaladdedlistener dan registeranimalremovedlistener. Dengan cara ini, jenis pendengar digunakan sebagai kualifikasi untuk menunjukkan jenis pengamat. Solusi lain adalah membuat fungsi yang lebih baik dan kelebihan beban, tetapi solusi 1 dapat lebih mudah mengetahui pendengar yang lebih rendah.
Sintaks idiomatik lainnya adalah untuk digunakan pada awalan alih -alih pembaruan, misalnya, fungsi pembaruan bernama Onanimaladded alih -alih UpdateAnimaladded. Situasi ini lebih umum ketika pendengar mendapat pemberitahuan untuk urutan, seperti menambahkan hewan ke daftar, tetapi jarang digunakan untuk memperbarui data yang terpisah, seperti nama hewan.
Selanjutnya, artikel ini akan menggunakan aturan simbolik Java. Meskipun aturan simbolis tidak akan mengubah desain nyata dan implementasi sistem, ini adalah prinsip pengembangan yang penting untuk menggunakan istilah yang akrab dengan pengembang lain, jadi Anda harus terbiasa dengan aturan simbolik pola pengamat di Java yang dijelaskan di atas. Konsep di atas akan dijelaskan di bawah ini menggunakan contoh sederhana di lingkungan Java 8.
Contoh sederhana
Ini juga merupakan contoh kebun binatang yang disebutkan di atas. Menggunakan antarmuka API Java8 untuk mengimplementasikan sistem sederhana, menjelaskan prinsip -prinsip dasar dari pola pengamat. Masalahnya digambarkan sebagai:
Buat kebun binatang sistem, memungkinkan pengguna untuk mendengarkan dan membatalkan keadaan menambahkan hewan objek baru, dan membuat pendengar tertentu, bertanggung jawab untuk mengeluarkan nama hewan baru.
Menurut pembelajaran sebelumnya dari pola pengamat, kita tahu bahwa untuk mengimplementasikan aplikasi semacam itu, kita perlu membuat 4 kelas, khususnya:
Pertama, kami membuat kelas hewan, yang merupakan objek Java sederhana yang berisi variabel anggota, konstruktor, getters dan metode setter. Kodenya adalah sebagai berikut:
hewan kelas publik {nama string pribadi; hewan publik (nama string) {this.name = name; } public string getName () {return this.name; } public void setName (name string) {this.name = name; }}Gunakan kelas ini untuk mewakili objek hewan, dan kemudian Anda dapat membuat antarmuka animalAddDistener:
antarmuka publik animaladdedlistener {public void onanimaladded (hewan hewan);}Dua kelas pertama sangat sederhana, jadi saya tidak akan memperkenalkannya secara detail. Selanjutnya, buat kelas kebun binatang:
zoo kelas publik {daftar pribadi <nejala> hewan = arraylist baru <> (); Daftar Privat <AnlyAddedListener> pendengar = ArrayList baru <> (); public void addanimal (hewan hewan) {// tambahkan hewan ke daftar hewan this.animals.add (hewan); // Beri tahu daftar pendengar terdaftar ini. NotifyAnimaladdedlisteners (Hewan); } public void registeranimaladdedlistener (animalAddedListener listener) {// Tambahkan pendengar ke daftar pendengar terdaftar this.listeners.add (pendengar); } public void unregisteranimaladdedlistener (animalAddedListener listener) {// Hapus pendengar dari daftar pendengar terdaftar this.listeners.remove (pendengar); } lindung void notifyanimaladdedlisteners (hewan hewan) {// beri tahu masing -masing pendengar dalam daftar pendengar pendengar terdaftar this.listeners.foreach (pendengar -> listener.updateanimaladded (hewan)); }}Analogi ini kompleks dari dua sebelumnya. Ini berisi dua daftar, satu digunakan untuk menyimpan semua hewan di kebun binatang dan yang lainnya digunakan untuk menyimpan semua pendengar. Mengingat bahwa benda -benda yang disimpan dalam koleksi hewan dan pendengar sederhana, artikel ini memilih arraylist untuk penyimpanan. Struktur data spesifik dari pendengar yang disimpan tergantung pada masalahnya. Misalnya, untuk masalah kebun binatang di sini, jika pendengar memiliki prioritas, Anda harus memilih struktur data lain, atau menulis ulang algoritma register pendengar.
Implementasi pendaftaran dan penghapusan adalah metode delegasi sederhana: Setiap pendengar ditambahkan atau dihapus dari daftar mendengarkan pendengar sebagai parameter. Implementasi fungsi notify sedikit dimatikan dari format standar pola pengamat. Ini termasuk parameter input: hewan yang baru ditambahkan, sehingga fungsi notify dapat meneruskan referensi hewan yang baru ditambahkan ke pendengar. Gunakan fungsi foreach dari API Streams untuk melintasi pendengar dan menjalankan fungsi Theonanimaladded pada setiap pendengar.
Dalam fungsi addanimal, objek hewan yang baru ditambahkan dan pendengar ditambahkan ke daftar yang sesuai. Jika kompleksitas proses pemberitahuan tidak diperhitungkan, logika ini harus dimasukkan dalam metode panggilan yang nyaman. Anda hanya perlu meneruskan referensi ke objek hewan yang baru ditambahkan. Inilah sebabnya mengapa implementasi logis dari pendengar pemberitahuan dienkapsulasi dalam fungsi notifyanimaladdedlisteners, yang juga disebutkan dalam implementasi addanimal.
Selain masalah logis dari fungsi -fungsi notify, perlu untuk menekankan masalah kontroversial tentang visibilitas fungsi -fungsi notify. Dalam model pengamat klasik, seperti yang dikatakan GOF pada halaman 301 dari pola desain buku, fungsi notify adalah publik, tetapi meskipun digunakan dalam pola klasik, ini tidak berarti bahwa itu harus publik. Pemilihan visibilitas harus didasarkan pada aplikasi. Misalnya, dalam contoh kebun binatang artikel ini, fungsi notify adalah jenis yang dilindungi dan tidak mengharuskan setiap objek untuk memulai pemberitahuan pengamat terdaftar. Ini hanya perlu memastikan bahwa objek dapat mewarisi fungsi dari kelas induk. Tentu saja, ini bukan masalahnya. Penting untuk mengetahui kelas mana yang dapat mengaktifkan fungsi notify, dan kemudian menentukan visibilitas fungsi.
Selanjutnya, Anda perlu mengimplementasikan kelas PrintNeAnimaladdedListener. Kelas ini menggunakan metode System.out.println untuk mengeluarkan nama hewan baru. Kode spesifiknya adalah sebagai berikut:
kelas publik printNeanimaladdedlistener mengimplementasikan animaladdedlistener {@override public void updateanimaladded (hewan hewan) {// cetak nama sistem hewan yang baru ditambahkan.out.println ("Menambahkan hewan baru dengan nama '" + hewan.getname () + ""); }}Akhirnya, kita perlu menerapkan fungsi utama yang mendorong aplikasi:
kelas publik {public static void main (string [] args) {// Buat kebun binatang untuk menyimpan binatang kebun binatang = zoo baru (); // Daftarkan seorang pendengar untuk diberitahu ketika seekor hewan ditambahkan zoo.registeranimaladdedlistener (printnameanimaladdedlistener baru ()); // Tambahkan hewan, beri tahu kebun binatang pendengar terdaftar. }}Fungsi utama hanya membuat objek kebun binatang, mendaftarkan pendengar yang mengeluarkan nama hewan, dan membuat objek hewan baru untuk memicu pendengar terdaftar. Output akhir adalah:
Menambahkan hewan baru dengan nama 'Tiger'
Menambahkan pendengar
Keuntungan dari mode pengamat sepenuhnya ditampilkan ketika pendengar dibangun kembali dan ditambahkan ke subjek. Misalnya, jika Anda ingin menambahkan pendengar yang menghitung jumlah total hewan di kebun binatang, Anda hanya perlu membuat kelas pendengar tertentu dan mendaftarkannya dengan kelas kebun binatang tanpa modifikasi ke kelas kebun binatang. Menambahkan kode penghitung penghitung penghitung pendengar adalah sebagai berikut:
Public Class CountanimaladdedListener mengimplementasikan animaladdedlistener {private static int animalsaddedcount = 0; @Override public void updateAnimalAdded (hewan hewan) {// menambah jumlah hewan hewan yang berada di bawah ++; // Cetak jumlah hewan sistem.out.println ("Total hewan ditambahkan:" + animalsaddedcount); }}Fungsi utama yang dimodifikasi adalah sebagai berikut:
kelas publik {public static void main (string [] args) {// Buat kebun binatang untuk menyimpan binatang kebun binatang = zoo baru (); // Daftarkan pendengar untuk diberitahu ketika seekor hewan ditambahkan zoo.registeranimaladdedlistener (printNeanimaladdedlistener baru ()); zoo.registeranimaladdedlistener (countanimaladdedlistener baru ()); // Tambahkan hewan, beri tahu kebun binatang pendengar terdaftar. zoo.addanimal (hewan baru ("singa")); zoo.addanimal (hewan baru ("beruang")); }}Hasil outputnya adalah:
Menambahkan hewan baru dengan nama 'Hewan Tiger'total Ditambahkan: 1 menanamkan hewan baru dengan nama' Hewan Singa yang ditambahkan: 2 diadded seekor hewan baru dengan nama 'Hewan Bear'total Ditambahkan: 3
Pengguna dapat membuat pendengar apa pun jika hanya memodifikasi kode pendaftaran pendengar. Skalabilitas ini terutama karena subjek dikaitkan dengan antarmuka pengamat, daripada secara langsung terkait dengan ConcreteObserver. Selama antarmuka tidak dimodifikasi, tidak perlu memodifikasi subjek antarmuka.
Kelas internal anonim, fungsi lambda dan pendaftaran pendengar
Peningkatan besar dalam Java 8 adalah penambahan fitur fungsional, seperti penambahan fungsi lambda. Sebelum memperkenalkan fungsi Lambda, Java menyediakan fungsi serupa melalui kelas internal anonim, yang masih digunakan dalam banyak aplikasi yang ada. Dalam mode pengamat, pendengar baru dapat dibuat kapan saja tanpa membuat kelas pengamat tertentu. Misalnya, kelas PrintNeAnimaladdedListener dapat diimplementasikan dalam fungsi utama dengan kelas internal anonim. Kode implementasi spesifik adalah sebagai berikut:
kelas publik {public static void main (string [] args) {// Buat kebun binatang untuk menyimpan binatang kebun binatang = zoo baru (); // Daftarkan pendengar untuk diberi tahu ketika seekor hewan ditambahkan zoo.registeranimaladdedlistener (new animaladdedlistener () {@Override public void updateAnimaladded (hewan hewan) {// cetak nama hewan yang baru ditambahkan.out.println ("menambahkan hewan baru dengan nama hewan ' + getname () () (" ");"); "" ditambahkan hewan baru dengan nama hewan " + getname. // Tambahkan hewan, beri tahu kebun binatang pendengar terdaftar. }}Demikian pula, fungsi Lambda juga dapat digunakan untuk menyelesaikan tugas -tugas tersebut:
kelas publik {public static void main (string [] args) {// Buat kebun binatang untuk menyimpan binatang kebun binatang = zoo baru (); // Daftarkan pendengar untuk diberitahu ketika hewan ditambahkan zoo.registeranimaladdedlistener ((hewan) -> system.out.println ("Menambahkan hewan baru dengan nama '" + animal.getname () + "'")); // Tambahkan hewan, beri tahu kebun binatang pendengar terdaftar. }}Perlu dicatat bahwa fungsi lambda hanya cocok untuk situasi di mana hanya ada satu fungsi di antarmuka pendengar. Meskipun persyaratan ini tampaknya ketat, banyak pendengar sebenarnya adalah fungsi tunggal, seperti animaladdedlistener dalam contoh. Jika antarmuka memiliki banyak fungsi, Anda dapat memilih untuk menggunakan kelas dalam anonim.
Ada masalah seperti itu dengan pendaftaran implisit dari pendengar yang dibuat: karena objek dibuat dalam ruang lingkup panggilan pendaftaran, tidak mungkin untuk menyimpan referensi ke pendengar tertentu. Ini berarti bahwa pendengar yang terdaftar melalui fungsi Lambda atau kelas internal anonim tidak dapat dicabut karena fungsi pencabutan memerlukan referensi ke pendengar terdaftar. Cara mudah untuk menyelesaikan masalah ini adalah dengan mengembalikan referensi ke pendengar terdaftar dalam fungsi registeranimaladdedlistener. Dengan cara ini, Anda dapat membatalkan pendaftaran pendengar yang dibuat dengan fungsi Lambda atau kelas internal anonim. Kode metode yang ditingkatkan adalah sebagai berikut:
Public AnimalAddedListener Registeranimaladdedlistener (AnimalAddedListener Listener) {// Tambahkan pendengar ke daftar pendengar terdaftar this.listeners.add (pendengar); mengembalikan pendengar;}Kode klien untuk interaksi fungsi yang dirancang ulang adalah sebagai berikut:
kelas publik {public static void main (string [] args) {// Buat kebun binatang untuk menyimpan binatang kebun binatang = zoo baru (); // Daftarkan pendengar untuk diberitahu ketika hewan ditambahkan animaladdedlistener listener = zoo.registeranimaladdedlistener ((hewan) -> system.out.println ("Menambahkan hewan baru dengan nama '" + animal.getname () + "'")); // Tambahkan hewan, beri tahu kebun binatang pendengar terdaftar. // Buka register pendengar kebun binatang. // Tambahkan hewan lain, yang tidak akan mencetak nama, karena pendengar // sebelumnya tidak terdaftar zoo.addanimal (hewan baru ("singa")); }}Output hasil saat ini hanya ditambahkan hewan baru dengan nama 'Tiger', karena pendengar telah dibatalkan sebelum hewan kedua ditambahkan:
Menambahkan hewan baru dengan nama 'Tiger'
Jika solusi yang lebih kompleks diadopsi, fungsi register juga dapat mengembalikan kelas penerima sehingga pendengar yang tidak terdaftar dipanggil, misalnya:
kelas publik animaladdedlistenerreceipt {private final animaladdedistener listener; Public AnimalAddedListenerReceipt (AnimalAddedListener Listener) {this.listener = pendengar; } public final animalAddedistener getListener () {return this.listener; }}Kwitansi akan digunakan sebagai nilai pengembalian fungsi pendaftaran dan parameter input dari fungsi pendaftaran dibatalkan. Saat ini, implementasi kebun binatang adalah sebagai berikut:
Kelas Publik ZoousingReceipt {// ... Atribut dan Konstruktor yang Ada ... Public AnimalAddedListenerReceipt RegisteranimaladdedListener (AnimalAddedListener Listener) {// Tambahkan pendengar ke daftar pendengar terdaftar this.listeners.add (pendengar); kembalikan animalAddedListenerReceipt baru (pendengar); } public void unregisteranimaladdedlistener (AnimalAddedListenerReceipt Reception) {// Hapus pendengar dari daftar pendengar terdaftar this.listeners.remove (receipt.getListener ()); } // ... metode pemberitahuan yang ada ...}Mekanisme implementasi penerima yang dijelaskan di atas memungkinkan penyimpanan informasi untuk panggilan ke pendengar saat dicabut, yaitu, jika algoritma pendaftaran pencabutan tergantung pada status pendengar ketika subjek mendaftarkan pendengar, status ini akan disimpan. Jika pendaftaran pencabutan hanya memerlukan referensi ke pendengar terdaftar sebelumnya, teknologi penerimaan akan tampak merepotkan dan tidak disarankan.
Selain pendengar khusus yang sangat kompleks, cara paling umum untuk mendaftarkan pendengar adalah melalui fungsi lambda atau melalui kelas internal anonim. Tentu saja, ada pengecualian, yaitu kelas yang berisi subjek mengimplementasikan antarmuka pengamat dan mendaftarkan pendengar yang menyebut target referensi. Kasus seperti yang ditunjukkan pada kode berikut:
zoocontainer kelas publik mengimplementasikan animalAddedListener {private zoo zoo = new zoo (); zoocontainer publik () {// Daftarkan objek ini sebagai pendengar this.zoo.registeranimaladdedlistener (ini); } public zoo getzoo () {return this.zoo; } @Override public void updateAnimalAdded (hewan hewan) {System.out.println ("ditambahkan hewan dengan nama '" + animal.getName () + "'"); } public static void main (string [] args) {// Buat zoo container zoocontainer zoocontainer = new zoocontainer (); // Tambahkan hewan memberi tahu pendengar yang diberitahu dalam zoocontainer.getzoo (). Addanimal (hewan baru ("harimau")); }}Pendekatan ini hanya cocok untuk kasus -kasus sederhana dan kodenya tampaknya tidak cukup profesional, dan masih sangat populer dengan pengembang Java modern, jadi perlu untuk memahami cara kerja contoh ini. Karena zoocontainer mengimplementasikan antarmuka animaladdedlistener, maka instance (atau objek) zoocontainer dapat didaftarkan sebagai animaladdedlistener. Di kelas Zoocontainer, referensi ini mewakili contoh objek saat ini, yaitu, zoocontainer, dan dapat digunakan sebagai animaladdedlistener.
Secara umum, tidak semua kelas kontainer diperlukan untuk mengimplementasikan fungsi -fungsi tersebut, dan kelas kontainer yang mengimplementasikan antarmuka pendengar hanya dapat memanggil fungsi pendaftaran subjek, tetapi cukup lulus referensi ke fungsi register sebagai objek pendengar. Dalam bab -bab berikut, FAQ dan solusi untuk lingkungan multithreaded akan diperkenalkan.
Implementasi keselamatan utas <br /> Bab sebelumnya memperkenalkan implementasi pola pengamat di lingkungan Java modern. Meskipun sederhana tetapi lengkap, implementasi ini mengabaikan masalah utama: keamanan utas. Sebagian besar aplikasi Java yang terbuka adalah multi-threaded, dan mode pengamat sebagian besar digunakan dalam sistem multi-threaded atau asinkron. Misalnya, jika layanan eksternal memperbarui basis data, aplikasi juga akan menerima pesan secara tidak sinkron dan kemudian memberi tahu komponen internal untuk memperbarui dalam mode pengamat, alih -alih mendaftarkan dan mendengarkan layanan eksternal secara langsung.
Keamanan utas dalam mode pengamat terutama difokuskan pada tubuh mode, karena konflik utas cenderung terjadi ketika memodifikasi koleksi pendengar terdaftar. Misalnya, satu utas mencoba menambahkan pendengar baru, sementara utas lainnya mencoba menambahkan objek hewan baru, yang memicu pemberitahuan untuk semua pendengar terdaftar. Mengingat urutan urutan, utas pertama mungkin atau mungkin belum menyelesaikan pendaftaran pendengar baru sebelum pendengar terdaftar menerima pemberitahuan tentang hewan yang ditambahkan. Ini adalah kasus klasik kompetisi sumber daya utas, dan fenomena inilah yang memberi tahu pengembang bahwa mereka membutuhkan mekanisme untuk memastikan keamanan utas.
Solusi termudah untuk masalah ini adalah: semua operasi yang mengakses atau memodifikasi daftar pendengar pendaftaran harus mengikuti mekanisme sinkronisasi Java, seperti:
Publik yang disinkronkan animalAddedListener Registeranimaladdedlistener (animaladdedlistener listener) {/*...*/} public disinkronkan void unregisteranimaladdedlistener (animalAddedListener pendengar) {/*...* (hewani (hewani) {hius {hewine {hewine {{hius {{hius {{hius {{hius {{public noifyAmalAddededlendedlendedlendedlendedlendedlendedlended (/*Dengan cara ini, pada saat yang sama, hanya satu utas yang dapat memodifikasi atau mengakses daftar pendengar terdaftar, yang dapat berhasil menghindari masalah kompetisi sumber daya, tetapi masalah baru muncul, dan kendala seperti itu terlalu ketat (untuk informasi lebih lanjut tentang kata kunci yang disinkronkan dan model konkurensi Java, silakan merujuk ke halaman web resmi). Melalui sinkronisasi metode, akses bersamaan ke daftar pendengar dapat diamati setiap saat. Mendaftarkan dan mencabut pendengar adalah operasi penulisan untuk daftar pendengar, sambil memberi tahu pendengar untuk mengakses daftar pendengar adalah operasi read-only. Karena akses melalui pemberitahuan adalah operasi baca, beberapa operasi pemberitahuan dapat dilakukan secara bersamaan.
Oleh karena itu, selama tidak ada pendaftaran atau pencabutan pendengar, selama pendaftaran tidak terdaftar, selama sejumlah pemberitahuan bersamaan dapat dieksekusi secara bersamaan tanpa memicu persaingan sumber daya untuk daftar pendengar terdaftar. Tentu saja, kompetisi sumber daya dalam situasi lain telah ada sejak lama. Untuk mengatasi masalah ini, penguncian sumber daya untuk ReadWritelock dirancang untuk mengelola operasi baca dan menulis secara terpisah. Kode implementasi ThreadSafezoo yang aman dari kelas Zoo adalah sebagai berikut:
Public Class ThreadSafezoo {private final readwritelock readwritelock = baru reentrantreadwritelock (); Readlock kunci akhir yang dilindungi = readwritelock.readlock (); Writelock kunci akhir yang dilindungi = readwritelock.writelock (); Daftar Pribadi <En Animal> Hewan = ArrayList baru <> (); Daftar Privat <AnlyAddedListener> pendengar = ArrayList baru <> (); public void addanimal (hewan hewan) {// tambahkan hewan ke daftar hewan this.animals.add (hewan); // Beri tahu daftar pendengar terdaftar ini. NotifyAnimaladdedlisteners (Hewan); } public AnimalAddedistenerer RegisteranimaladdedListener (AnimalAddedListener Listener) {// Kunci daftar pendengar untuk menulis this.writelock.lock (); coba {// tambahkan pendengar ke daftar pendengar terdaftar this.listeners.add (pendengar); } akhirnya {// buka kunci penulis kunci this.writelock.unlock (); } return listener; } public void unregisteranimaladdedlistener (animalAddedListener listener) {// Kunci daftar pendengar untuk menulis this.writelock.lock (); coba {// hapus pendengar dari daftar pendengar terdaftar this.listeners.remove (pendengar); } akhirnya {// buka kunci penulis kunci this.writelock.unlock (); }} public void notifyanimaladdedlisteners (hewan hewan) {// Kunci daftar pendengar untuk membaca this.readlock.lock (); Coba {// beri tahu masing -masing pendengar dalam daftar pendengar terdaftar this.listeners.foreach (pendengar -> listener.updateanimaladded (hewan)); } akhirnya {// buka kunci pembaca kunci this.readlock.unlock (); }}}Melalui penyebaran tersebut, implementasi subjek dapat memastikan keamanan utas dan banyak utas dapat mengeluarkan pemberitahuan secara bersamaan. Namun terlepas dari ini, masih ada dua masalah kompetisi sumber daya yang tidak dapat diabaikan:
Akses bersamaan ke setiap pendengar. Beberapa utas dapat memberi tahu pendengar bahwa hewan baru diperlukan, yang berarti bahwa pendengar dapat dipanggil oleh banyak utas secara bersamaan.
Akses Bersamaan ke Daftar Hewan. Beberapa utas dapat menambahkan objek ke daftar hewan secara bersamaan. Jika urutan pemberitahuan memiliki dampak, itu dapat menyebabkan persaingan sumber daya, yang membutuhkan mekanisme pemrosesan operasi bersamaan untuk menghindari masalah ini. Jika daftar pendengar terdaftar menerima pemberitahuan untuk menambahkan Animal2 dan kemudian menerima pemberitahuan untuk menambahkan hewan, kompetisi sumber daya akan terjadi. Namun, jika penambahan Animal1 dan Animal2 dilakukan oleh utas yang berbeda, juga dimungkinkan untuk menyelesaikan penambahan hewan 1 sebelum hewan. Secara khusus, Thread 1 menambahkan Animal1 sebelum memberi tahu pendengar dan mengunci modul, Thread 2 menambahkan Animal2 dan memberi tahu pendengar, dan kemudian Thread 1 memberi tahu pendengar bahwa Animal1 telah ditambahkan. Meskipun persaingan sumber daya dapat diabaikan ketika urutan urutan tidak dipertimbangkan, masalahnya nyata.
Akses Bersamaan dengan Pendengar
并发访问监听器可以通过保证监听器的线程安全来实现。秉承着类的“责任自负”精神,监听器有“义务”确保自身的线程安全。例如,对于前面计数的监听器,多线程的递增或递减动物数量可能导致线程安全问题,要避免这一问题,动物数的计算必须是原子操作(原子变量或方法同步),具体解决代码如下:
public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener { private static AtomicLong animalsAddedCount = new AtomicLong(0); @Override public void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount.incrementAndGet(); // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}方法同步解决方案代码如下:
public class CountingAnimalAddedListener implements AnimalAddedListener { private static int animalsAddedCount = 0; @Override public synchronized void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount++; // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}要强调的是监听器应该保证自身的线程安全,subject需要理解监听器的内部逻辑,而不是简单确保对监听器的访问和修改的线程安全。否则,如果多个subject共用同一个监听器,那每个subject类都要重写一遍线程安全的代码,显然这样的代码不够简洁,因此需要在监听器类内实现线程安全。
监听器的有序通知当要求监听器有序执行时,读写锁就不能满足需求了,而需要引入一个新的机制,可以保证notify函数的调用顺序和animal添加到zoo的顺序一致。有人尝试过用方法同步来实现,然而根据Oracle文档中的方法同步介绍,可知方法同步并不提供操作执行的顺序管理。它只是保证原子操作,也就是说操作不会被打断,并不能保证先来先执行(FIFO)的线程顺序。ReentrantReadWriteLock可以实现这样的执行顺序,代码如下:
public class OrderedThreadSafeZoo { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<Animal> animals = new ArrayList<>(); private List<AnimalAddedListener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Add the listener to the list of registered listeners this.listeners.add(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } return listener; } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } } public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners this.listeners.forEach(listener -> listener.updateAnimalAdded(animal)); } finally { // Unlock the reader lock this.readLock.unlock(); }}}这样的实现方式,register, unregister和notify函数将按照先进先出(FIFO)的顺序获得读写锁权限。例如,线程1注册一个监听器,线程2在开始执行注册操作后试图通知已注册的监听器,线程3在线程2等待只读锁的时候也试图通知已注册的监听器,采用fair-ordering方式,线程1先完成注册操作,然后线程2可以通知监听器,最后线程3通知监听器。这样保证了action的执行顺序和开始顺序一致。
如果采用方法同步,虽然线程2先排队等待占用资源,线程3仍可能比线程2先获得资源锁,而且不能保证线程2比线程3先通知监听器。问题的关键所在:fair-ordering方式可以保证线程按照申请资源的顺序执行。读写锁的顺序机制很复杂,应参照ReentrantReadWriteLock的官方文档以确保锁的逻辑足够解决问题。
截止目前实现了线程安全,在接下来的章节中将介绍提取主题的逻辑并将其mixin类封装为可重复代码单元的方式优缺点。
主题逻辑封装到Mixin类<br />把上述的观察者模式设计实现封装到目标的mixin类中很具吸引力。通常来说,观察者模式中的观察者包含已注册的监听器的集合;负责注册新的监听器的register函数;负责撤销注册的unregister函数和负责通知监听器的notify函数。对于上述的动物园的例子,zoo类除动物列表是问题所需外,其他所有操作都是为了实现主题的逻辑。
Mixin类的案例如下所示,需要说明的是为使代码更为简洁,此处去掉关于线程安全的代码:
public abstract class ObservableSubjectMixin<ListenerType> { private List<ListenerType> listeners = new ArrayList<>(); public ListenerType registerListener (ListenerType listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); return listener; } public void unregisterAnimalAddedListener (ListenerType listener) { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } public void notifyListeners (Consumer<? super ListenerType> algorithm) { // Execute some function on each of the listeners this.listeners.forEach(algorithm); }}正因为没有提供正在注册的监听器类型的接口信息,不能直接通知某个特定的监听器,所以正需要保证通知功能的通用性,允许客户端添加一些功能,如接受泛型参数类型的参数匹配,以适用于每个监听器,具体实现代码如下:
public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.updateAnimalAdded(animal)); }}Mixin类技术的最大优势是把观察者模式的Subject封装到一个可重复调用的类中,而不是在每个subject类中都重复写这些逻辑。此外,这一方法使得zoo类的实现更为简洁,只需要存储动物信息,而不用再考虑如何存储和通知监听器。
然而,使用mixin类并非只有优点。比如,如果要存储多个类型的监听器怎么办?例如,还需要存储监听器类型AnimalRemovedListener。mixin类是抽象类,Java中不能同时继承多个抽象类,而且mixin类不能改用接口实现,这是因为接口不包含state,而观察者模式中state需要用来保存已经注册的监听器列表。
其中的一个解决方案是创建一个动物增加和减少时都会通知的监听器类型ZooListener,代码如下所示:
public interface ZooListener { public void onAnimalAdded (Animal animal); public void onAnimalRemoved (Animal animal);}这样就可以使用该接口实现利用一个监听器类型对zoo状态各种变化的监听了:
public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalAdded(animal)); } public void removeAnimal (Animal) animal) { // Remove the animal from the list of animals this.animals.remove(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalRemoved(animal)); }}将多个监听器类型合并到一个监听器接口中确实解决了上面提到的问题,但仍旧存在不足之处,接下来的章节会详细讨论。
Multi-Method监听器和适配器
在上述方法,监听器的接口中实现的包含太多函数,接口就过于冗长,例如,Swing MouseListener就包含5个必要的函数。尽管可能只会用到其中一个,但是只要用到鼠标点击事件就必须要添加这5个函数,更多可能是用空函数体来实现剩下的函数,这无疑会给代码带来不必要的混乱。
其中一种解决方案是创建适配器(概念来自GoF提出的适配器模式),适配器中以抽象函数的形式实现监听器接口的操作,供具体监听器类继承。这样一来,具体监听器类就可以选择其需要的函数,对adapter不需要的函数采用默认操作即可。例如上面例子中的ZooListener类,创建ZooAdapter(Adapter的命名规则与监听器一致,只需要把类名中的Listener改为Adapter即可),代码如下:
public class ZooAdapter implements ZooListener { @Override public void onAnimalAdded (Animal animal) {} @Override public void onAnimalRemoved (Animal animal) {}}乍一看,这个适配器类微不足道,然而它所带来的便利却是不可小觑的。比如对于下面的具体类,只需选择对其实现有用的函数即可:
public class NamePrinterZooAdapter extends ZooAdapter { @Override public void onAnimalAdded (Animal animal) { // Print the name of the animal that was added System.out.println("Added animal named " + animal.getName()); }}有两种替代方案同样可以实现适配器类的功能:一是使用默认函数;二是把监听器接口和适配器类合并到一个具体类中。默认函数是Java8新提出的,在接口中允许开发者提供默认(防御)的实现方法。
Java库的这一更新主要是方便开发者在不改变老版本代码的情况下,实现程序扩展,因此应该慎用这个方法。部分开发者多次使用后,会感觉这样写的代码不够专业,而又有开发者认为这是Java8的特色,不管怎样,需要明白这个技术提出的初衷是什么,再结合具体问题决定是否要用。使用默认函数实现的ZooListener接口代码如下示:
public interface ZooListener { default public void onAnimalAdded (Animal animal) {} default public void onAnimalRemoved (Animal animal) {}}通过使用默认函数,实现该接口的具体类,无需在接口中实现全部函数,而是选择性实现所需函数。虽然这是接口膨胀问题一个较为简洁的解决方案,开发者在使用时还应多加注意。
第二种方案是简化观察者模式,省略了监听器接口,而是用具体类实现监听器的功能。比如ZooListener接口就变成了下面这样:
public class ZooListener { public void onAnimalAdded (Animal animal) {} public void onAnimalRemoved (Animal animal) {}}这一方案简化了观察者模式的层次结构,但它并非适用于所有情况,因为如果把监听器接口合并到具体类中,具体监听器就不可以实现多个监听接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口写在同一个具体类中,那么单独一个具体监听器就不可以同时实现这两个接口了。此外,监听器接口的意图比具体类更显而易见,很显然前者就是为其他类提供接口,但后者就并非那么明显了。
如果没有合适的文档说明,开发者并不会知道已经有一个类扮演着接口的角色,实现了其对应的所有函数。此外,类名不包含adapter,因为类并不适配于某一个接口,因此类名并没有特别暗示此意图。综上所述,特定问题需要选择特定的方法,并没有哪个方法是万能的。
在开始下一章前,需要特别提一下,适配器在观察模式中很常见,尤其是在老版本的Java代码中。Swing API正是以适配器为基础实现的,正如很多老应用在Java5和Java6中的观察者模式中所使用的那样。zoo案例中的监听器或许并不需要适配器,但需要了解适配器提出的目的以及其应用,因为我们可以在现有的代码中对其进行使用。下面的章节,将会介绍时间复杂的监听器,该类监听器可能会执行耗时的运算或进行异步调用,不能立即给出返回值。
Complex & Blocking监听器关于观察者模式的一个假设是:执行一个函数时,一系列监听器会被调用,但假定这一过程对调用者而言是完全透明的。例如,客户端代码在Zoo中添加animal时,在返回添加成功之前,并不知道会调用一系列监听器。如果监听器的执行需要时间较长(其时间受监听器的数量、每个监听器执行时间影响),那么客户端代码将会感知这一简单增加动物操作的时间副作用。
本文不能面面俱到的讨论这个话题,下面几条是开发者调用复杂的监听器时应该注意的事项:
监听器启动新线程。新线程启动后,在新线程中执行监听器逻辑的同时,返回监听器函数的处理结果,并运行其他监听器执行。
Subject启动新线程。与传统的线性迭代已注册的监听器列表不同,Subject的notify函数重启一个新的线程,然后在新线程中迭代监听器列表。这样使得notify函数在执行其他监听器操作的同时可以输出其返回值。需要注意的是需要一个线程安全机制来确保监听器列表不会进行并发修改。
队列化监听器调用并采用一组线程执行监听功能。将监听器操作封装在一些函数中并队列化这些函数,而非简单的迭代调用监听器列表。这些监听器存储到队列中后,线程就可以从队列中弹出单个元素并执行其监听逻辑。这类似于生产者-消费者问题,notify过程产生可执行函数队列,然后线程依次从队列中取出并执行这些函数,函数需要存储被创建的时间而非执行的时间供监听器函数调用。例如,监听器被调用时创建的函数,那么该函数就需要存储该时间点,这一功能类似于Java中的如下操作:
public class
如何使用Java8 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!