Mastering Design Patterns: Decorator

Mastering Design Patterns: Decorator

Understanding Decorator Design Pattern

Definition

"The ability to attach additional responsibilities to an object dynamically. Decorators, provide a flexible alternative to subclassing for extending functionality."

In the complex realm of design patterns, the Decorator, known alternatively as the Wrapper, stands out as a robust structural construct. Its unique strength lies in the ability to amplify the behavior of a specific object either statically or dynamically without influencing the behaviours of other entities from the identical class.?

We conceptualize the Decorator Pattern as a set of decorator classes, each of which is designated to encase concrete objects. These decorators serve as a mirror to the types of the objects they encapsulate - they are indistinguishable when viewed from an interface standpoint.?

This pattern emerges as a potent alternative to subclassing. While subclassing imparts behavior at compile time, consequently affecting all instances of the original class, decorating flexes its versatility by offering new behavior at run-time, with the impact confined to the individual objects.


Key Components of Decorator Design Pattern

Component: This is an abstract interface that defines the types of objects that are primed for the dynamic addition of responsibilities.

Concrete Component: This defines the object that stands ready to have responsibilities added to it.

Decorator: This contains a reference to a Component object(wraps a component) and outlines an interface in conformance with the Component's interface.

Concrete Decorators: These extend the Decorator class, adding responsibilities to the Concrete Component.

No hay texto alternativo para esta imagen


Things that we know about Decorator

  • Decorators have the same supertype as the objects that they decorate.
  • You can use one or more decorators to wrap the object.
  • Given that the decorator has the same supertype as the object it decorates, we can pass around a decorated object in place of the original(wrapped) object.
  • The decorator adds its own behaviour before and/or after the delegating to the object it decorates to do the rest of the job.
  • Objects can be decorated at any time, so we can decorate objects dynamically at runtime with as many decorators as we like.


Decorator Design Pattern: Recommended Practices

Prioritize composition over inheritance: The decorator pattern champions the power of composition, providing a pathway to append new functionality devoid of any modifications to the existing system. This feat is accomplished by housing the old code within a specialized wrapper that injects the new features.

Keep component classes lean: The Component classes should zero in on core functionality, with all additional functionality deflected to the decorators.

Synchronization of interfaces between decorators and components: The decorators should reflect the types of the components they encapsulate. They must be indistinguishable from the components they decorate at the interface level.


Examples

Let's delve deeper into the process flow of the Decorator Design Pattern, while shedding light on its working principles through an illustrative example.?

Do you want a slice of pizza?

In this example, we are going to create a pizza and decorate it with mozzarella and Pepperoni topping. Looks yummy, right? Let's write some "slices of code" to further explore the benefits of Decorator Design Pattern.

Lest Start from the class diagram.

No hay texto alternativo para esta imagen
Class Diagram

Component (Pizza): This is an interface that defines operations that can be altered by decorators. In our case, "Pizza" is our component which has a method "getDescription()" to return the description of the pizza and "getCost()" to return the cost of the pizza.

Concrete Component (PlainPizza): This is a class implementing the "Pizza" component. It represents an object to which we will add new behavior. "PlainPizza" is our concrete component which has default behaviour and properties of a basic pizza.

Decorator (PizzaDecorator): This is also an interface but it maintains a reference to a Component object - in this case, an instance of "Pizza" - and defines an interface that conforms to Component's interface. "PizzaDecorator" is our decorator which implements "Pizza" interface and it is used as a base class for all future decorators. The decorators will inherit this class and use its constructor to set the pizza instance.

Concrete Decorators (MozzarellaDecorator, PepperoniDecorator): These are the classes that add responsibilities to the component. They modify the component's behavior accordingly when the component's operations are invoked. In our example, "MozzarellaDecorator" and "PepperoniDecorator" are our concrete decorators. They also implement the "Pizza" interface and are used to add toppings (additional responsibilities) to the pizza.

Let's take the "MozzarellaDecorator" as an example: it takes a "Pizza" object (which could be a "PlainPizza" or a decorated pizza), and adds its own behavior (adding mozzarella) to "getDescription()" and "getCost()" methods. This means it is taking the original pizza, and adding mozzarella to it both in terms of description and cost.

The same concept applies to "PepperoniDecorator", which also decorates a "Pizza" object by adding pepperoni.


Java Example Explained

Step 1 - Setup

In the Decorator pattern, the initial setup involves establishing a 'Component' interface and a 'Concrete Component' that implements this interface. This concrete component represents the base object we'll be adding new functionalities to.

// Component
public interface Pizza {
? ? String getDescription();
? ? double getCost();
}


// Concrete Component
public class PlainPizza implements Pizza {
? ? @Override
? ? public String getDescription() {
? ? ? ? return "Plain dough";
? ? }


? ? @Override
? ? public double getCost() {
? ? ? ? return 4.00;
? ? }
}        

In this setup, we've defined a basic 'Pizza' interface and a 'PlainPizza' as our concrete component. This PlainPizza comes with dough and has a base cost.


