Gå til hovedindhold

Decorator Pattern

Decorator pattern bruges når du vil tilføje funktionalitet til et objekt dynamisk uden at ændre objektet selv, og uden at lave en ny klasse for hver mulig kombination af funktionalitet.


Problemet
#

Forestil dig, at du skal bygge et notifikationssystem. Systemet skal kunne sende beskeder via email, SMS og e-boks. Nogle beskeder skal beskyttes med MitID og nogle steder skal ikke. Du skal kunne kombinere de forskellige features frit.

Hvis du forsøger at løse det med arv, ender du hurtigt her:

class EmailNotifier { }
class SMSNotifier { }
class EboksNotifier { }
class EmailAndSMSNotifier { }
class EmailAndEboksNotifier { }
class SMSAndEboksNotifier { }
class EmailAndSMSAndEboksNotifier { }
class MitIDProtectedEmailNotifier { }
class MitIDProtectedEmailAndSMSNotifier { }
// ... og  videre

Og så er vi ikke engang begyndt på logging, retry-logik eller kryptering. Klassehierarkiet eksploderer.


Løsningen: Decorator Pattern
#

I stedet for at lave en klasse for hver kombination, laver vi et fælles interface og wrapper objekter ind i hinanden. Hvert lag tilføjer sin egen funktionalitet og sender resten videre til det lag det wrapper. Du kan tænke på det som små blokke, der kan “klikke sig sammen” til en mere kompliceret funktionalitet.

Fælles interface
#

interface Notifier {
    void send(String message);
}

Basisimplementering
#

class BaseNotifier implements Notifier {
    public void send(String message) {
        System.out.println("Sender: " + message);
    }
}

Decorator-klasser
#

Hver decorator implementerer Notifier og wrapper en anden Notifier:

class EmailNotifier implements Notifier {
    private Notifier wrapped;

    EmailNotifier(Notifier notifier) {
        this.wrapped = notifier;
    }

    public void send(String message) {
        System.out.println("Sender email: " + message);
        wrapped.send(message);
    }
}

class SMSNotifier implements Notifier {
    private Notifier wrapped;

    SMSNotifier(Notifier notifier) {
        this.wrapped = notifier;
    }

    public void send(String message) {
        System.out.println("Sender SMS: " + message);
        wrapped.send(message);
    }
}

class EboksNotifier implements Notifier {
    private Notifier wrapped;

    EboksNotifier(Notifier notifier) {
        this.wrapped = notifier;
    }

    public void send(String message) {
        System.out.println("Sender e-boks: " + message);
        wrapped.send(message);
    }
}

class MitIDProtectedNotifier implements Notifier {
    private Notifier wrapped;

    MitIDProtectedNotifier(Notifier notifier) {
        this.wrapped = notifier;
    }

    public void send(String message) {
        System.out.println("Verificerer MitID...");
        wrapped.send(message);
    }
}

Brug
#

Når vi “klikker” vores notifiere sammen (wrapper), så kan vi lave en funktionalitet, som kan være beskyttet af MitID, samt sende til SMS og email. Eller hvilken som helst anden kombination, vi ønsker.

For eksempel kan du kombinere

// Email og SMS
Notifier notifier = new EmailNotifier(new SMSNotifier(new BaseNotifier()));
notifier.send("Din ordre er afsendt");

// Email, SMS og e-boks med MitID-beskyttelse
Notifier sikkerNotifier = new MitIDProtectedNotifier(new EmailNotifier(new SMSNotifier(new EboksNotifier(new BaseNotifier()))));
sikkerNotifier.send("Din årsopgørelse er klar");

Der er ingen nye klasser og ingen eksploderende hierarki. Du klikker det sammen som du har brug for det.


Rækkefølge betyder noget
#

Det er ikke ligegyldigt hvordan du wrapper. Disse to er forskellige:

// MitID-beskyttelse udenpå det hele — brugeren skal verificere før noget sendes
new MitIDProtectedNotifier(new EmailNotifier(new SMSNotifier(new BaseNotifier())))

// Kun SMS-delen er MitID-beskyttet — email sendes uden verificering
new EmailNotifier(new MitIDProtectedNotifier(new SMSNotifier(new BaseNotifier())))

I det første tilfælde skal brugeren verificere med MitID før der overhovedet sendes noget. I det andet sendes emailen frit, men SMS-delen kræver verificering. Det er en reel og vigtig forskel — og den kommer naturligt ud af hvordan du wrapper.


Et eksempel fra Javas API
#

Decorator pattern er ikke en abstrakt opfindelse, men bruges fx i Java IO:

BufferedReader reader = new BufferedReader(new FileReader("fil.txt"));

FileReader læser fra en fil. BufferedReader wrapper den og tilføjer buffering, så du ikke læser én byte ad gangen. Du kan tilføje flere lag:

new DataInputStream(new BufferedInputStream(new FileInputStream("fil.txt")))

Hvert lag er en InputStream. Hvert lag tilføjer noget. Og du kan kombinere dem frit præcis som med notifikationerne.


Hvornår bruger du det?
#

Decorator pattern er et godt valg når:

  • Du har brug for at kombinere funktionalitet frit uden at antallet af klasser eksploderer
  • Du vil tilføje funktionalitet til et objekt uden at ændre det
  • Rækkefølgen af funktionalitet har betydning