Mediator Pattern #

Bayangkan sebuah form pendaftaran penerbangan: field tanggal mempengaruhi daftar penerbangan yang tersedia, pilihan kelas mempengaruhi harga dan ketersediaan kursi, kode promo mempengaruhi harga final, jumlah penumpang mempengaruhi total, dan tombol “Beli” hanya aktif ketika semua field valid. Tanpa koordinasi terpusat, setiap komponen harus tahu tentang semua komponen lain yang dipengaruhinya — field tanggal harus punya referensi ke dropdown penerbangan, dropdown penerbangan harus punya referensi ke komponen harga, dan seterusnya hingga terbentuk jaringan ketergantungan yang rumit. Saat ada perubahan kecil, tidak ada yang tahu komponen mana yang perlu di-update. Mediator Pattern memotong semua ketergantungan silang ini dengan memperkenalkan satu objek koordinator — setiap komponen hanya berbicara ke Mediator, dan Mediator yang mengatur siapa harus tahu apa.

Apa itu Mediator Pattern? #

Mediator Pattern adalah behavioral design pattern yang mendefinisikan objek yang memusatkan koordinasi komunikasi antar komponen, sehingga komponen tidak perlu saling tahu satu sama lain secara langsung. Alih-alih banyak koneksi langsung antar komponen (yang jumlahnya bisa tumbuh secara kuadratik), setiap komponen hanya punya satu koneksi ke mediator.

Dua properti yang mendefinisikan Mediator Pattern:

  • Decoupling total — komponen (disebut colleague) tidak menyimpan referensi ke komponen lain; hanya tahu mediator
  • Logika koordinasi terpusat — aturan “jika X berubah, Y dan Z harus di-update” ada di satu tempat, bukan tersebar di masing-masing komponen
flowchart LR
    subgraph "Tanpa Mediator — N² koneksi"
        A1[Comp A] <--> B1[Comp B]
        A1 <--> C1[Comp C]
        A1 <--> D1[Comp D]
        B1 <--> C1
        B1 <--> D1
        C1 <--> D1
        note1["4 komponen = 6 koneksi\n10 komponen = 45 koneksi"]
    end

    subgraph "Dengan Mediator — N koneksi"
        A2[Comp A] --> M[Mediator]
        B2[Comp B] --> M
        C2[Comp C] --> M
        D2[Comp D] --> M
        M --> A2
        M --> B2
        M --> C2
        M --> D2
        note2["4 komponen = 4 koneksi\n10 komponen = 10 koneksi"]
    end

Masalah yang Dipecahkan #

Mediator paling jelas manfaatnya ketika ada banyak komponen yang saling mempengaruhi satu sama lain — karena jumlah koneksi tumbuh secara kuadratik tanpa mediator, tapi hanya linear dengan mediator.

Masalah: Spaghetti Coupling #

// ANTI-PATTERN: setiap komponen menyimpan referensi ke komponen lain

type InventoryComponent struct {
    priceComp   *PriceComponent   // tahu tentang price
    orderComp   *OrderComponent   // tahu tentang order
    notifComp   *NotifComponent   // tahu tentang notifikasi
}

func (ic *InventoryComponent) OnStockUpdate(productID string, qty int) {
    // Harus tahu cara update setiap komponen lain secara langsung
    ic.priceComp.RecalculateForProduct(productID)
    if qty == 0 {
        ic.priceComp.MarkOutOfStock(productID)
        ic.orderComp.BlockNewOrders(productID)
        ic.notifComp.NotifySubscribers(productID, "out_of_stock")
    }
}

// Setiap komponen punya ketergantungan ke semua komponen lain
// Menambah komponen baru = update semua komponen yang perlu tahu tentangnya

// BENAR: setiap komponen hanya tahu mediator
type InventoryComponent struct {
    mediator Mediator
}

