Observer Pattern #
Sebuah pesanan baru masuk ke sistem e-commerce. Akibatnya, banyak hal harus terjadi: inventory harus dikurangi, email konfirmasi harus dikirim, tim fulfillment harus dinotifikasi, analytics harus diupdate, dan loyalty points harus ditambahkan ke akun pembeli. Tanpa Observer Pattern, OrderService harus memanggil semuanya secara eksplisit — setiap subsistem baru yang perlu tahu tentang pesanan baru berarti satu baris kode tambahan di OrderService. OrderService yang seharusnya hanya mengurusi logika pesanan, akhirnya mengurusi inventory, email, analytics, dan loyalty sekaligus — dan setiap kali ada subsistem baru, ia harus diubah. Observer Pattern membalik tanggung jawab ini: OrderService hanya mengumumkan “pesanan baru masuk”, dan siapapun yang peduli bisa mendaftarkan diri untuk mendengar pengumuman itu — tanpa OrderService perlu tahu siapa yang mendengar atau berapa banyak.
Apa itu Observer Pattern? #
Observer Pattern adalah behavioral design pattern yang mendefinisikan mekanisme subscription untuk memberi tahu banyak objek tentang kejadian yang terjadi pada objek yang mereka pantau, tanpa objek yang dipantau (Subject) perlu tahu siapa yang memantaunya atau apa yang mereka lakukan dengan informasi tersebut.
Dua peran utama dalam Observer Pattern:
- Subject (Publisher) — objek yang menyimpan state dan memberi tahu observer ketika state berubah; tidak tahu siapa observer-nya, hanya tahu bahwa mereka ada
- Observer (Subscriber) — objek yang mendaftarkan diri untuk mendapat notifikasi; bertanggung jawab atas apa yang dilakukan ketika notifikasi diterima
Tiga properti yang mendefinisikan Observer Pattern:
- Loose coupling — Subject tidak bergantung pada Observer konkret; keduanya hanya saling kenal melalui interface
- Dynamic subscription — Observer bisa didaftarkan dan dicabut saat runtime, tanpa Subject perlu diubah
- Broadcast — satu notifikasi dari Subject menjangkau semua Observer yang terdaftar sekaligus
sequenceDiagram
participant OS as OrderService (Subject)
participant IS as InventoryObserver
participant ES as EmailObserver
participant AS as AnalyticsObserver
Note over IS,AS: Register diri ke OrderService
IS->>OS: Register(inventoryObs)
ES->>OS: Register(emailObs)
AS->>OS: Register(analyticsObs)
Note over OS: Pesanan baru masuk
OS->>OS: CreateOrder(req)
OS->>IS: Notify(OrderCreatedEvent)
OS->>ES: Notify(OrderCreatedEvent)
OS->>AS: Notify(OrderCreatedEvent)
IS-->>IS: kurangi stok
ES-->>ES: kirim email konfirmasi
AS-->>AS: record event
Masalah yang Dipecahkan #
Observer Pattern menyelesaikan dua masalah yang saling berkaitan: tight coupling antara Subject dan pihak yang bereaksi terhadap perubahannya, dan Open-Closed Principle yang dilanggar setiap kali ada reaksi baru yang perlu ditambahkan.
Masalah: Subject Tahu Terlalu Banyak #
// ANTI-PATTERN: OrderService memanggil semua subsistem secara langsung
func (s *OrderService) CreateOrder(req CreateOrderRequest) (*Order, error) {
order, err := s.repo.Create(req)
if err != nil {
return nil, err
}
// OrderService harus tahu tentang inventory
s.inventorySvc.DeductStock(order.Items)
// OrderService harus tahu tentang email
s.emailSvc.SendConfirmation(order.UserEmail, order)
// OrderService harus tahu tentang analytics
s.analyticsSvc.TrackPurchase(order)
// OrderService harus tahu tentang loyalty
s.loyaltySvc.AddPoints(order.UserID, order.TotalAmount)
// Tambah WhatsApp notification? Ubah OrderService lagi.
// Tambah fraud detection? Ubah OrderService lagi.
// Total coupling yang terjadi: OrderService tahu 4+ subsistem
return order, nil
}
// BENAR: OrderService hanya tahu bahwa ada event, bukan siapa yang perlu tahu
func (s *OrderService) CreateOrder(req CreateOrderRequest) (*Order, error) {
order, err := s.repo.Create(req)
if err != nil {
return nil, err
}
// Satu baris — tidak peduli siapa yang mendengar atau berapa banyak
s.eventBus.Publish(OrderCreatedEvent{Order: order})
return order, nil
}
// Tambah WhatsApp? Daftarkan WhatsAppObserver — OrderService tidak berubah
// Tambah fraud detection? Daftarkan FraudObserver — OrderService tidak berubah
Struktur dan Komponen #
classDiagram
class Subject {
<<interface>>
+Register(observer Observer)
+Unregister(observer Observer)
+Notify(event Event)
}
class Observer {
<<interface>>
+OnEvent(event Event)
}
class OrderEventBus {
-observers map[EventType][]Observer
-mu sync.RWMutex
+Register(eventType EventType, observer Observer)
+Unregister(eventType EventType, observer Observer)
+Publish(event Event)
}
class InventoryObserver {
-inventorySvc InventoryService
+OnEvent(event Event)
}
class EmailObserver {
-emailSvc EmailService
+OnEvent(event Event)
}
class AnalyticsObserver {
-analyticsSvc AnalyticsService
+OnEvent(event Event)
}
Subject <|.. OrderEventBus
Observer <|.. InventoryObserver
Observer <|.. EmailObserver
Observer <|.. AnalyticsObserver
OrderEventBus o-- Observer : manages list
| Komponen | Peran | Karakteristik |
|---|---|---|
| Subject interface | Kontrak untuk mendaftarkan dan menotifikasi observer | Register, Unregister, Notify |
| Concrete Subject | Menyimpan daftar observer, men-trigger notifikasi | Thread-safe dengan mutex |
| Observer interface | Kontrak yang harus dipenuhi semua subscriber | Biasanya satu method: OnEvent atau Update |
| Concrete Observer | Bereaksi terhadap event sesuai tanggung jawabnya | Independen satu sama lain |
Implementasi Lengkap: Order Event System #
Event Types dan Interfaces #
package events
import "time"
// EventType mendefinisikan jenis event yang bisa dipublikasikan.
type EventType string
const (
EventOrderCreated EventType = "order.created"
EventOrderPaid EventType = "order.paid"
EventOrderShipped EventType = "order.shipped"
EventOrderCancelled EventType = "order.cancelled"
)
// Event adalah interface untuk semua event dalam sistem.
type Event interface {
Type() EventType
OccurredAt() time.Time
}
// OrderCreatedEvent dipublikasikan ketika pesanan baru berhasil dibuat.
type OrderCreatedEvent struct {
OrderID string
UserID string
UserEmail string
Items []OrderItem
TotalAmount float64
CreatedAt time.Time
}
func (e OrderCreatedEvent) Type() EventType { return EventOrderCreated }
func (e OrderCreatedEvent) OccurredAt() time.Time { return e.CreatedAt }
// OrderPaidEvent dipublikasikan ketika pembayaran berhasil dikonfirmasi.
type OrderPaidEvent struct {
OrderID string
UserID string
UserEmail string
TransactionID string
Amount float64
PaidAt time.Time
}
func (e OrderPaidEvent) Type() EventType { return EventOrderPaid }
func (e OrderPaidEvent) OccurredAt() time.Time { return e.PaidAt }
// OrderShippedEvent dipublikasikan ketika pesanan dikirim.
type OrderShippedEvent struct {
OrderID string
UserID string
UserEmail string
TrackingNumber string
Courier string
ShippedAt time.Time
}
func (e OrderShippedEvent) Type() EventType { return EventOrderShipped }
func (e OrderShippedEvent) OccurredAt() time.Time { return e.ShippedAt }
// OrderItem adalah item dalam sebuah pesanan.
type OrderItem struct {
ProductID string
Quantity int
Price float64
}
// Observer adalah interface yang harus diimplementasikan semua subscriber.
type Observer interface {
OnEvent(event Event)
ObserverName() string // untuk logging dan debugging
}
Concrete Subject: EventBus yang Thread-Safe #
package events
import (
"fmt"
"log/slog"
"sync"
)
// EventBus adalah Subject — mengelola daftar observer per tipe event
// dan mendistribusikan event ke observer yang relevan.
type EventBus struct {
mu sync.RWMutex
observers map[EventType][]Observer
logger *slog.Logger
}
// NewEventBus membuat EventBus baru yang siap digunakan.
func NewEventBus(logger *slog.Logger) *EventBus {
return &EventBus{
observers: make(map[EventType][]Observer),
logger: logger,
}
}
// Register mendaftarkan observer untuk tipe event tertentu.
// Observer yang sama bisa didaftarkan untuk beberapa tipe event.
func (b *EventBus) Register(eventType EventType, observer Observer) {
b.mu.Lock()
defer b.mu.Unlock()
// Cek apakah observer sudah terdaftar untuk event type ini
for _, existing := range b.observers[eventType] {
if existing == observer {
b.logger.Warn("observer already registered",
"event_type", eventType,
"observer", observer.ObserverName())
return
}
}
b.observers[eventType] = append(b.observers[eventType], observer)
b.logger.Info("observer registered",
"event_type", eventType,
"observer", observer.ObserverName(),
"total", len(b.observers[eventType]),
)
}
// Unregister mencabut pendaftaran observer dari tipe event tertentu.
func (b *EventBus) Unregister(eventType EventType, observer Observer) {
b.mu.Lock()
defer b.mu.Unlock()
observers := b.observers[eventType]
for i, existing := range observers {
if existing == observer {
b.observers[eventType] = append(observers[:i], observers[i+1:]...)
b.logger.Info("observer unregistered",
"event_type", eventType,
"observer", observer.ObserverName(),
)
return
}
}
}
// Publish mendistribusikan event ke semua observer yang terdaftar.
// Notifikasi dilakukan secara sinkron — semua observer selesai sebelum Publish return.
func (b *EventBus) Publish(event Event) {
b.mu.RLock()
observers := make([]Observer, len(b.observers[event.Type()]))
copy(observers, b.observers[event.Type()])
b.mu.RUnlock()
b.logger.Info("publishing event",
"event_type", event.Type(),
"observer_count", len(observers),
"occurred_at", event.OccurredAt(),
)
for _, observer := range observers {
func() {
defer func() {
if r := recover(); r != nil {
b.logger.Error("observer panicked",
"observer", observer.ObserverName(),
"event_type", event.Type(),
"panic", r,
)
}
}()
observer.OnEvent(event)
}()
}
}
// PublishAsync mendistribusikan event secara asinkron via goroutine.
// Gunakan untuk observer yang bisa berjalan paralel dan independent.
func (b *EventBus) PublishAsync(event Event) {
b.mu.RLock()
observers := make([]Observer, len(b.observers[event.Type()]))
copy(observers, b.observers[event.Type()])
b.mu.RUnlock()
for _, observer := range observers {
go func(obs Observer) {
defer func() {
if r := recover(); r != nil {
b.logger.Error("async observer panicked",
"observer", obs.ObserverName(),
"event_type", event.Type(),
"panic", fmt.Sprintf("%v", r),
)
}
}()
obs.OnEvent(event)
}(observer)
}
}
// ObserverCount mengembalikan jumlah observer terdaftar untuk event type tertentu.
func (b *EventBus) ObserverCount(eventType EventType) int {
b.mu.RLock()
defer b.mu.RUnlock()
return len(b.observers[eventType])
}
Concrete Observers #
package observers
import (
"context"
"fmt"
"log/slog"
"myapp/events"
)
// InventoryObserver mengurangi stok ketika pesanan dibuat.
type InventoryObserver struct {
inventorySvc InventoryService
logger *slog.Logger
}
func NewInventoryObserver(svc InventoryService, logger *slog.Logger) *InventoryObserver {
return &InventoryObserver{inventorySvc: svc, logger: logger}
}
func (o *InventoryObserver) OnEvent(event events.Event) {
e, ok := event.(events.OrderCreatedEvent)
if !ok {
return // event yang tidak relevan, abaikan
}
ctx := context.Background()
if err := o.inventorySvc.DeductStock(ctx, e.Items); err != nil {
o.logger.Error("failed to deduct stock",
"order_id", e.OrderID,
"error", err,
)
return
}
o.logger.Info("stock deducted", "order_id", e.OrderID, "items", len(e.Items))
}
func (o *InventoryObserver) ObserverName() string { return "InventoryObserver" }
// EmailObserver mengirim email konfirmasi ketika pesanan dibuat dan dibayar.
type EmailObserver struct {
emailSvc EmailService
logger *slog.Logger
}
func NewEmailObserver(svc EmailService, logger *slog.Logger) *EmailObserver {
return &EmailObserver{emailSvc: svc, logger: logger}
}
func (o *EmailObserver) OnEvent(event events.Event) {
ctx := context.Background()
switch e := event.(type) {
case events.OrderCreatedEvent:
if err := o.emailSvc.SendOrderConfirmation(ctx, e.UserEmail, e.OrderID, e.TotalAmount); err != nil {
o.logger.Error("failed to send confirmation email",
"order_id", e.OrderID,
"email", e.UserEmail,
"error", err,
)
}
case events.OrderShippedEvent:
if err := o.emailSvc.SendShippingNotification(ctx, e.UserEmail, e.OrderID, e.TrackingNumber, e.Courier); err != nil {
o.logger.Error("failed to send shipping email",
"order_id", e.OrderID,
"error", err,
)
}
default:
// Event lain tidak perlu dihandle oleh EmailObserver
}
}
func (o *EmailObserver) ObserverName() string { return "EmailObserver" }
// AnalyticsObserver mencatat semua event ke analytics platform.
type AnalyticsObserver struct {
analyticsSvc AnalyticsService
logger *slog.Logger
}
func NewAnalyticsObserver(svc AnalyticsService, logger *slog.Logger) *AnalyticsObserver {
return &AnalyticsObserver{analyticsSvc: svc, logger: logger}
}
func (o *AnalyticsObserver) OnEvent(event events.Event) {
ctx := context.Background()
properties := map[string]interface{}{
"event_type": string(event.Type()),
"occurred_at": event.OccurredAt(),
}
switch e := event.(type) {
case events.OrderCreatedEvent:
properties["order_id"] = e.OrderID
properties["user_id"] = e.UserID
properties["amount"] = e.TotalAmount
properties["item_count"] = len(e.Items)
case events.OrderPaidEvent:
properties["order_id"] = e.OrderID
properties["user_id"] = e.UserID
properties["amount"] = e.Amount
properties["transaction_id"] = e.TransactionID
}
if err := o.analyticsSvc.Track(ctx, string(event.Type()), properties); err != nil {
o.logger.Warn("failed to track analytics event", "error", err)
// Non-fatal: analytics gagal tidak boleh menghentikan flow utama
}
}
func (o *AnalyticsObserver) ObserverName() string { return "AnalyticsObserver" }
// LoyaltyObserver menambahkan loyalty points ketika pembayaran berhasil.
type LoyaltyObserver struct {
loyaltySvc LoyaltyService
logger *slog.Logger
}
func NewLoyaltyObserver(svc LoyaltyService, logger *slog.Logger) *LoyaltyObserver {
return &LoyaltyObserver{loyaltySvc: svc, logger: logger}
}
func (o *LoyaltyObserver) OnEvent(event events.Event) {
e, ok := event.(events.OrderPaidEvent)
if !ok {
return
}
// Hitung poin: 1 poin per 10.000 yang dibayarkan
points := int(e.Amount / 10000)
if points == 0 {
return
}
ctx := context.Background()
if err := o.loyaltySvc.AddPoints(ctx, e.UserID, points); err != nil {
o.logger.Error("failed to add loyalty points",
"user_id", e.UserID,
"points", points,
"error", err,
)
return
}
o.logger.Info("loyalty points added", "user_id", e.UserID, "points", points)
}
func (o *LoyaltyObserver) ObserverName() string { return "LoyaltyObserver" }
Subject: OrderService yang Bersih #
package order
import (
"context"
"fmt"
"time"
"myapp/events"
)
// OrderService adalah Subject — mempublikasikan event, tidak peduli siapa observernya.
type OrderService struct {
repo OrderRepository
eventBus *events.EventBus
}
func NewOrderService(repo OrderRepository, eventBus *events.EventBus) *OrderService {
return &OrderService{repo: repo, eventBus: eventBus}
}
func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error) {
order, err := s.repo.Create(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to create order: %w", err)
}
// Publikasikan event — tidak tahu dan tidak peduli siapa yang subscribe
s.eventBus.Publish(events.OrderCreatedEvent{
OrderID: order.ID,
UserID: req.UserID,
UserEmail: req.UserEmail,
Items: req.Items,
TotalAmount: order.TotalAmount,
CreatedAt: time.Now(),
})
return order, nil
}
func (s *OrderService) ConfirmPayment(ctx context.Context, orderID, transactionID string, amount float64) error {
order, err := s.repo.UpdateStatus(ctx, orderID, "paid")
if err != nil {
return fmt.Errorf("failed to update order status: %w", err)
}
s.eventBus.Publish(events.OrderPaidEvent{
OrderID: orderID,
UserID: order.UserID,
UserEmail: order.UserEmail,
TransactionID: transactionID,
Amount: amount,
PaidAt: time.Now(),
})
return nil
}
func (s *OrderService) ShipOrder(ctx context.Context, orderID, trackingNumber, courier string) error {
order, err := s.repo.UpdateStatus(ctx, orderID, "shipped")
if err != nil {
return fmt.Errorf("failed to update order status: %w", err)
}
s.eventBus.Publish(events.OrderShippedEvent{
OrderID: orderID,
UserID: order.UserID,
UserEmail: order.UserEmail,
TrackingNumber: trackingNumber,
Courier: courier,
ShippedAt: time.Now(),
})
return nil
}
Perakitan di main.go #
func main() {
logger := slog.Default()
eventBus := events.NewEventBus(logger)
// Buat observers
inventoryObs := observers.NewInventoryObserver(inventorySvc, logger)
emailObs := observers.NewEmailObserver(emailSvc, logger)
analyticsObs := observers.NewAnalyticsObserver(analyticsSvc, logger)
loyaltyObs := observers.NewLoyaltyObserver(loyaltySvc, logger)
// Daftarkan observer ke event yang relevan
eventBus.Register(events.EventOrderCreated, inventoryObs)
eventBus.Register(events.EventOrderCreated, emailObs)
eventBus.Register(events.EventOrderCreated, analyticsObs)
eventBus.Register(events.EventOrderPaid, loyaltyObs)
eventBus.Register(events.EventOrderPaid, analyticsObs)
eventBus.Register(events.EventOrderShipped, emailObs)
eventBus.Register(events.EventOrderShipped, analyticsObs)
// OrderService tidak perlu tahu semua ini
orderSvc := order.NewOrderService(orderRepo, eventBus)
// Tambah FraudDetectionObserver di kemudian hari:
// eventBus.Register(events.EventOrderCreated, fraudObs)
// — tidak ada kode lain yang berubah
}
Async Observer: Notifikasi Non-Blocking #
Untuk observer yang bisa berjalan secara independen dan tidak perlu menunggu satu sama lain, async notification via goroutine lebih tepat.
// AsyncEventBus mendistribusikan event ke setiap observer dalam goroutine terpisah.
// Gunakan ketika observer tidak perlu urutan eksekusi yang dijamin.
type AsyncEventBus struct {
EventBus
errorHandler func(obs Observer, event Event, err interface{})
}
func NewAsyncEventBus(logger *slog.Logger) *AsyncEventBus {
bus := &AsyncEventBus{}
bus.observers = make(map[EventType][]Observer)
bus.logger = logger
bus.errorHandler = func(obs Observer, event Event, err interface{}) {
logger.Error("async observer error",
"observer", obs.ObserverName(),
"event_type", event.Type(),
"error", fmt.Sprintf("%v", err),
)
}
return bus
}
func (b *AsyncEventBus) Publish(event Event) {
b.mu.RLock()
observers := make([]Observer, len(b.observers[event.Type()]))
copy(observers, b.observers[event.Type()])
b.mu.RUnlock()
var wg sync.WaitGroup
for _, observer := range observers {
wg.Add(1)
go func(obs Observer) {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
b.errorHandler(obs, event, r)
}
}()
obs.OnEvent(event)
}(observer)
}
// Tunggu semua selesai — atau tidak tergantung kebutuhan
// wg.Wait() // uncomment jika perlu tunggu semua observer selesai
}
Perbandingan sync vs async:
| Aspek | Synchronous Publish | Asynchronous Publish |
|---|---|---|
| Urutan eksekusi | Dijamin — observer dipanggil berurutan | Tidak dijamin — berjalan paralel |
| Error handling | Error observer bisa menunda observer berikutnya | Error satu observer tidak mempengaruhi lain |
| Rollback | Bisa implement rollback jika observer gagal | Sulit — event sudah dipublish |
| Performa | Observer lambat memblok subject | Observer berjalan paralel |
| Cocok untuk | Observer yang saling bergantung, butuh urutan | Observer independent, best-effort |
Mencegah Memory Leak #
Salah satu masalah terbesar Observer Pattern yang tidak ditangani dengan baik adalah memory leak — observer yang tidak lagi aktif masih terdaftar dan mencegah garbage collection.
// ANTI-PATTERN: observer tidak pernah di-unregister
type SessionHandler struct {
eventBus *events.EventBus
}
func (h *SessionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Observer baru didaftarkan setiap request — tidak pernah dihapus!
obs := &RequestScopedObserver{requestID: r.Header.Get("X-Request-ID")}
h.eventBus.Register(events.EventOrderCreated, obs)
// Setelah request selesai, obs tidak dihapus → accumulate terus
}
// BENAR: selalu unregister observer yang tidak lagi dibutuhkan
func (h *SessionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
obs := &RequestScopedObserver{requestID: r.Header.Get("X-Request-ID")}
h.eventBus.Register(events.EventOrderCreated, obs)
defer h.eventBus.Unregister(events.EventOrderCreated, obs) // cleanup otomatis
// handle request...
}
// Atau: gunakan WeakReference-style dengan timeout
type TimedObserver struct {
observer Observer
expiresAt time.Time
}
// EventBus dengan auto-cleanup observer yang sudah expired
func (b *EventBus) CleanupExpired() {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
for eventType, observers := range b.observers {
var active []Observer
for _, obs := range observers {
if timed, ok := obs.(*TimedObserver); ok {
if now.Before(timed.expiresAt) {
active = append(active, obs)
}
} else {
active = append(active, obs)
}
}
b.observers[eventType] = active
}
}
Observer Hidup Lebih Lama dari yang Diharapkan
Jika Subject menyimpan referensi ke Observer, Observer tidak akan di-garbage collect meski semua referensi lain ke Observer sudah tidak ada. Ini adalah sumber memory leak yang sangat umum. Selalu sediakan mekanisme
Unregisterdan pastikan lifecycle Observer dikelola dengan benar — terutama untuk Observer yang dibuat per-request atau per-session.
Testing Observer Pattern #
// MockObserver untuk testing — mencatat semua event yang diterima
type MockObserver struct {
ReceivedEvents []events.Event
mu sync.Mutex
ShouldPanic bool
}
func (m *MockObserver) OnEvent(event events.Event) {
if m.ShouldPanic {
panic("mock panic")
}
m.mu.Lock()
defer m.mu.Unlock()
m.ReceivedEvents = append(m.ReceivedEvents, event)
}
func (m *MockObserver) ObserverName() string { return "MockObserver" }
func (m *MockObserver) EventCount() int {
m.mu.Lock()
defer m.mu.Unlock()
return len(m.ReceivedEvents)
}
func TestEventBus_Publish_NotifiesAllObservers(t *testing.T) {
bus := events.NewEventBus(slog.Default())
obs1 := &MockObserver{}
obs2 := &MockObserver{}
bus.Register(events.EventOrderCreated, obs1)
bus.Register(events.EventOrderCreated, obs2)
event := events.OrderCreatedEvent{
OrderID: "ORD-001", UserID: "user-1",
TotalAmount: 150000, CreatedAt: time.Now(),
}
bus.Publish(event)
if obs1.EventCount() != 1 {
t.Errorf("expected obs1 to receive 1 event, got %d", obs1.EventCount())
}
if obs2.EventCount() != 1 {
t.Errorf("expected obs2 to receive 1 event, got %d", obs2.EventCount())
}
}
func TestEventBus_Unregister_StopsNotification(t *testing.T) {
bus := events.NewEventBus(slog.Default())
obs := &MockObserver{}
bus.Register(events.EventOrderCreated, obs)
bus.Publish(events.OrderCreatedEvent{OrderID: "ORD-001", CreatedAt: time.Now()})
bus.Unregister(events.EventOrderCreated, obs)
bus.Publish(events.OrderCreatedEvent{OrderID: "ORD-002", CreatedAt: time.Now()})
if obs.EventCount() != 1 {
t.Errorf("expected 1 event after unregister, got %d", obs.EventCount())
}
}
func TestEventBus_PanicInObserver_DoesNotStopOthers(t *testing.T) {
bus := events.NewEventBus(slog.Default())
panicObs := &MockObserver{ShouldPanic: true}
normalObs := &MockObserver{}
bus.Register(events.EventOrderCreated, panicObs)
bus.Register(events.EventOrderCreated, normalObs)
// Publish tidak boleh panic meski salah satu observer panic
require.NotPanics(t, func() {
bus.Publish(events.OrderCreatedEvent{OrderID: "ORD-001", CreatedAt: time.Now()})
})
// Observer yang normal tetap menerima event
if normalObs.EventCount() != 1 {
t.Errorf("normal observer should still receive event despite panic in another observer")
}
}
func TestEventBus_TypeFiltering(t *testing.T) {
bus := events.NewEventBus(slog.Default())
obs := &MockObserver{}
// Hanya subscribe ke EventOrderPaid
bus.Register(events.EventOrderPaid, obs)
// Publish EventOrderCreated — tidak seharusnya diterima obs
bus.Publish(events.OrderCreatedEvent{OrderID: "ORD-001", CreatedAt: time.Now()})
if obs.EventCount() != 0 {
t.Error("observer should not receive event for unsubscribed type")
}
// Publish EventOrderPaid — seharusnya diterima
bus.Publish(events.OrderPaidEvent{OrderID: "ORD-001", Amount: 100000, PaidAt: time.Now()})
if obs.EventCount() != 1 {
t.Errorf("expected 1 event for subscribed type, got %d", obs.EventCount())
}
}
func TestEventBus_ConcurrentPublish(t *testing.T) {
bus := events.NewEventBus(slog.Default())
obs := &MockObserver{}
bus.Register(events.EventOrderCreated, obs)
const goroutines = 100
var wg sync.WaitGroup
for i := 0; i < goroutines; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
bus.Publish(events.OrderCreatedEvent{
OrderID: fmt.Sprintf("ORD-%d", i),
CreatedAt: time.Now(),
})
}(i)
}
wg.Wait()
if obs.EventCount() != goroutines {
t.Errorf("expected %d events from concurrent publish, got %d", goroutines, obs.EventCount())
}
}
Observer vs Event Bus vs Mediator #
Observer Pattern bisa diimplementasikan dengan berbagai pendekatan. Penting memahami perbedaannya.
| Aspek | Observer Klasik | Event Bus (Typed) | Mediator |
|---|---|---|---|
| Subject tahu Observer? | Ya — menyimpan list | Tidak — hanya tahu EventType | Tidak — semua lewat mediator |
| Observer tahu Subject? | Tidak — hanya tahu event | Tidak — hanya tahu EventType | Tidak |
| Coupling | Subject-Observer loosely coupled | Paling loosely coupled | Komponen tidak tahu satu sama lain |
| Event filtering | Observer filter sendiri | Filter by type di bus | Mediator memutuskan siapa yang perlu tahu |
| Cocok untuk | Satu subject, beberapa observer | Banyak subject, banyak observer | Komunikasi kompleks antar komponen |
Kapan Menggunakan dan Kapan Tidak #
GUNAKAN Observer jika:
✓ Perubahan pada satu objek perlu diketahui objek lain tanpa tight coupling
✓ Jumlah dan identitas observer tidak diketahui di compile-time
✓ Observer bisa didaftarkan dan dicabut secara dinamis
✓ Membangun sistem event-driven atau reactive
✓ Ingin menerapkan Open-Closed Principle untuk reaksi terhadap event
HINDARI Observer jika:
✗ Chain notifikasi terlalu panjang dan membuat debugging sulit
✗ Observer perlu tahu tentang observer lain — gunakan Mediator
✗ Urutan observer sangat kritis dan tidak bisa dijamin
✗ Sistem sangat kecil dengan 1-2 reaksi yang tidak akan berubah
Checklist Review Observer #
DESAIN:
□ Subject tidak bergantung pada concrete Observer — hanya pada interface
□ Observer bisa didaftarkan dan dicabut tanpa mengubah Subject
□ Event membawa cukup informasi sehingga Observer tidak perlu query balik ke Subject
□ Setiap Observer handle hanya event yang relevan — abaikan yang lain
IMPLEMENTASI:
□ EventBus thread-safe — Register, Unregister, Publish menggunakan mutex
□ Publish mengambil salinan list observer sebelum memanggil (hindari deadlock)
□ Panic di satu observer tidak menghentikan notifikasi ke observer lain
□ Error di observer tidak di-propagate ke Subject — Subject tidak peduli
LIFECYCLE:
□ Ada mekanisme Unregister yang mudah digunakan
□ Observer di-unregister ketika tidak lagi dibutuhkan
□ Tidak ada referensi sirkular antara Subject dan Observer
TESTING:
□ MockObserver mencatat semua event untuk di-assert
□ Test bahwa semua observer terotifikasi
□ Test bahwa Unregister menghentikan notifikasi
□ Test bahwa panic di satu observer tidak mempengaruhi lain
□ Test thread-safety dengan concurrent Publish
Ringkasan #
- Observer mendefinisikan mekanisme subscription — Subject mengumumkan perubahan, Observer bereaksi; keduanya tidak perlu saling kenal secara konkret.
- Loose coupling yang sesungguhnya: Subject tidak tahu siapa observernya, berapa banyak, atau apa yang mereka lakukan — hanya memanggil
OnEvent()melalui interface.- Typed Event Bus adalah evolusi Observer klasik — filter event by type di bus, bukan di setiap observer; lebih scalable untuk sistem dengan banyak event.
- Thread safety wajib:
Register,Unregister, danPublishsering dipanggil dari goroutine berbeda; gunakansync.RWMutexdan salin list observer sebelum iterasi.- Panic recovery di dalam Publish — satu observer yang panic tidak boleh menghentikan notifikasi ke observer lain; wrap setiap panggilan observer dengan
recover().- Memory leak adalah risiko nyata — observer yang tidak di-unregister mencegah garbage collection; selalu gunakan
defer Unregister()untuk observer dengan lifecycle terbatas.- Sync vs Async: synchronous untuk observer yang butuh urutan; async (goroutine) untuk observer independent yang bisa berjalan paralel.
- Bedakan dari Mediator: Observer untuk “satu subject, banyak subscriber”; Mediator untuk “banyak komponen yang saling berkomunikasi melalui perantara”.