Dekorator

Dekorator

Wzorzec Dekorator (Decorator) jest jednym z wzorców strukturalnych. Pozwala on na dynamiczne rozszerzanie funkcjonalności obiektów, bez potrzeby modyfikacji ich kodu źródłowego. Dzięki wzorcowi Dekorator możemy “opakowywać” obiekty innymi obiektami, które dodają nowe zachowania lub właściwości.

Cechy charakterystyczne:

  • Opakowywanie obiektów: Zamiast tworzyć nową klasę, która dziedziczy po bazowej, dekorujemy obiekt innymi obiektami, które zmieniają jego zachowanie.
  • Elastyczne dodawanie funkcji: Funkcjonalności można dodawać w czasie działania programu, bez modyfikowania istniejących klas.
  • Zachowanie interfejsu: Każdy dekorator implementuje ten sam interfejs, co dekorowany obiekt, dzięki czemu dekoratory mogą być używane zamiennie z oryginalnymi obiektami.

Kiedy używać wzorca Dekorator?

  • Gdy chcesz dynamicznie dodawać nowe funkcjonalności do obiektów, ale nie chcesz modyfikować ich kodu źródłowego.
  • Gdy potrzebujesz różnych kombinacji funkcji, które mogą być dodawane lub usuwane w trakcie działania programu.
  • Gdy dziedziczenie nie jest elastyczne lub prowadziłoby do nadmiernego wzrostu liczby klas.

Przykład bez wzorca Dekorator

Dla przykładu weźmiemy system zamawiania kawy, gdzie można dodawać różne dodatki do podstawowej kawy, np. mleko i cukier. W tym przykładzie, dla każdej kombinacji kawy musimy tworzyć nowe klasy, co prowadzi do nadmiernej liczby klas.

// Klasa bazowa Kawa
class Coffee {
    public String getDescription() {
        return "Zwykła kawa";
    }
}

// Kawa z mlekiem
class CoffeeWithMilk extends Coffee {
    @Override
    public String getDescription() {
        return "Zwykła kawa, z mlekiem";
    }
}

// Kawa z cukrem
class CoffeeWithSugar extends Coffee {
    @Override
    public String getDescription() {
        return "Zwykła kawa, z cukrem";
    }
}

// Kawa z mlekiem i cukrem
class CoffeeWithMilkAndSugar extends Coffee {
    @Override
    public String getDescription() {
        return "Zwykła kawa, z mlekiem, z cukrem";
    }
}

// Klient
public class Main {
    public static void main(String[] args) {
        // Kawa z mlekiem i cukrem
        Coffee coffee = new CoffeeWithMilkAndSugar();
        System.out.println(coffee.getDescription());

        // Kawa z mlekiem
        coffee = new CoffeeWithMilk();
        System.out.println(coffee.getDescription());

        // Kawa z cukrem
        coffee = new CoffeeWithSugar();
        System.out.println(coffee.getDescription());
    }
}

Problemy z takim podejściem:

  • Duplikacja kodu: Każda kombinacja kawy (np. z mlekiem, cukrem, mlekiem i cukrem) wymaga utworzenia osobnej klasy.
  • Brak elastyczności: Każdorazowe dodanie nowego dodatku wymaga stworzenia nowej klasy dla każdej możliwej kombinacji.
  • Trudna rozbudowa: Dodanie kolejnych dodatków (np. syropu) powoduje jeszcze większą eksplozję klas.

Przykład z użyciem wzorca Dekorator

// Interfejs Kawa (Coffee)
interface Coffee {
    String getDescription();
}

// Konkretna klasa podstawowej kawy
class BasicCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Zwykła kawa";
    }
}

// Abstrakcyjny dekorator
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

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

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription();
    }
}

// Konkretne dekoratory
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", z mlekiem";
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return decoratedCoffee.getDescription() + ", z cukrem";
    }
}

// Klient
public class Main {
    public static void main(String[] args) {
        // Tylko kawa z cukrem
        Coffee coffeeWithSugar = new SugarDecorator(new BasicCoffee());
        System.out.println(coffeeWithSugar.getDescription());

        // Kawa z mlekiem i cukrem
        Coffee coffeeWithMilkAndSugar = new SugarDecorator(new MilkDecorator(new BasicCoffee()));
        System.out.println(coffeeWithMilkAndSugar.getDescription());

        // Tylko kawa z mlekiem
        Coffee coffeeWithMilk = new MilkDecorator(new BasicCoffee());
        System.out.println(coffeeWithMilk.getDescription());
    }
}

Zasada działania:

  • BasicCoffee to nasza podstawowa kawa.
  • MilkDecorator dodaje opis “z mlekiem” do podstawowej kawy.
  • SugarDecorator dodaje “z cukrem”, budując opis krok po kroku, bez konieczności zmiany klasy podstawowej.

Zalety wzorca Dekorator:

  • Elastyczność: Możemy dynamicznie dodawać nowe funkcjonalności (dodatki) bez tworzenia nowych klas.
  • **Zgodność z zasadą Open/Closed Principle: Obiekty są otwarte na rozszerzenia, ale zamknięte na modyfikacje.
  • Modularność: Możemy dowolnie łączyć dekoratory, np. kawa z mlekiem, cukrem lub oba.

Wnioski:

Dzięki wzorcowi Dekorator unikasz tworzenia dużej liczby klas dla każdej kombinacji dodatków, co czyni system bardziej elastycznym i łatwiejszym w zarządzaniu.

Scroll to Top