func (ic *InventoryComponent) OnStockUpdate(productID string, qty int) {
    ic.mediator.Notify(ic, "stock_updated", map[string]interface{}{
        "product_id": productID,
        "quantity":   qty,
    })
    // Komponen tidak tahu siapa yang akan bereaksi atau bagaimana
}

Struktur dan Komponen #

classDiagram
    class Mediator {
        <<interface>>
        +Notify(sender Colleague, event string, data interface{})
        +Register(colleague Colleague)
    }

    class Colleague {
        <<interface>>
        +SetMediator(mediator Mediator)
        +ComponentName() string
    }

    class WorkflowMediator {
        -colleagues map[string]Colleague
        -rules map[string][]Handler
        +Notify(sender Colleague, event string, data interface{})
        +Register(colleague Colleague)
    }

    class InventoryComponent {
        -mediator Mediator
        +SetMediator(mediator Mediator)
        +UpdateStock(productID string, qty int)
        +ComponentName() string
    }

    class PriceComponent {
        -mediator Mediator
        +SetMediator(mediator Mediator)
        +ComponentName() string
    }

    Mediator <|.. WorkflowMediator
    Colleague <|.. InventoryComponent
    Colleague <|.. PriceComponent
    WorkflowMediator o-- Colleague : manages
    InventoryComponent --> Mediator : notifies
    PriceComponent --> Mediator : notifies
Komponen Peran Karakteristik
Mediator interface Kontrak untuk semua mediator Method Notify dan Register
Concrete Mediator Menyimpan referensi ke semua colleague; mengimplementasikan logika koordinasi Bisa membengkak jika tidak dijaga
Colleague interface Kontrak untuk semua komponen yang berkomunikasi via mediator Hanya tahu mediator, tidak tahu colleague lain
Concrete Colleague Memanggil mediator.Notify() saat ada event; bereaksi saat mediator memanggil method-nya Tidak punya referensi ke colleague lain

Implementasi Lengkap: Chatroom #

Chatroom adalah contoh klasik Mediator Pattern — setiap user tidak perlu tahu tentang user lain; mereka hanya tahu chatroom sebagai mediator.

Mediator Interface dan Colleague #

package chat

import (
    "fmt"
    "strings"
    "time"
)

// Mediator adalah interface untuk semua chatroom mediator.
type Mediator interface {
    SendMessage(sender *User, message string)
    SendPrivateMessage(sender *User, recipientName, message string) error
    Register(user *User)
    Unregister(username string)
    ListUsers() []string
}

// User adalah Colleague — berkomunikasi melalui mediator, tidak langsung ke user lain.
type User struct {
    Name      string
    mediator  Mediator
    inbox     []Message
}

// Message merepresentasikan satu pesan dalam chatroom.
type Message struct {
    From      string
    To        string // kosong = broadcast
    Content   string
    SentAt    time.Time
    IsPrivate bool
}

func NewUser(name string) *User {
    return &User{
        Name:  name,
        inbox: make([]Message, 0),
    }
}

// SetMediator mendaftarkan mediator ke user.
func (u *User) SetMediator(m Mediator) {
    u.mediator = m
    m.Register(u)
}

// Send mengirim pesan ke semua user dalam chatroom.
func (u *User) Send(message string) {
    u.mediator.SendMessage(u, message)
}

// SendTo mengirim pesan pribadi ke user tertentu.
func (u *User) SendTo(recipientName, message string) error {
    return u.mediator.SendPrivateMessage(u, recipientName, message)
}

// Receive dipanggil oleh mediator ketika ada pesan yang masuk.
// User tidak memanggil ini sendiri — ini adalah "callback" dari mediator.
func (u *User) Receive(msg Message) {
    u.inbox = append(u.inbox, msg)
    if msg.IsPrivate {
        fmt.Printf("[DM to %s] %s → %s: %s\n", u.Name, msg.From, u.Name, msg.Content)
    } else {
        fmt.Printf("[%s] %s: %s\n", u.Name, msg.From, msg.Content)
    }
}

// Inbox mengembalikan semua pesan yang diterima user.
func (u *User) Inbox() []Message { return u.inbox }

