Díszítő programtervezési minta

A Wikipédiából, a szabad enciklopédiából

Az objektumorientált programozásban (OOP) a díszítő programtervező minta egy olyan programtervezési minta, amely lehetővé teszi adott objektumokhoz más viselkedések hozzáadását akár statikusan, akár dinamikusan anélkül, hogy hatással lenne az azonos osztályból származó többi objektumra.[1] A díszítő gyakran alkalmas arra is, hogy a program megfeleljen az egyértelmű felelősség elvének, mivel lehetővé teszi a felelősségek egyértelmű felosztását különböző osztályok között.[2]

Bevezetés[szerkesztés]

Díszítő minta UML osztály diagramja

A díszítő minta arra használható, hogy kiterjeszthető legyen (díszítés) számos objektum funkcionalitása statikusan vagy néhány esetben futási időben, függetlenül más azonos osztályhoz tartozó példányoktól, mindehhez alapozást nyújtva tervezési időben. Mindez megvalósítható egy új díszítő osztály tervezésével, amely becsomagolja az eredeti osztályt. A becsomagolás megvalósítható a következő lépések sorozatával:

  1. hozzunk létre egy "díszítő" osztályt az eredeti "komponens" alosztályaként (lásd az UML diagramot);
  2. a díszítő osztályban adjunk hozzá egy komponens referenciát mezőként;
  3. adjunk át egy komponenst a díszítő osztály konstruktorának, hogy kezdő értékkel töltse fel a komponens referenciát;
  4. a díszítő osztályban minden "Komponens" metódust irányítsunk át a "Komponens" referenciára és
  5. a konkrét díszítő osztályban felüldefiniálhatjuk bármelyik Komponens metódust, amelyik viselkedését módosítani szükséges.

Ezt a mintát arra tervezték, hogy többszörös díszítőket lehessen egymásra halmozni a másik tetején, minden alkalommal új funkcionalitást adva hozzá a felüldefiniált metódus(ok)hoz.

Jegyezzük meg, hogy a díszítők és az eredeti osztály objektum közös funkcionalitás halmazon osztoznak. A fenti diagramban az "operation()" metódus elérhető a díszítőkben és a díszített objektumban egyaránt.

A díszítő jellegzetességek lehetnek metódusok, tulajdonságok, vagy egyéb tagok, melyeket általában egy interfész deklarál, vagy osztály öröklődés definiál, amelyet a díszítők megosztanak egymás közt és a díszítő objektum között. A fenti példában a "Komponens" osztályt örökli mind a konkrét komponens, mind a díszítőből (Decorator) származó alosztályok.

A díszítő minta alternatívája az alosztályok képzésének. Az alosztályok képzésével viselkedések adhatók hozzá fordítási időben, és a változás kihat az eredeti osztály összes példányára, míg a díszítő minta használatával futási időben biztosítható új viselkedés egyéni objektumok számára.

Ez a különbség akkor válik nagyon fontossá, amikor sokféle független módon lehetséges a funkcionalitás kiterjesztése. Néhány objektumorientált programozási nyelvben az osztályok létrehozása futási időben nem lehetséges, és tipikusan nem lehetséges megjósolni tervezési időben, hogy a kiterjesztések milyen kombinációjára lesz szükség később. Ez azt jelentené, hogy egy új osztálynak el kéne készíteni az összes lehetséges kombinációját. Ezzel szemben a díszítők objektumok, melyek futási időben jönnek létre, kombinálhatók használat előtt. Az I/O folyamok megvalósítása mind a Java-ban, mind a .NET keretrendszerben alkalmazza a díszítő mintát.

Motiváció[szerkesztés]

Windows példa UML Diagramja

Példaként vegyünk egy ablakot egy ablakozó rendszerben. Ahhoz, hogy egy ablak tartalmát görgetni tudjuk, szükség lehet arra, hogy megfelelő vízszintes ill. függőleges görgetősávot adjunk hozzá. Tegyük fel, hogy az ablakokat a Window osztály példányai reprezentálják, és tegyük fel, hogy ennek az osztálynak nem létezik görgetősáv hozzáadás funkcionalitása. Készíthetünk alosztályt ScrollingWindow néven, amely ellátja ezt a feladatot, vagy készíthetünk egy ScrollingWindowDecorator-t, amely hozzáadja ezt a funkcionalitást a létező Window objektumokhoz. Ezen a ponton, bármelyik megoldás megfelelő lenne.

