Observer (Observatör)

Introduktion

Observer är ett beteendemönster som låter dig definiera en prenumerationsmekanism för att notifiera flera objekt om händelser som inträffar i objektet de observerar.


Problem

Tänk dig att du har en Butik och en Kund. Kunden vill veta när en produkt kommer i lager, men det är slöseri att kolla varje dag — och butiken vill inte spamma alla kunder. Observer löser detta med en opt-in prenumerationsmekanism.

Lösning

Observer-mönstret lägger till en prenumerationsmekanism i objektet med ett intressant tillstånd (Publisher). Individuella objekt väljer själva att prenumerera och tar emot notifieringar automatiskt.

När ska Observer användas?

  • När en ändring i ett objekt kräver att andra objekt ändras, och du inte vet i förväg hur många.
  • När ett objekt ska notifiera andra utan att göra antaganden om vilka de är.
  • För händelsestyrd arkitektur och reaktiva system.

Struktur

RollAnsvar
PublisherHåller listan av observatörer och tillhandahåller subscribe/unsubscribe.
Subscriber InterfaceDefinierar notifieringsgränssnittet.
Concrete SubscriberImplementerar reaktionen på notifieringen.

Exempel — Filhanteringssystem (TypeScript)

Scenariot: En Editor publicerar händelser (open, save) och prenumeranter reagerar automatiskt.

// ── Subscriber Interface ───────────────────────────────────

interface EventListener {
  update(eventType: string, fileName: string): void;
}

// ── Publisher ─────────────────────────────────────────────

class EventManager {
  private listeners: Map<string, EventListener[]> = new Map();

  constructor(...operations: string[]) {
    for (const op of operations) {
      this.listeners.set(op, []);
    }
  }

  subscribe(eventType: string, listener: EventListener): void {
    this.listeners.get(eventType)?.push(listener);
  }

  unsubscribe(eventType: string, listener: EventListener): void {
    const list = this.listeners.get(eventType) ?? [];
    const idx  = list.indexOf(listener);
    if (idx !== -1) list.splice(idx, 1);
  }

  notify(eventType: string, fileName: string): void {
    for (const listener of this.listeners.get(eventType) ?? []) {
      listener.update(eventType, fileName);
    }
  }
}

class Editor {
  public events = new EventManager('open', 'save');
  private currentFile = '';

  openFile(path: string): void {
    this.currentFile = path;
    this.events.notify('open', path);
  }

  saveFile(): void {
    this.events.notify('save', this.currentFile);
  }
}

// ── Concrete Subscribers ──────────────────────────────────

class EmailNotificationListener implements EventListener {
  constructor(private email: string) {}

  update(eventType: string, fileName: string): void {
    console.log(`E-post till ${this.email}: händelse '${eventType}' på fil ${fileName}`);
  }
}

class LoggingListener implements EventListener {
  constructor(private logFile: string) {}

  update(eventType: string, fileName: string): void {
    console.log(`Logg [${this.logFile}]: ${eventType} - ${fileName}`);
  }
}

// ── Klientkod ─────────────────────────────────────────────

const editor = new Editor();

editor.events.subscribe('open', new LoggingListener('log.txt'));
editor.events.subscribe('save', new EmailNotificationListener('admin@bytebase.se'));
editor.events.subscribe('save', new LoggingListener('log.txt'));

editor.openFile('rapport.docx');
editor.saveFile();

Output:

Logg [log.txt]: open - rapport.docx
E-post till admin@bytebase.se: händelse 'save' på fil rapport.docx
Logg [log.txt]: save - rapport.docx

Fördelar

  • Open/Closed Principle — Lägg till nya subscribers utan att ändra publisherns kod.
  • Prenumerationsrelationer kan etableras och avslutas vid runtime.

Nackdelar

  • Subscribers notifieras i obestämd ordning.
  • Svårt att spåra händelsekedjan i komplexa system.

Av Victor Hernandez från Bytebase.se