Concrete Mediator: ChatRoom #

package chat

import (
    "fmt"
    "sync"
    "time"
)

// ChatRoom adalah Concrete Mediator — mengatur semua komunikasi antar user.
type ChatRoom struct {
    mu    sync.RWMutex
    users map[string]*User
    log   []Message
}

func NewChatRoom() *ChatRoom {
    return &ChatRoom{
        users: make(map[string]*User),
        log:   make([]Message, 0),
    }
}

// Register mendaftarkan user baru ke chatroom.
func (c *ChatRoom) Register(user *User) {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.users[user.Name] = user
    fmt.Printf("[ChatRoom] %s bergabung. Total user: %d\n", user.Name, len(c.users))

    // Beritahu user lain bahwa ada user baru
    joinMsg := Message{
        From:    "ChatRoom",
        Content: fmt.Sprintf("%s bergabung ke chatroom", user.Name),
        SentAt:  time.Now(),
    }
    for name, u := range c.users {
        if name != user.Name {
            u.Receive(joinMsg)
        }
    }
}

// Unregister menghapus user dari chatroom.
func (c *ChatRoom) Unregister(username string) {
    c.mu.Lock()
    defer c.mu.Unlock()

    delete(c.users, username)
    fmt.Printf("[ChatRoom] %s keluar. Total user: %d\n", username, len(c.users))

    leaveMsg := Message{
        From:    "ChatRoom",
        Content: fmt.Sprintf("%s keluar dari chatroom", username),
        SentAt:  time.Now(),
    }
    for _, u := range c.users {
        u.Receive(leaveMsg)
    }
}

// SendMessage membroadcast pesan ke semua user kecuali pengirim.
// Ini adalah inti koordinasi mediator.
func (c *ChatRoom) SendMessage(sender *User, content string) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    msg := Message{
        From:    sender.Name,
        Content: content,
        SentAt:  time.Now(),
    }
    c.log = append(c.log, msg)

    // Kirim ke semua user KECUALI pengirim
    for name, user := range c.users {
        if name != sender.Name {
            user.Receive(msg)
        }
    }
}

// SendPrivateMessage mengirim pesan hanya ke satu user tertentu.
func (c *ChatRoom) SendPrivateMessage(sender *User, recipientName, content string) error {
    c.mu.RLock()
    defer c.mu.RUnlock()

    recipient, ok := c.users[recipientName]
    if !ok {
        return fmt.Errorf("user %q tidak ditemukan dalam chatroom", recipientName)
    }

    msg := Message{
        From:      sender.Name,
        To:        recipientName,
        Content:   content,
        SentAt:    time.Now(),
        IsPrivate: true,
    }
    c.log = append(c.log, msg)
    recipient.Receive(msg)
    return nil
}

// ListUsers mengembalikan daftar username yang aktif.
func (c *ChatRoom) ListUsers() []string {
    c.mu.RLock()
    defer c.mu.RUnlock()
    names := make([]string, 0, len(c.users))
    for name := range c.users {
        names = append(names, name)
    }
    return names
}

// MessageLog mengembalikan seluruh log pesan.
func (c *ChatRoom) MessageLog() []Message {
    c.mu.RLock()
    defer c.mu.RUnlock()
    result := make([]Message, len(c.log))
    copy(result, c.log)
    return result
}

Demonstrasi #

func main() {
    room := chat.NewChatRoom()

    alice := chat.NewUser("Alice")
    bob   := chat.NewUser("Bob")
    citra := chat.NewUser("Citra")

    // Daftarkan ke chatroom — user tidak saling tahu satu sama lain
    alice.SetMediator(room)
    bob.SetMediator(room)
    citra.SetMediator(room)

    fmt.Println("\n--- Pesan Broadcast ---")
    alice.Send("Halo semua!")
    bob.Send("Hai Alice!")

    fmt.Println("\n--- Pesan Privat ---")
    _ = alice.SendTo("Citra", "Hai Citra, ada waktu ngobrol?")
    _ = bob.SendTo("NonExistent", "test") // error — user tidak ada

    fmt.Printf("\nTotal user: %v\n", room.ListUsers())
    fmt.Printf("Alice inbox: %d pesan\n", len(alice.Inbox()))
}

