Patternskolen del 7 – Decorator vs. Arv

I denne artikkelen skal vi se på Decorator.

Bjørn Herve Moslet

I forrige utbrudd så vi på State. Denne gangen skal vi se på Decorator, som noen ganger kan erstatte et av de mest kjente medlemmene i den objektorienterte verktøykassa: arv.

Decorator pattern

Decorator brukes til å utvide funksjonaliteten i en klasse, mye i likhet med arv. Decorator kalles av og til «wrapper», men det er greit å unngå dette navnet da Adapter patternet også omtales med samme navn. Hensikten er å utvide funksjonaliteten i en klasse ved at dekoratorklassen pakker den inn (eller «dekorerer» den med ny funksjonalitet, om du vil). Diagrammet under viser hvordan dette kan implementeres:

decorator

Vi tar utgangspunkt i et interface eller en abstrakt klasse, kalt Komponent i figuren. Den må vi ha en eller flere konkrete implementasjoner av (KonkretKomponent).

For å dekorere denne lager vi en abstrakt klasse, i dette tilfellet kalt Decorator. Den har en medlemsvariabel som peker til komponenten vi dekorerer.

Til slutt oppretter vi én eller flere konkrete implementasjoner av Decorator. I diagrammet over har vi bare én metode (operasjon), men vi kunne hatt mange flere. I dekoratørene videreformidles alle metodekall til den konkrete klassen, med unntak av de vi ønsker å overstyre med ny funksjonalitet.

Dette gjør at man kan dekorere en klasse med flere dekoratører og legge til flere features.

Eksempler

Har du jobbet med Stream-klassene i Java eller .NET, har du vært borti Decorator allerede.

For eksempel i Java:

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

Eller i .NET:

var bs = new BufferedStream(new NetworkStream(socket, true), size);

Under følger et eksempel i .NET som viser hvordan vi kan bruke flere dekoratører samtidig:

public void Eksempel(SqlFileStream sqlstrøm)

{

    // Opprettelse av de øvrige parametrene til dekoratørene er utelatt

    var bufferedStream = new BufferedStream(sqlstrøm);

    var gzipStream = new GZipStream(bufferedStream, level);

    var cryptoStream = new CryptoStream(gzipStream, transform, mode);

    // Les fra strømmen

}


I eksempelet ønsker vi å lese en fil fra en database. Når vi leser fra en strøm, kan vi få bedre ytelse ved å bufre leseoperasjoner og vi dekorerer derfor med BufferedStream. For å spare lagringsplass har vi komprimert filinnholdet med GZip. For å dekomprimere dekorerer vi med GZipStream. Innholdet er også kryptert, og vi avslutter med å dekryptere med CryptoStream. Dette er neppe et godt eksempel på bruk av disse klassene, men illustrerer bruken av Decorator.

Vi kan relatere .NET-klassene til UML-diagrammet over: Den abstrakte klassen System.IO.Stream er Komponent. System.Data.SqlTypes.SqlFileStream er KonkretKomponent. System.IO.BufferedStream, System.IO.Compression.GZipStream og System.Security.Cryptography.CryptoStream er eksempler på KonkretDecorator.

Konklusjon

Som vi tidligere har sett med for eksempel Strategy og Template method så vil noen patterns trå i kraft i runtime og andre ved kompilering. Slik er det også med arv og Decorator. Ved arv legger man til funksjonalitet ved kompilering av koden, mens med Decorator skjer det i runtime.

Styrken i Decorator ligger i de tilfellene det er vanskelig eller praktisk umulig å implementere mange nok arvende klasser som tilbyr alle mulige kombinasjoner. For eksempel i Stream-klassene i Java og .NET, der vi ikke har lyst til å implementere klasser for varianter av FileStream, BufferedFileStream, CryptoBufferedFileStream, GZipBufferedFileStream, DeflateCryptoBufferedFileStream, GZipCryptoBufferedFileStream osv.

En ser også at Decorator gjør det enkelt å følge «Open/close principle» innen objektorientering.

Temaer