Most tegyük fel, hogy szeretnénk olyan képességet is, amely keretet ad hozzá az ablakunkhoz, de az eredeti Window osztálynak nincs támogatása ehhez sem. A ScrollingWindow alosztály problémával kerül szembe, mert valójában egy új típusú ablakot hoz létre. Ha szeretnénk keretet hozzáadni az összes ablakhoz, új osztályokat kell létrehoznunk WindowWithBorder (keretes ablak) és ScrollingWindowWithBorder (görgetősávos keretes ablak) néven. Nyilvánvalóan ez a probléma rosszabbra fordul minden új tulajdonság hozzáadásakor. A díszítő megoldás esetén csak egyszerűen készítünk egy új BorderedWindowDecorator-t (keretes ablak díszítőt) — futási időben, díszíthetjük a létező ablakot ScrollingWindowDecorator-ral (görgethető ablak díszítővel) vagy BorderedWindowDecorator-ral (keretes ablak díszítővel), vagy mindkettővel, igény szerint.

Megjegyzendő, hogy az előző példában a "SimpleWindow" és "Window" osztályok megvalósítják a "Window" interfészt, amely definiálja a "draw();" metódust és a "getDescription();" metódust. Ezek szükségesek ebben a felállásban, hogy díszíteni tudjuk az ablak vezérlőt.

Egy másik jó példa, ahol a díszítő használata kívánatos lehet, pl. ha egy objektum tulajdonságainak, ill. metódusainak az elérését korlátozni szükséges néhány szabály halmaz szerint, vagy esetleg számos párhuzamosan használt szabály halmaz szerint (pl. különböző felhasználói hitelesítések esetén, stb.). Ebben az esetben az eredeti objektumban az elérések szabályozásának megvalósítása helyett az változatlanul hagyható, és bárminemű korlátozástól mentesen használható. Az eredeti objektum becsomagolható egy elérést szabályozó díszítő objektumba, amely kizárólagosan szolgálhatja ki az eredeti objektum interfészének egy megengedett részhalmazát.

Használata[szerkesztés]

A díszítő lehetővé teszi a viselkedés futásidejű hozzáadását. Ha számítani kell a többalakúságra (polimorfizmus), és még egy interfészt kell figyelembe venni, akkor az illesztő programtervezési minta használható; ha pedig le kell egyszerűsíteni a viselkedést, akkor a homlokzat programtervezési minta jelenthet segítséget.[3]

Példák[szerkesztés]

Java[szerkesztés]

Első példa (ablak/görgetés esete)[szerkesztés]

A következő Java példa illusztrálja a díszítők használatát az ablak/görgetés esetén.

// a Window interfész osztály
public interface Window {
    public void draw(); // ablak rajzolás
    public String getDescription(); // visszaadja az ablak leírását
}

// egy egyszerű ablak kiterjesztése görgetősáv nélkül
class SimpleWindow implements Window {
    public void draw() {
        //ablak rajzolása
    }

    public String getDescription() {
        return "simple window";
    }
}

A következő osztályok díszítőket tartalmaznak az Window osztályhoz, beleértve a díszítő osztályokat magukat is.

// absztrakt díszítő osztály, amely megvalósítja a Window interfészt
abstract class WindowDecorator implements Window {
    protected Window decoratedWindow; // the Window being decorated

    public WindowDecorator (Window decoratedWindow) {
        this.decoratedWindow = decoratedWindow;
    }
    public void draw() {
        decoratedWindow.draw(); //delegation
    }
    public String getDescription() {
        return decoratedWindow.getDescription(); //delegation
    }
}

//az első konkrét díszítő, amely vízszintes görgetősáv funkcionalitást ad hozzá
class VerticalScrollBarDecorator extends WindowDecorator {
    public VerticalScrollBarDecorator (Window decoratedWindow) {
        super(decoratedWindow);
    }