Studi Kasus Kedua: Workflow Engine #

Workflow engine adalah use case Mediator yang lebih kompleks — setiap step dalam workflow tidak perlu tahu tentang step lain; workflow coordinator (mediator) yang menentukan step mana yang harus dieksekusi berikutnya.

package workflow

import (
    "context"
    "fmt"
    "log/slog"
)

// WorkflowStep adalah Colleague — satu langkah dalam workflow.
type WorkflowStep interface {
    Execute(ctx context.Context, data map[string]interface{}) error
    StepName() string
    SetCoordinator(coordinator WorkflowCoordinator)
}

// WorkflowCoordinator adalah Mediator — mengatur urutan dan kondisi eksekusi step.
type WorkflowCoordinator interface {
    TriggerStep(ctx context.Context, stepName string, data map[string]interface{}) error
    OnStepComplete(step WorkflowStep, data map[string]interface{})
    OnStepFailed(step WorkflowStep, err error)
    RegisterStep(step WorkflowStep)
}

// BaseStep menyediakan implementasi default untuk SetCoordinator.
type BaseStep struct {
    coordinator WorkflowCoordinator
}

func (s *BaseStep) SetCoordinator(c WorkflowCoordinator) { s.coordinator = c }

// OrderFulfillmentCoordinator mengorkestrasikan proses fulfillment pesanan.
// Setiap step hanya perlu tahu coordinator, tidak tahu step lain.
type OrderFulfillmentCoordinator struct {
    steps  map[string]WorkflowStep
    logger *slog.Logger

    // Transisi yang dikontrol mediator: ketika step A selesai, jalankan step B
    transitions map[string][]string
}

func NewOrderFulfillmentCoordinator(logger *slog.Logger) *OrderFulfillmentCoordinator {
    c := &OrderFulfillmentCoordinator{
        steps:  make(map[string]WorkflowStep),
        logger: logger,
        transitions: map[string][]string{
            "validate_order":    {"reserve_inventory"},
            "reserve_inventory": {"process_payment"},
            "process_payment":   {"send_confirmation", "update_analytics"},
            "send_confirmation": {}, // terminal — tidak ada step berikutnya
            "update_analytics":  {}, // terminal
        },
    }
    return c
}

// RegisterStep mendaftarkan step ke coordinator.
func (c *OrderFulfillmentCoordinator) RegisterStep(step WorkflowStep) {
    step.SetCoordinator(c)
    c.steps[step.StepName()] = step
}

// TriggerStep menjalankan step tertentu.
func (c *OrderFulfillmentCoordinator) TriggerStep(ctx context.Context, stepName string, data map[string]interface{}) error {
    step, ok := c.steps[stepName]
    if !ok {
        return fmt.Errorf("step %q tidak terdaftar", stepName)
    }

    c.logger.InfoContext(ctx, "executing step", "step", stepName)
    if err := step.Execute(ctx, data); err != nil {
        c.OnStepFailed(step, err)
        return err
    }

    return nil
}

// OnStepComplete dipanggil setelah sebuah step berhasil.
// Koordinator menentukan step mana yang harus dijalankan selanjutnya.
func (c *OrderFulfillmentCoordinator) OnStepComplete(step WorkflowStep, data map[string]interface{}) {
    nextSteps, ok := c.transitions[step.StepName()]
    if !ok || len(nextSteps) == 0 {
        c.logger.Info("workflow branch complete", "last_step", step.StepName())
        return
    }

    ctx := context.Background()
    for _, nextStep := range nextSteps {
        c.logger.InfoContext(ctx, "triggering next step",
            "from", step.StepName(), "to", nextStep)
        if err := c.TriggerStep(ctx, nextStep, data); err != nil {
            c.logger.ErrorContext(ctx, "step failed in chain",
                "step", nextStep, "error", err)
        }
    }
}

