Toko online ingin memberi tahu pelanggan saat stok buku yang mereka nanti sudah tersedia kembali. Solusi langsung yang terpikir: toko memanggil kirimEmailBudi(), kirimSmsSiti() satu per satu — dan setiap pelanggan baru menambah satu panggilan di kode toko. Observer pattern membalik arahnya: toko hanya mengumumkan “stok produk ini berubah”, siapa pun yang berminat mendaftar sebagai pendengar, dan toko tidak perlu mengenal satu pelanggan pun secara langsung. Pola ini adalah fondasi pemrograman berbasis peristiwa (event-driven) yang kita jumpai dari antarmuka pengguna sampai sistem olahpesan besar. Chapter ini membangun notifikasi stok yang bisa dijalankan, lengkap dengan jebakan urutan dan kebocoran pendengar yang sering menghantui pola ini.
Subject dan Observer #
Dua peran bekerja sama di sini. Subject adalah pemilik peristiwa: subject menyimpan daftar pendengar dan menyediakan cara mendaftar. Observer adalah antarmuka yang dipanggil saat peristiwa terjadi. Mekanisme penghubungnya sederhana: method notifikasi() di subject menelusuri daftar pendengar dan memanggil method update pada masing-masing — dynamic dispatch JVM yang memilih implementasi email, sms, atau apa pun di balik antarmuka.
Hasil decoupling-nya dua arah. Subject hanya mengenal antarmuka Pelanggan, sehingga jenis pendengar bisa bertambah tanpa menyentuh class toko; pendengar hanya mengenal data peristiwa, sehingga toko bisa berganti mesin notifikasi tanpa menyentuh class pendengar. Ketergantungan timbal balik yang tadinya melekat kini hanya bertemu di satu titik kecil: daftar langganan.
Notifikasi Stok dalam Kode #
Program berikut membangun tokonya lengkap dengan pelanggan email dan sms yang berlangganan produk berbeda:
import java.util.ArrayList;
import java.util.List;
public class NotifikasiStok {
public static void main(String[] args) {
Toko toko = new Toko();
// Pelanggan hanya berlangganan ke produk yang mereka minati
toko.daftar("Buku Java", new PelangganEmail("[email protected]"));
toko.daftar("Buku Java", new PelangganSms("Siti", "0812-1111-2222"));
toko.daftar("Pena", new PelangganEmail("[email protected]"));
toko.stokBerubah("Buku Java", 3); // hanya Budi dan Siti yang menerima
toko.stokBerubah("Pena", 0); // hanya Dewi
toko.stokBerubah("Tas", 5); // tidak ada pelanggan berlangganan
}
}
// Observer: antarmuka yang dipanggil subject saat peristiwa terjadi
interface Pelanggan {
void terima(String produk, int stokBaru);
}
class PelangganEmail implements Pelanggan {
private final String alamat;
PelangganEmail(String alamat) {
this.alamat = alamat;
}
@Override
public void terima(String produk, int stokBaru) {
System.out.println(" -> email ke " + alamat
+ ": stok \"" + produk + "\" kini " + stokBaru);
}
}
class PelangganSms implements Pelanggan {
private final String nama;
private final String nomor;
PelangganSms(String nama, String nomor) {
this.nama = nama;
this.nomor = nomor;
}
@Override
public void terima(String produk, int stokBaru) {
System.out.println(" -> sms ke " + nomor + " (" + nama
+ "): stok \"" + produk + "\" kini " + stokBaru);
}
}
// Subject: menyimpan daftar pendengar dan siarkan peristiwa tanpa mengenal detailnya
class Toko {
private final List<Langganan> daftarLangganan = new ArrayList<>();
private record Langganan(String produk, Pelanggan pelanggan) {
}
void daftar(String produk, Pelanggan pelanggan) {
daftarLangganan.add(new Langganan(produk, pelanggan));
}
void stokBerubah(String produk, int stokBaru) {
System.out.println("stok \"" + produk + "\" berubah menjadi " + stokBaru);
// Urutan pemberitahuan mengikuti urutan pendaftaran: dapat diprediksi
for (Langganan langganan : daftarLangganan) {
if (langganan.produk().equals(produk)) {
langganan.pelanggan().terima(produk, stokBaru);
}
}
}
}Output yang Dihasilkan:
stok "Buku Java" berubah menjadi 3
-> email ke [email protected]: stok "Buku Java" kini 3
-> sms ke 0812-1111-2222 (Siti): stok "Buku Java" kini 3
stok "Pena" berubah menjadi 0
-> email ke [email protected]: stok "Pena" kini 0
stok "Tas" berubah menjadi 5Mari kita bedah perannya satu per satu:
- Baris 20-22 (
interface Pelanggan): kontrak observer satu method. Subject memanggil method ini dan tidak tahu — juga tidak perlu tahu — apakah di baliknya berdiri email, sms, atau pencatat audit. - Baris 55-63 (
class Tokodanrecord Langganan): subject menyimpan daftar langganan sebagaiListdan memasangkan tiap pendengar dengan produk yang diminati.Langganansengaja dibuatprivateagar bentuk penyimpanan internal boleh berganti tanpa mengubah API toko. - Baris 65-74 (
stokBerubah): satu-satunya method yang menyiarkan peristiwa. Iterasi mengikuti urutan pendaftaran sehingga hasil eksekusi dapat diprediksi — di program nyata dengan banyak pendengar, urutan seperti ini tetap sebaiknya dijaga atau didokumentasikan. - Baris 7-15 (
main): toko tidak pernah menyebutPelangganEmailatauPelangganSms; pendaftaran datang dari luar. Peristiwa “Tas” tidak menghasilkan notifikasi karena tidak ada langganan untuk produk tersebut — subject memang tidak wajib punya pendengar.
Observer di JDK dan Luar Sana #
JDK lama pernah menyediakan java.util.Observer bersama java.util.Observable, lalu keduanya ditandai deprecated sejak Java 9 karena desainnya yang kaku dan tidak aman untuk thread. Penggantinya lebih modern: java.util.concurrent.Flow (sejak Java 9) menstandarkan aliran reaktif berbasis publisher–subscriber untuk beban asinkron, dan PropertyChangeListener di modul java.desktop masih dipakai untuk perubahan properti komponen antarmuka. Di luar JDK, event bus pada framework aplikasi dan message broker seperti Kafka pada arsitektur mikro layanan adalah observer berskala besar — subject dan observernya bahkan tidak lagi tinggal di satu proses, topik yang kita singgung kembali di Section Microservices dan Distributed Systems.
Observer menuntaskan pemisahan siapa tahu apa. Pola berikutnya memisahkan hal lain: bentuk data yang tidak cocok. Ketika sistem lawas bicara format CSV dan kode modern meminta objek Cuaca, adapter pattern menjadi penerjemah di antara keduanya.
Referensi #
- Oracle: JDK 25 API — java.util.concurrent.Flow
- Oracle: JDK 25 API — Observer (deprecated)
- Oracle: The Java Tutorials (dev.java)
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
NotifikasiStok.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class NotifikasiStok {
public static void main(String[] args) {
Toko toko = new Toko();
// Pelanggan hanya berlangganan ke produk yang mereka minati
toko.daftar("Buku Java", new PelangganEmail("[email protected]"));
toko.daftar("Buku Java", new PelangganSms("Siti", "0812-1111-2222"));
toko.daftar("Pena", new PelangganEmail("[email protected]"));
toko.stokBerubah("Buku Java", 3); // hanya Budi dan Siti yang menerima
toko.stokBerubah("Pena", 0); // hanya Dewi
toko.stokBerubah("Tas", 5); // tidak ada pelanggan berlangganan
}
}
// Observer: antarmuka yang dipanggil subject saat peristiwa terjadi
interface Pelanggan {
void terima(String produk, int stokBaru);
}
class PelangganEmail implements Pelanggan {
private final String alamat;
PelangganEmail(String alamat) {
this.alamat = alamat;
}
@Override
public void terima(String produk, int stokBaru) {
System.out.println(" -> email ke " + alamat
+ ": stok \"" + produk + "\" kini " + stokBaru);
}
}
class PelangganSms implements Pelanggan {
private final String nama;
private final String nomor;
PelangganSms(String nama, String nomor) {
this.nama = nama;
this.nomor = nomor;
}
@Override
public void terima(String produk, int stokBaru) {
System.out.println(" -> sms ke " + nomor + " (" + nama
+ "): stok \"" + produk + "\" kini " + stokBaru);
}
}
// Subject: menyimpan daftar pendengar dan siarkan peristiwa tanpa mengenal detailnya
class Toko {
private final List<Langganan> daftarLangganan = new ArrayList<>();
private record Langganan(String produk, Pelanggan pelanggan) {
}
void daftar(String produk, Pelanggan pelanggan) {
daftarLangganan.add(new Langganan(produk, pelanggan));
}
void stokBerubah(String produk, int stokBaru) {
System.out.println("stok \"" + produk + "\" berubah menjadi " + stokBaru);
// Urutan pemberitahuan mengikuti urutan pendaftaran: dapat diprediksi
for (Langganan langganan : daftarLangganan) {
if (langganan.produk().equals(produk)) {
langganan.pelanggan().terima(produk, stokBaru);
}
}
}
}