    @Override
    public void draw() {
        super.draw();
        drawVerticalScrollBar();
    }

    private void drawVerticalScrollBar() {
        // draw the vertical scrollbar
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", including vertical scrollbars";
    }
}

//a második konkrét díszítő, amely horizontális görgetősáv funkcionalitást ad hozzá
class HorizontalScrollBarDecorator extends WindowDecorator {
    public HorizontalScrollBarDecorator (Window decoratedWindow) {
        super(decoratedWindow);
    }

    @Override
    public void draw() {
        super.draw();
        drawHorizontalScrollBar();
    }

    private void drawHorizontalScrollBar() {
        // draw the horizontal scrollbar
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", including horizontal scrollbars";
    }
}

Íme a teszt program, ami létrehoz egy olyan Window példányt, amely teljesen díszített (azaz vízszintes és függőleges görgetősávval rendelkezik), és kiírja a leírásukat:

public class DecoratedWindowTest {
    public static void main(String[] args) {
        // create a decorated Window with horizontal and vertical scrollbars
        Window decoratedWindow = new HorizontalScrollBarDecorator (
                new VerticalScrollBarDecorator(new SimpleWindow()));

        // print the Window's description
        System.out.println(decoratedWindow.getDescription());
    }
}

Ezen program kimenete a következő: "simple window, including vertical scrollbars, including horizontal scrollbars". Figyeljük meg, hogy a getDescription metódusa a két díszítőnek először visszaadja a díszített Window leírását, és azt díszíti egy utótaggal.

Második példa (kávékészítés esete)[szerkesztés]

A következő Java példa a díszítők használatát szemlélteti a kávékészítés esetén. Ebben a példában az esetünk csak a költségeket és a hozzávalókat tartalmazza.

// The abstract Coffee class defines the functionality of Coffee implemented by decorator
public abstract class Coffee {
    public abstract double getCost(); // returns the cost of the coffee
    public abstract String getIngredients(); // returns the ingredients of the coffee
}

// extension of a simple coffee without any extra ingredients
public class SimpleCoffee extends Coffee {
    public double getCost() {
        return 1;
    }

    public String getIngredients() {
        return "Coffee";
    }
}

A következő osztályok tartalmazzák a díszítőket az összes Coffee(kávé) osztályra, beleértve a díszítő osztályokat magukat.

// absztrakt díszítő osztály, amely kiterjeszti a Coffee absztrakt osztályt
public abstract class CoffeeDecorator extends Coffee {
    protected final Coffee decoratedCoffee;
    protected String ingredientSeparator = ", ";

    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }

    public double getCost() { // implementing methods of the abstract class
        return decoratedCoffee.getCost();
    }

    public String getIngredients() {
        return decoratedCoffee.getIngredients();
    }
}

// Decorator Milk that mixes milk with coffee
// note it extends CoffeeDecorator
class Milk extends CoffeeDecorator {
    public Milk(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    public double getCost() { // overriding methods defined in the abstract superclass
        return super.getCost() + 0.5;
    }

    public String getIngredients() {
        return super.getIngredients() + ingredientSeparator + "Milk";
    }
}

// Decorator Whip that mixes whip with coffee
// note it extends CoffeeDecorator
class Whip extends CoffeeDecorator {
    public Whip(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    public double getCost() {
        return super.getCost() + 0.7;
    }

    public String getIngredients() {
        return super.getIngredients() + ingredientSeparator + "Whip";
    }
}

// Decorator Sprinkles that mixes sprinkles with coffee
// note it extends CoffeeDecorator
class Sprinkles extends CoffeeDecorator {
    public Sprinkles(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    public double getCost() {
        return super.getCost() + 0.2;
    }

    public String getIngredients() {
        return super.getIngredients() + ingredientSeparator + "Sprinkles";
    }
}

Íme a tesztprogram, amely létrehoz egy Coffee példányt, amely teljes mértékben díszített (azaz tejjel, habbal, permettel) és kiszámolja kávé költségét és kiírja a hozzávalóit:

public class Main {
    