// OnStepFailed dipanggil ketika sebuah step gagal.
func (c *OrderFulfillmentCoordinator) OnStepFailed(step WorkflowStep, err error) {
    c.logger.Error("step failed",
        "step", step.StepName(),
        "error", err,
    )
    // Coordinator bisa menentukan: retry? rollback? alarm?
}

// Start memulai workflow dari step pertama.
func (c *OrderFulfillmentCoordinator) Start(ctx context.Context, data map[string]interface{}) error {
    return c.TriggerStep(ctx, "validate_order", data)
}


// Concrete Steps — masing-masing tidak tahu tentang step lain

type ValidateOrderStep struct {
    BaseStep
}

func (s *ValidateOrderStep) StepName() string { return "validate_order" }

func (s *ValidateOrderStep) Execute(ctx context.Context, data map[string]interface{}) error {
    orderID, ok := data["order_id"].(string)
    if !ok || orderID == "" {
        return fmt.Errorf("order_id is required")
    }
    fmt.Printf("[ValidateOrder] Validating order: %s\n", orderID)
    // ... validasi ...
    s.coordinator.OnStepComplete(s, data) // beritahu coordinator bahwa selesai
    return nil
}

type ReserveInventoryStep struct {
    BaseStep
    inventorySvc InventoryService
}

func (s *ReserveInventoryStep) StepName() string { return "reserve_inventory" }

func (s *ReserveInventoryStep) Execute(ctx context.Context, data map[string]interface{}) error {
    fmt.Printf("[ReserveInventory] Reserving stock for order: %s\n", data["order_id"])
    reservationID, err := s.inventorySvc.Reserve(ctx, data["items"])
    if err != nil {
        return fmt.Errorf("reservation failed: %w", err)
    }
    data["reservation_id"] = reservationID
    s.coordinator.OnStepComplete(s, data)
    return nil
}

func (s *ReserveInventoryStep) SetCoordinator(c WorkflowCoordinator) { s.coordinator = c }

// ... step lainnya mengikuti pola yang sama

// Perakitan
func SetupFulfillmentWorkflow(logger *slog.Logger, inventorySvc InventoryService) *OrderFulfillmentCoordinator {
    coord := NewOrderFulfillmentCoordinator(logger)
    coord.RegisterStep(&ValidateOrderStep{})
    coord.RegisterStep(&ReserveInventoryStep{inventorySvc: inventorySvc})
    // ... daftarkan step lainnya
    return coord
}

Typed Event Mediator: Pendekatan Modern #

Versi yang lebih type-safe dari Mediator menggunakan generic atau typed event untuk menghindari type assertion di setiap handler.

package mediator

import (
    "fmt"
    "sync"
)

// Event adalah interface yang diimplementasikan semua event.
type Event interface {
    EventName() string
}

// Handler adalah fungsi yang menangani event tertentu.
type Handler[T Event] func(event T) error

// TypedMediator adalah mediator dengan type-safe event handling.
type TypedMediator struct {
    mu       sync.RWMutex
    handlers map[string][]interface{} // map[eventName][]handlerFunc
}

func NewTypedMediator() *TypedMediator {
    return &TypedMediator{
        handlers: make(map[string][]interface{}),
    }
}

// Subscribe mendaftarkan handler untuk event tertentu.
func Subscribe[T Event](m *TypedMediator, handler Handler[T]) {
    var zero T
    eventName := zero.EventName()
    m.mu.Lock()
    defer m.mu.Unlock()
    m.handlers[eventName] = append(m.handlers[eventName], handler)
}

// Publish mengirimkan event ke semua handler yang terdaftar.
func Publish[T Event](m *TypedMediator, event T) []error {
    m.mu.RLock()
    handlers := m.handlers[event.EventName()]
    m.mu.RUnlock()

    var errs []error
    for _, h := range handlers {
        if typedHandler, ok := h.(Handler[T]); ok {
            if err := typedHandler(event); err != nil {
                errs = append(errs, err)
            }
        }
    }
    return errs
}

