Description
The Decorator design pattern solves the problem of extending classes, where each object can be extended without affecting the functionality of other objects in the same class. This design pattern is similar to the Chain of Responsibility design pattern, but the difference is that all requests are received by all classes through a central element.
In other words, this design pattern allows adding functionality to an object without influencing others, thus bringing more flexibility to inheritance. This model creates a good alternative to the use of subclasses, which do not allow adjustments during execution, whereas this design pattern does.
Scenario
📚 You are developing a website that allows selling books online. The concept of this store is to sell books at fixed prices, so on some books you are cheaper than the competition! In your e-commerce, you sell several categories of books:
Category | Price |
---|---|
Computer books | 15$ |
Manga | 10$ |
Adventure novels | 20$ |
The minimum price for books is set at 5$ (as part of the business plan).
Your specifications for the launch of your platform are established, but you know that in the future there will be other categories. One solution would be to add more subclasses that will implement a common interface for other books. But the problem is that if they become numerous, we will have too many subclasses that will implement their logic where we would have common features for each book.
This is where the Decorator pattern comes to the rescue:
- We will have a common interface for all books
- This interface will be implemented by the decorator that will take care of gathering the information common to the objects.
- Each category will then extend the decorator and override the data it wants to modify.
Pros and Cons
Pros | Cons |
---|---|
✔️Flexibility compared to static inheritance, as it offers more possibilities for adding functionality. This allows with the decorator to add/remove functionality during execution. | ❌Complexity by adding many classes that can make the system more complex. This effect leads to debugging issues, since the number of implemented classes can waste time searching for solutions. |
✔️Code readability, in static inheritance we have a class hierarchy where the highest class is loaded. The decorator allows not to have a concrete class that defines all methods, but several classes that use methods specific to it. | ❌Not suitable for beginners as when the system becomes complex there is a lot of generated code. Which can make it difficult to understand for a beginner. |
✔️Performance: traditionally we would have a class that implements all methods, while the pattern advocates the use of specific functionality for a use case. |
🔹 When to use it?
- You often use it unconsciously when developing user interfaces.
- You use it when creating input and output data management.
- You implement an inheritance that can have a huge number of children and different functionalities.
UML Diagram
In the case of this example, we are developing an e-commerce site selling books online at a fixed price! We have several categories of books that could evolve in the future.
Faced with this situation, we notice that a decorator pattern is a very good solution.
But why? Let's imagine that a new category emerges, in the case of static inheritance we would have an implemented class that inherits from a class and so on. In the case of the decorator, each of the classes will add its own logic!
Implementation
Let's start by creating the basis of our design pattern: the interface BookCategory.
BookCategory
package com.design.decorator.category;
public interface BookCategory {
double cost();
}
Our base interface is not very complicated for now, it requires the implementation of a Cost
class that will define the cost of our book.
Alright, now that we have finished our interface, let's add a base book, which in our store refers to the minimum price of a book and has no real attached category.
BasicBook
package com.design.decorator.category.impl;
import com.design.decorator.category.BookCategory;
public class BasicBook implements BookCategory {
public BasicBook() {
System.out.println("Buy a basic book");
}
@Override
public double cost() {
return 5.00;
}
}
Perfect! Now let's get to the heart of the decorator. For this, we need our decorator to implement our BookCategory
interface. Also, we need a base object which will be our BasicBook
that will give us the basis of our categories.
BookDecorator
package com.design.decorator.decorator;
import com.design.decorator.category.BookCategory;
public class BookDecorator implements BookCategory {
private BookCategorybookCategory;
public BookDecorator(BookCategory bookCategory) {
this.bookCategory = bookCategory;
}
@Override
public double cost) {
return this.bookCategory.cost();
}
}
Our decorator is implemented, now let's move on to the classes that will extend our decorator and set their prices.
AdventureBook
package com.design.decorator.category.impl;
import com.design.decorator.category.BookCategory;
import com.design.decorator.decorator.BookDecorator;
public class AdventureBook extends BookDecorator {
public AdventureBook(BookCategory bookCategory) {
super(bookCategory);
}
@Override
public double cost() {
System.out.println("Buy adventure book");
return 15.00 + super.cost();
}
MangaBook
package com.design.decorator.category.impl;
import com.design.decorator.category.BookCategory;
import com.design.decorator.decorator.BookDecorator;
public class MangaBook extends BookDecorator {
public MangaBook(BookCategory bookCategory) {
super(bookCategory);
}
@Override
public double cost() {
System.out.println("Buy manga book");
return 5.00 + super.cost();
}
}
ItBook
package com.design.decorator.category.impl;
import com.design.decorator.category.BookCategory;
import com.design.decorator.decorator.BookDecorator;
public class ItBook extends BookDecorator {
public ItBook(BookCategory bookCategory) {
super(bookCategory);
}
@Override
public double cost() {
System.out.println("Buy it book");
return 10.00 + super.cost();
}
}
Alright, we have everything we need to test our code in our main
class! We just need to implement all of this:
Main
package com.design.decorator;
import com.design.decorator.category.BookCategory;
import com.design.decorator.category.impl.AdventureBook;
import com.design.decorator.category.impl.BasicBook;
import com.design.decorator.category.impl.ItBook;
import com.design.decorator.category.impl.MangaBook;
public class Main {
public static void main(String[] args) {
BookCategory basicBook = new BasicBook();
System.out.println("Basic book cost " + basicBook.cost() + "$");
//Manga
BookCategory manga = new MangaBook(basicBook); //wrapping
System.out.println("Manga book cost " + manga.cost() + "$");
//It
BookCategory it = new ItBook(basicBook); //wrapping
System.out.println("It book cost " + it.cost() + "$");
//Adventure
BookCategory adventure = new AdventureBook(basicBook); //wrapping
System.out.println("Adventure book cost " + adventure.cost() + "$");
}
}
Result :
Buy a basic book
Basic book cost 5.0$
Buy manga book
Manga book cost 10.0$
Buy it book
It book cost 15.0$
Buy adventure book
Adventure book cost 20.0$
Here, we have finished our implementation. What we can notice:
- We have created a concrete base class and we could have others in the future.
- Our logic is divided by book category and not based on a concrete class.
- Our decorator is the reference class for components of the same family.
- Our interface is the basis of our class and allows flexibility by not implementing an object directly.