    public static final void main(String[] args) {
	Coffee c = new SimpleCoffee();
	System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());

	c = new Milk(c);
	System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());

	c = new Sprinkles(c);
	System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());

	c = new Whip(c);
	System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());

	// Note that you can also stack more than one decorator of the same type
	c = new Sprinkles(c);
	System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
    }
    
}

C++[szerkesztés]

Következő példa program egy C++-os megvalósítás:

Include-ok

#include <iostream>
#include <string>

Absztrakt alaposztályok

// Az absztrakt Coffee (kávé) osztály, amely definiálja a Coffee funkcionalitását, melyet a díszítő fog megvalósítani 
struct Coffee {
  virtual double getCost() = 0; // returns the cost of the coffee
  virtual std::string getIngredients() = 0; // returns the ingredients of the coffee
  virtual ~Coffee() = 0;
};
inline Coffee::~Coffee(){}

SimpleCoffee osztály.

// egy egyszerű kávé kiterjesztése extra hozzávalók nélkül
struct SimpleCoffee : Coffee {
  double getCost() {
    return 1.0;
  }

  std::string getIngredients() {
    return "Coffee";
  }
};

Díszítők

 
// Milk (tej) díszítő, amely tejet ad hozzá kávéhoz
struct MilkDecorator : Coffee {
public: 
  MilkDecorator(Coffee* basicCoffee)
    :basicCoffee_(basicCoffee) {
  }

  double getCost() { // providing methods defined in the abstract superclass
    return basicCoffee_->getCost() + 0.5;
  }

  std::string getIngredients() {
    return basicCoffee_->getIngredients() + ", " + "Milk";
  }
private:
  Coffee* basicCoffee_;
};
 
// Decorator Whip that adds whip to coffee
struct WhipDecorator : Coffee {
public:
  WhipDecorator(Coffee* basicCoffee)
    :basicCoffee_(basicCoffee) {
  }

  double getCost() {
    return basicCoffee_->getCost() + 0.7;
  }

  std::string getIngredients() {
    return basicCoffee_->getIngredients() + ", " + "Whip";
  }
private:
  Coffee* basicCoffee_;

};

Tesztprogram

int main() 
{
  SimpleCoffee s;
  std::cout << "Cost: " << s.getCost() << "; Ingredients: " << s.getIngredients() << std::endl;

  MilkDecorator m(&s);
  std::cout << "Cost: " << m.getCost() << "; Ingredients: " << m.getIngredients() << std::endl;

  WhipDecorator w(&s);
  std::cout << "Cost: " << w.getCost() << "; Ingredients: " << w.getIngredients() << std::endl;

  // Note that you can stack decorators:
  MilkDecorator m2(&w);
  std::cout << "Cost: " << m2.getCost() << "; Ingredients: " << m2.getIngredients() << std::endl;
}

A program kimenete:

Cost: 1.0; Ingredients: Coffee
Cost: 1.5; Ingredients: Coffee, Milk
Cost: 1.7; Ingredients: Coffee, Whip
Cost: 2.2; Ingredients: Coffee, Whip, Milk

Dinamikus nyelvek[szerkesztés]

A díszítő minta megvalósítható a dinamikus nyelvekkel, akár interfészekkel, akár OOP-ben hagyományos öröklődéssel.

Kapcsolódó szócikkek[szerkesztés]

Jegyzetek[szerkesztés]

  1. Gamma, et. al., Erich. Design Patterns. Reading, MA: Addison-Wesley Publishing Co, Inc., 175ff. o. (1995). ISBN 0-201-63361-2 
  2. How to Implement a Decorator Pattern. [2015. július 7-i dátummal az eredetiből archiválva].
  3. (2004) „Head First Design Patterns” (paperback) 1, 243, 252, 258, 260. o, Kiadó: O'Reilly. (Hozzáférés: 2012. július 2.)  

További információk[szerkesztés]

Fordítás[szerkesztés]

Ez a szócikk részben vagy egészben a Decorator pattern című angol Wikipédia-szócikk ezen változatának fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.