// Contoh penggunaan typed mediator
type UserRegisteredEvent struct {
    UserID   string
    Email    string
    Username string
}
func (e UserRegisteredEvent) EventName() string { return "user.registered" }

type OrderCreatedEvent struct {
    OrderID string
    UserID  string
    Amount  float64
}
func (e OrderCreatedEvent) EventName() string { return "order.created" }

func setupMediator() {
    m := NewTypedMediator()

    // Type-safe: handler menerima UserRegisteredEvent, bukan interface{}
    Subscribe(m, func(event UserRegisteredEvent) error {
        fmt.Printf("Sending welcome email to %s\n", event.Email)
        return nil
    })

    Subscribe(m, func(event UserRegisteredEvent) error {
        fmt.Printf("Setting up user profile for %s\n", event.Username)
        return nil
    })

    Subscribe(m, func(event OrderCreatedEvent) error {
        fmt.Printf("Processing order %s worth Rp %.0f\n", event.OrderID, event.Amount)
        return nil
    })

    // Publish — type-safe, tidak perlu type assertion di handler
    errs := Publish(m, UserRegisteredEvent{
        UserID: "user-1", Email: "[email protected]", Username: "alice",
    })
    for _, err := range errs {
        fmt.Printf("Handler error: %v\n", err)
    }
}

Mediator vs Observer: Kapan Mana #

Ini adalah perbandingan yang paling sering ditanyakan. Keduanya melibatkan notifikasi antar objek, tapi mekanisme dan tujuannya berbeda.

flowchart TD
    subgraph "Observer"
        S[Subject] -->|broadcast\ntanpa koordinasi| O1[Observer 1]
        S -->|broadcast| O2[Observer 2]
        S -->|broadcast| O3[Observer 3]
        note_obs["Subject tidak tahu apa yang\ndilakukan Observer dengan event-nya"]
    end

    subgraph "Mediator"
        C1[Comp A] -->|notify| M[Mediator]
        M -->|selectively notify\nberdasarkan aturan| C2[Comp B]
        M --> C3[Comp C]
        M -. "tidak dinotifikasi\n(mediator putuskan)" .-> C4[Comp D]
        note_med["Mediator tahu aturan:\nsiapa harus tahu apa"]
    end
Aspek Observer Mediator
Siapa yang koordinasi Tidak ada — Subject broadcast, Observer bereaksi sendiri Mediator aktif mengatur siapa mendapat apa
Pengetahuan tentang aturan Observer filter sendiri Mediator menentukan routing
Jumlah subject Biasanya satu subject, banyak observer Banyak komponen, semua bicara ke satu mediator
Coupling Subject-Observer loosely coupled Semua colleague hanya kenal mediator
Cocok untuk Event broadcasting, notifikasi Koordinasi kompleks multi-komponen

Bahaya: Mediator sebagai God Object #

Mediator paling rentan berkembang menjadi God Object — satu objek yang tahu dan mengurus segalanya. Ini menghilangkan manfaat yang seharusnya didapat.

// ANTI-PATTERN: Mediator yang mengerjakan bisnis logic
func (m *OrderMediator) Notify(sender Colleague, event string, data interface{}) {
    switch event {
    case "stock_updated":
        // Mediator langsung hitung harga — ini bisnis logic!
        if qty, ok := data.(int); ok && qty < 10 {
            newPrice := m.currentPrice * 1.2 // markup 20% kalau stok sedikit
            m.priceComp.SetPrice(newPrice)
        }
        // ...
    case "order_paid":
        // Mediator langsung update analytics — juga bukan tugasnya!
        m.analyticsDB.Insert("purchases", data)
    }
}

