Decorator Pattern: Let's have an ice cream!

I'm an enthusiastic Chilean software engineer in New Zeland. I mostly focus on the back-end of the systems. This is my site, Señor Developer, where I share my knowledge and experience.
Search for a command to run...

I'm an enthusiastic Chilean software engineer in New Zeland. I mostly focus on the back-end of the systems. This is my site, Señor Developer, where I share my knowledge and experience.
In this series, I will show you a set of Object Oriented Design Patterns which provides ways to solve common problems in software design.
Imagine you are a professional tennis player who travel around the world for important tournaments but you are not able to speak any language other than English... So, in every non-english country you need someone to helps you with the speech. You mi...
Quilla Frontend Kit turns recurring API, authentication, and server-state concerns into composable TypeScript packages.

Quilla Backend Kit provides the foundations for structured TypeScript services while leaving the application architecture in your hands.

Practical constraints for preserving the framework’s boundaries, dependency direction, and readability as a test suite grows.

A tool-agnostic approach to separating test intent from UI mechanics as an automation suite grows.

In this article, we will go through the entire process of set up Visual Studio Code for debugging “Deno” programs using “V8 Inspector Protocol”. Step-by-step, we will be learning how to configure VSCode to debug a Deno project. If you don’t know Deno...

In this article I will explain one Object Oriented Programing Pattern called the "Decorators Pattern" and how we can implement it using typescript to handle multiple-optional behaviors.
Lets imagine that we have an ice-cream shop which offers to our customers the following flavors and toppings:
| Flavor | Cost |
| Chocolate | 7 |
| Pistachio | 6 |
| Trululu | 5 |
| Toppings | |
| Crumbled Waffles | 4 |
| JellyBeans | 2 |
| Marshmallow | 3 |
| Nutella | 2 |
The customer can order a Chocolate ice Cream with any topping. For example: Chocolate ice cream with JellyBeans or Chocolate ice cream with Marshmallow, or Chocolate ice cream with CrumbledWaffles, or even worst Chocolate ice cream with Jellybeans, Marshmallow and CrumbledWaffles... and so on... all the posible combinations that the customer wants...
We can solve it using inheritance and try to represent every posible combinations with a subclass like this:

It is not maintainable and hard to make changes in the system. if the prices change or the shop needs to add more flavors and toppings, we will need to cover all the new posible combinations... (Imagine yourself diving in hundred of classes trying to understand the implementation details or making small changes...)
The pattern definition: "It attaches additional responsibilities to an object dynamically without altering their structure. Decorators provide a flexible alternative to subclassing for extending functionality"
With decorator pattern we can use extensions at runtime rather than at compile time using a form of object composition:

The decorator implement the same abstract class to the component that they are decorating. Also, decorators extend the state of the component and add new methods if it need it.
The concrete component is the object that we can add dynamically new behaviors.
Lets apply the decorator pattern to our ice cream shop:

The Component is represented by the IceCream class
The ConcreteComponents are represented by the ice cream flavors (Chocolate, Pistachio, Trululu)
And Decorators are represented by every topping (JellyBeans, Nutella, Marshmallow and Crumbled Waffles)
In the next section we will create the code that we need to get it implemented in typescript.
First run the npm init command to create our package json file:
npm init --yes # this will trigger automatically populated initialization with default values
Now, install the "typescript" and "ts-node" dependencies:
npm install --save-dev ts-node typescript
And create the following project structure:
.
├── main.ts
├── package-lock.json
├── package.json
└── src
├── components
└── decorators
Finally, create the "start" script in the package.json file:
"scripts": {
"start": "ts-node main.ts"
}
export abstract class IceCream {
protected description = "no ice cream description";
public getDescription(): string {
return this.description;
}
public abstract getCost(): number;
}
import { IceCream } from "./ice-cream";
export class Chocolate extends IceCream {
constructor() {
super();
this.description = "Chocolate ice cream";
}
public getCost(): number {
return 7;
}
}
import { IceCream } from "./ice-cream";
export class Pistachio extends IceCream {
constructor() {
super();
this.description = "Pistachio ice cream";
}
public getCost(): number {
return 5;
}
}
import { IceCream } from "./ice-cream";
export class Trululu extends IceCream {
constructor() {
super();
this.description = "Trululu ice cream";
}
public getCost(): number {
return 5;
}
}
import { IceCream } from "../components/ice-cream";
export abstract class TopppingDecorator extends IceCream {
constructor(protected iceCream: IceCream) {
super();
}
public abstract getDescription(): string;
}
import { Toppping } from "./topping";
export class CrumbledWaffles extends Toppping {
public getDescription(): string {
return this.iceCream.getDescription() + ", CrumbledWaffles";
}
public getCost(): number {
return this.iceCream.getCost() + 4;
}
}
import { Toppping } from "./topping";
export class JellyBeans extends Toppping {
public getDescription(): string {
return this.iceCream.getDescription() + ", JellyBeans";
}
public getCost(): number {
return this.iceCream.getCost() + 2;
}
}
import { Toppping } from "./topping";
export class Marshmallow extends Toppping {
public getDescription(): string {
return this.iceCream.getDescription() + ", Marshmallow";
}
public getCost(): number {
return this.iceCream.getCost() + 4;
}
}
import { Toppping } from "./topping";
export class Nutella extends Toppping {
public getDescription(): string {
return this.iceCream.getDescription() + ", Nutella";
}
public getCost(): number {
return this.iceCream.getCost() + 3;
}
}
//add this code to main.ts
import { Chocolate } from "./src/components/chocolate";
import { IceCream } from "./src/components/ice-cream";
import { Marshmallow } from "./src/decorators/marshmallow";
// making a chocolate ice cream
let chocolateIceCream: IceCream = new Chocolate();
// adding marshmallow
chocolateIceCream = new Marshmallow(chocolateIceCream);
// get your order
console.log("Your order:", chocolateIceCream.getDescription());
console.log("Cost: $", chocolateIceCream.getCost());
//Your order: Chocolate ice cream, Marshmallow
//Cost: $ 9
Execute "npm run start" to get the order details
//add this code to main.ts
import { IceCream } from "./src/components/ice-cream";
import { Pistachio } from "./src/components/pistachio";
import { JellyBeans } from "./src/decorators/jelly-beans";
import { Nutella } from "./src/decorators/nutella";
// making a Pistachio ice cream
let pistachioIceCream: IceCream = new Pistachio();
// adding jellybeans
pistachioIceCream = new JellyBeans(pistachioIceCream);
// adding nutella
pistachioIceCream = new Nutella(pistachioIceCream);
// get your order
console.log("Your order:", pistachioIceCream.getDescription());
console.log("Cost: $", pistachioIceCream.getCost());
//Your order: Pistachio ice cream, JellyBeans, Nutella
//Cost: $ 10
Execute "npm run start" to get the order details
//add this code to main.ts
import { Trululu } from "./src/components/trululu";
import { IceCream } from "./src/components/ice-cream";
import { CrumbledWaffles } from "./src/decorators/crumbled-waffles";
import { JellyBeans } from "./src/decorators/jelly-beans";
import { Marshmallow } from "./src/decorators/marshmallow";
// making a "trululu" ice cream
let trululuIceCream: IceCream = new Trululu();
// adding crumbledWaffles
trululuIceCream = new CrumbledWaffles(trululuIceCream);
// adding jellybeans
trululuIceCream = new JellyBeans(trululuIceCream);
// adding marshmallow
trululuIceCream = new Marshmallow(trululuIceCream);
// more marshmallow... the customer wants it with doble marshmallow
trululuIceCream = new Marshmallow(trululuIceCream);
// get your order
console.log("Your order:", trululuIceCream.getDescription());
console.log("Cost: $", trululuIceCream.getCost());
//Your order: Trululu ice cream, CrumbledWaffles, JellyBeans, Marshmallow, Marshmallow
//Cost: $ 14
Execute "npm run start" to get the order details
By wrapping the IceCream with Toppings you can achieve any combination that the customer wants or the ice cream shop needs.
Let's have a look at the topping decorator
export abstract class TopppingDecorator extends IceCream {
constructor(protected iceCream: IceCream) {
super();
}
public abstract getDescription(): string;
}
IceCream is a reference to the Component that each decorator will be wrapping.
getDescription(): every decorator has to reimplement this method to include the ice cream and topping descriptions.
Now, is time to understand the Marshmallow Topping:
export class Marshmallow extends Toppping {
public getDescription(): string {
return this.iceCream.getDescription() + ", Marshmallow";
}
public getCost(): number {
return this.iceCream.getCost() + 4;
}
}
getDescription(): Delegate to the decorated object to get its description and then append "Marshmallow" description to that description.
getCost(): Delegate to the decorated object to compute its cost and then add the cost of "Marshmallow" (+4) to the final cost.
// make a Trululu Object
let trululuIceCream: IceCream = new Trululu();
// wrap it with CrumbledWaffles
trululuIceCream = new CrumbledWaffles(trululuIceCream);
// wrap it with Jellybeans
trululuIceCream = new JellyBeans(trululuIceCream);
// wrap it with Marshmallow
trululuIceCream = new Marshmallow(trululuIceCream);
// get your order
console.log("Your order:", trululuIceCream.getDescription());
console.log("Cost: $", trululuIceCream.getCost());
We can visualize the wrap order of the getDescription and getCost method as an onion diagram:

By using this approach we apply one of the S.O.L.I.D principles: Open/Closed (OCP) which allows every decorator implement their own behavior without modifying the existent code.
You can have a look at oas-to-joi library which is a real example of how you can implement the decorator pattern in another context.
Also, here is the Github repository of the this project.
See you in the next article