Step 2 - Building Decorator Structure

Next, we define a Decorator class that also implements the 'Component' interface. This Decorator class maintains a reference to a Component object and can be used as a base class for all future decorators.

public abstract class PizzaDecorator implements Pizza {
? ? protected Pizza tempPizza;


? ? public PizzaDecorator(Pizza newPizza) {
? ? ? ? tempPizza = newPizza;
? ? }


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


? ? @Override
? ? public double getCost() {
? ? ? ? return tempPizza.getCost();
? ? }
}        

In our pizza example, we've now created a 'PizzaDecorator' which can be used to add additional toppings to our pizza.


Step 3 - Implementing Concrete Decorators:

We then implement multiple concrete decorators by extending the base decorator. Each of these concrete decorators adds new behaviours or states to the component they're decorating.

public class Mozzarella extends PizzaDecorator {
? ? public Mozzarella(Pizza newPizza) {
? ? ? ? super(newPizza);
? ? }


? ? @Override
? ? public String getDescription() {
? ? ? ? return tempPizza.getDescription() + ", mozzarella";
? ? }


? ? @Override
? ? public double getCost() {
? ? ? ? return tempPizza.getCost() + 0.50;
? ? }
}        

In our example, we added 'Mozzarella' as a concrete decorator that augments our PlainPizza with mozzarella and adjusts the cost accordingly.


Step 4 - Decorating Components

At runtime, we can now wrap our 'Concrete Component' (PlainPizza) with any number of 'Concrete Decorators' (like Mozzarella), each adding new behavior or state.

Pizza basicPizza = new PlainPizza();
Pizza mozzarellaPizza = new Mozzarella(basicPizza);
System.out.println(mozzarellaPizza.getDescription());
System.out.println(mozzarellaPizza.getCost());        

We've now created a PlainPizza and decorated it with Mozzarella, then printed its description and cost. The decorators have dynamically added new behaviors to our PlainPizza without modifying the base PlainPizza class.


Hey, but what If I what Pepperoni Pizza? Do I have to change everything?

Unwind, and join us as we delve into the artistry of pizza creation! With a sprinkle of topping on a foundation of mozzarella pizza, culinary magic is born!?

In this instance, we'll adorn our masterpiece with the Pepperoni Decorator, which graciously extends from the PizzaDecorator. As simple as that, and yet a world of flavor is unlocked!

public static class Pepperoni extends PizzaDecorator {
? ? public Pepperoni(Pizza newPizza) {
? ? ? ? super(newPizza);
? ? }

? ? @Override
? ? public String getDescription() {
? ? ? ? return tempPizza.getDescription() + ", pepperoni";
? ? }

? ? @Override
? ? public double getCost() {
? ? ? ? return tempPizza.getCost() + 0.75; // Assume pepperoni costs an extra 75 cents.
? ? }
}        

Let's try some Pepperoni Pizza:


public static void main(String[] args) {
? ? Pizza basicPizza = new PlainPizza();
? ? Pizza mozzarellaPizza = new Mozzarella(basicPizza);
? ? Pizza pepperoniPizza = new Pepperoni(mozzarellaPizza);
? ? System.out.println(pepperoniPizza.getDescription());
? ? System.out.println(pepperoniPizza.getCost());
}        


Additional Examples

Python

from abc import ABC, abstractmethod


class Pizza(ABC):
? ? @abstractmethod
? ? def get_description(self):
? ? ? ? pass


? ? @abstractmethod
? ? def get_cost(self):
? ? ? ? pass


class PlainPizza(Pizza):
? ? def get_description(self):
? ? ? ? return "Thin crust, tomato sauce"


? ? def get_cost(self):
? ? ? ? return 6.00


class PizzaDecorator(Pizza):
? ? def __init__(self, pizza):
? ? ? ? self.temp_pizza = pizza


? ? def get_description(self):
? ? ? ? return self.temp_pizza.get_description()


? ? def get_cost(self):
? ? ? ? return self.temp_pizza.get_cost()


class Mozzarella(PizzaDecorator):
? ? def __init__(self, pizza):
? ? ? ? super().__init__(pizza)


? ? def get_description(self):
? ? ? ? return self.temp_pizza.get_description() + ", mozzarella"


? ? def get_cost(self):
? ? ? ? return self.temp_pizza.get_cost() + 0.50


class Pepperoni(PizzaDecorator):
? ? def __init__(self, pizza):
? ? ? ? super().__init__(pizza)


? ? def get_description(self):
? ? ? ? return self.temp_pizza.get_description() + ", pepperoni"


? ? def get_cost(self):
? ? ? ? return self.temp_pizza.get_cost() + 0.75


basic_pizza = PlainPizza()
mozzarella_pizza = Mozzarella(basic_pizza)
pepperoni_pizza = Pepperoni(mozzarella_pizza)
print(pepperoni_pizza.get_description())
print(pepperoni_pizza.get_cost())        


C#(.NET)?

public interface IPizza
{
? ? string GetDescription();
? ? double GetCost();
}

public class PlainPizza : IPizza
{
? ? public string GetDescription()
? ? {
? ? ? ? return "Thin crust, tomato sauce";
? ? }


? ? public double GetCost()
? ? {
? ? ? ? return 6.00;
? ? }
}

public abstract class PizzaDecorator : IPizza
{
? ? protected IPizza TempPizza;
? ? protected PizzaDecorator(IPizza newPizza)
? ? {
? ? ? ? TempPizza = newPizza;
? ? }


? ? public virtual string GetDescription()
? ? {
? ? ? ? return TempPizza.GetDescription();
? ? }


? ? public virtual double GetCost()
? ? {
? ? ? ? return TempPizza.GetCost();
? ? }
}


public class Mozzarella : PizzaDecorator
{
? ? public Mozzarella(IPizza newPizza) : base(newPizza) { }


? ? public override string GetDescription()
? ? {
? ? ? ? return TempPizza.GetDescription() + ", mozzarella";
? ? }


? ? public override double GetCost()
? ? {
? ? ? ? return TempPizza.GetCost() + 0.50;
? ? }
}

public class Pepperoni : PizzaDecorator
{
? ? public Pepperoni(IPizza newPizza) : base(newPizza) { }


? ? public override string GetDescription()
? ? {
? ? ? ? return TempPizza.GetDescription() + ", pepperoni";
? ? }


? ? public override double GetCost()
? ? {
? ? ? ? return TempPizza.GetCost() + 0.75;
? ? }
}        

To run the program:

public class Program
{
? ? static void Main(string[] args)
? ? {
? ? ? ? IPizza basicPizza = new PlainPizza();
? ? ? ? IPizza mozzarellaPizza = new Mozzarella(basicPizza);
? ? ? ? IPizza pepperoniPizza = new Pepperoni(mozzarellaPizza);
? ? ? ? Console.WriteLine(pepperoniPizza.GetDescription());
? ? ? ? Console.WriteLine(pepperoniPizza.GetCost());
? ? }
}        


Javascript

class Pizza {
? ? getCost() {}
? ? getDescription() {}
}


class PlainPizza extends Pizza {
? ? getCost() {
? ? ? ? return 7;
? ? }
? ? getDescription() {
? ? ? ? return 'Plain Pizza';
? ? }
}


class PizzaDecorator extends Pizza {
? ? constructor(pizza) {
? ? ? ? super();
? ? ? ? this.decoratedPizza = pizza;
? ? }
}


class MozzarellaDecorator extends PizzaDecorator {
? ? constructor(pizza) {
? ? ? ? super(pizza);
? ? }
? ? getCost() {
? ? ? ? return this.decoratedPizza.getCost() + 0.5;
? ? }
? ? getDescription() {
? ? ? ? return this.decoratedPizza.getDescription() + ', Mozzarella';
? ? }
}


class PepperoniDecorator extends PizzaDecorator {
? ? constructor(pizza) {
? ? ? ? super(pizza);
? ? }
? ? getCost() {
? ? ? ? return this.decoratedPizza.getCost() + 0.7;
? ? }
? ? getDescription() {
? ? ? ? return this.decoratedPizza.getDescription() + ', Pepperoni';
? ? }
}


// Let's create a pizza
let pizza = new PlainPizza();
pizza = new MozzarellaDecorator(pizza);
pizza = new PepperoniDecorator(pizza);

// Outputs: 'Plain Pizza, Mozzarella, Pepperoni'
console.log(pizza.getDescription()); 
// Outputs: 8.2
console.log(pizza.getCost());         


Conclusion

The Decorator design pattern provides an effective way to add functionality to objects dynamically, without altering their structure. By using this pattern, we maintain the Open-Closed principle and increase flexibility and extensibility. As shown with the pizza example, decorators can be stacked to create complex combinations while keeping our code clean and maintainable.

We truly hope you enjoyed this exploration of the Decorator design pattern. It's a compelling technique with the potential to significantly elevate the flexibility and maintainability of your code. Looking forward to engaging more with you on these essential topics in future posts!


Credits

Special thanks to Eric Freeman, Ph.D.,?Elisabeth Robson, Bert Bates, and Kathy Sierra for writing such a wonderful book "Head First Design Patterns".


#DesignPatterns?#SoftwareEngineering?#DecoratorDesignPattern?#BestPractices?#Kubrik?#MasteringDesignPatterns?#Programming?#KubrikDigital?#.NET?#Java?#Python?#Javascript?#Typescript

Vittorio Pugliese

Web and Mobile Developer. TypeScript, React, React Native, Expo, Angular.

1 年

Great! thanks for spreading valuable information

Juan Couselo

Developer in Kubrik Digital

1 年

Great post! Where can I find my pizza slice? ??

Ivan Burbello

Programador Full Stack

1 年

Amazing!

要查看或添加评论,请登录

Kubrik Digital的更多文章

社区洞察

其他会员也浏览了