// BENAR: Mediator hanya mengatur routing, logika di komponen
func (m *OrderMediator) Notify(sender Colleague, event string, data interface{}) {
    switch event {
    case "stock_updated":
        // Mediator hanya routing — "beritahu price component tentang stock update"
        m.priceComp.OnStockUpdated(data)
    case "order_paid":
        // Mediator hanya routing — tidak tahu apa yang dilakukan analytics
        m.analyticsComp.OnOrderPaid(data)
    }
    // Logika ada di masing-masing komponen, bukan di mediator
}

Tanda-tanda mediator sudah terlalu besar dan perlu dipecah:

Mediator yang sehat:
  ✓ Hanya berisi routing logic ("kirim ini ke sana")
  ✓ Tidak berisi business logic
  ✓ Method-nya pendek dan sederhana
  ✓ Bisa di-test tanpa mock yang kompleks

Mediator yang sudah God Object:
  ✗ Berisi kalkulasi bisnis
  ✗ Mengakses database langsung
  ✗ Method punya ratusan baris kode
  ✗ Sulit di-test karena terlalu banyak dependency

Solusi ketika mediator terlalu besar: pecah menjadi beberapa mediator yang masing-masing bertanggung jawab atas satu domain.


Testing Mediator Pattern #

// MockColleague untuk testing mediator
type MockColleague struct {
    name           string
    mediator       Mediator
    receivedEvents []struct {
        event string
        data  interface{}
    }
}

func (m *MockColleague) ComponentName() string { return m.name }

func (m *MockColleague) SetMediator(med Mediator) { m.mediator = med }

func (m *MockColleague) Notify(event string, data interface{}) {
    m.mediator.Notify(m, event, data)
}

func (m *MockColleague) OnNotified(event string, data interface{}) {
    m.receivedEvents = append(m.receivedEvents, struct {
        event string
        data  interface{}
    }{event, data})
}

func (m *MockColleague) EventCount() int { return len(m.receivedEvents) }


func TestChatRoom_BroadcastToAllExceptSender(t *testing.T) {
    room := chat.NewChatRoom()
    alice := chat.NewUser("Alice")
    bob   := chat.NewUser("Bob")
    citra := chat.NewUser("Citra")

    alice.SetMediator(room)
    bob.SetMediator(room)
    citra.SetMediator(room)

    alice.Send("Hello everyone!")

    // Bob dan Citra harus terima, Alice tidak
    if len(bob.Inbox()) != 1 {
        t.Errorf("Bob should receive 1 message, got %d", len(bob.Inbox()))
    }
    if len(citra.Inbox()) != 1 {
        t.Errorf("Citra should receive 1 message, got %d", len(citra.Inbox()))
    }
    if len(alice.Inbox()) != 0 {
        t.Errorf("Alice should NOT receive her own message")
    }
}

func TestChatRoom_PrivateMessage_OnlyToRecipient(t *testing.T) {
    room := chat.NewChatRoom()
    alice := chat.NewUser("Alice")
    bob   := chat.NewUser("Bob")
    citra := chat.NewUser("Citra")

    alice.SetMediator(room)
    bob.SetMediator(room)
    citra.SetMediator(room)

    err := alice.SendTo("Bob", "Private message for Bob only")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    if len(bob.Inbox()) != 1 {
        t.Errorf("Bob should receive private message, got %d", len(bob.Inbox()))
    }
    if !bob.Inbox()[0].IsPrivate {
        t.Error("message should be marked as private")
    }
    if len(citra.Inbox()) != 0 {
        t.Error("Citra should NOT receive private message between Alice and Bob")
    }
}

func TestChatRoom_UnknownRecipient_ReturnsError(t *testing.T) {
    room := chat.NewChatRoom()
    alice := chat.NewUser("Alice")
    alice.SetMediator(room)

    err := alice.SendTo("NonExistentUser", "test")
    if err == nil {
        t.Error("expected error for unknown recipient")
    }
}

func TestChatRoom_Unregister_StopsMessages(t *testing.T) {
    room := chat.NewChatRoom()
    alice := chat.NewUser("Alice")
    bob   := chat.NewUser("Bob")

    alice.SetMediator(room)
    bob.SetMediator(room)

    room.Unregister("Bob")
    alice.Send("Hello Bob!")

    // Bob sudah keluar, tidak terima pesan baru
    if len(bob.Inbox()) != 0 {
        t.Errorf("Bob should not receive message after unregistering")
    }
}

Kapan Menggunakan dan Kapan Tidak #

GUNAKAN Mediator jika:
  ✓ Banyak komponen saling berinteraksi dan jumlah koneksi tumbuh tak terkendali
  ✓ Logika koordinasi antar komponen perlu dipusatkan di satu tempat
  ✓ Menambah komponen baru tidak boleh mempengaruhi komponen yang sudah ada
  ✓ Membangun chatroom, workflow engine, form UI kompleks, atau event bus
  ✓ Ingin bisa mengganti aturan koordinasi tanpa mengubah komponen

HINDARI Mediator jika:
  ✗ Hanya 2-3 komponen yang berinteraksi — coupling langsung lebih sederhana
  ✗ Komponen berinteraksi dengan cara yang sangat berbeda-beda — mediator akan jadi God Object
  ✗ Aturan koordinasi tidak akan berubah — abstraksi berlebihan
  ✗ Mediator sudah menjadi God Object — pertanda domain mediator terlalu luas

Checklist Review Mediator #

DESAIN:
  □ Setiap colleague hanya punya referensi ke mediator, tidak ke colleague lain
  □ Mediator hanya berisi routing logic, bukan business logic
  □ Colleague tidak tahu siapa yang akan dinotifikasi mediator
  □ Menambah colleague baru tidak mengubah colleague yang sudah ada

IMPLEMENTASI:
  □ Thread-safe: Register, Unregister, Notify menggunakan mutex
  □ Mediator menyalin list colleague sebelum iterasi untuk hindari deadlock
  □ Panggilan ke colleague di-wrap dengan recovery untuk hindari panic cascade

UKURAN MEDIATOR:
  □ Method Notify tidak punya lebih dari 10-15 case
  □ Tidak ada kalkulasi bisnis di dalam Mediator
  □ Mediator bisa di-test tanpa banyak mock yang kompleks

TESTING:
  □ Test broadcast: semua colleague yang seharusnya terima, menerima
  □ Test selective notify: colleague yang tidak relevan tidak menerima
  □ Test Unregister: colleague yang sudah unregister tidak menerima
  □ Test error handling: panic di satu colleague tidak crash yang lain

Ringkasan #

  • Mediator memusatkan logika koordinasi — komponen tidak perlu saling tahu; semuanya berbicara ke satu mediator yang mengatur siapa harus tahu apa.
  • N koneksi, bukan N² — tanpa mediator, 10 komponen bisa punya 45 koneksi; dengan mediator, hanya 10; ini adalah manfaat terbesar saat sistem tumbuh.
  • Mediator vs Observer: Observer broadcast tanpa koordinasi aktif; Mediator mengatur routing secara eksplisit berdasarkan aturan yang tersimpan di dalamnya.
  • Typed Event Mediator — versi modern dengan generics menghilangkan type assertion di handler; lebih aman dan lebih mudah di-refactor.
  • Mediator harus tipis — hanya routing logic, bukan business logic; kalau method Notify sudah ratusan baris, itu God Object yang perlu dipecah.
  • Workflow engine adalah Mediator — coordinator menentukan step mana yang dijalankan berdasarkan hasil step sebelumnya; setiap step hanya tahu coordinator.
  • Chatroom adalah use case klasik — user tidak saling tahu; semua pesan melalui ChatRoom yang menentukan siapa menerima apa.
  • Pecah mediator yang terlalu besar — domain yang berbeda sebaiknya punya mediator tersendiri; satu mediator untuk semua domain adalah resep God Object.

← Sebelumnya: Template Method   Berikutnya: Memento →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact