Menu
CoddyTech

Decorator Pattern

Part of the Object Oriented Programming section of Coddy's GDScript journey. Lesson 52 of 65.

The Decorator pattern adds behavior to an object by wrapping it in another object with the same methods. Each wrapper calls the object inside it and adds its own part, and wrappers can be stacked in any order.

# weapon.gd
class_name Weapon
extends RefCounted

func get_damage() -> int:
	return 10

func describe() -> String:
	return "Sword"

A decorator extends Weapon, so it can go wherever a weapon is expected, and it holds the weapon it wraps:

# fire_enchant.gd
class_name FireEnchant
extends Weapon

var inner: Weapon

func _init(wrapped: Weapon) -> void:
	inner = wrapped

func get_damage() -> int:
	return inner.get_damage() + 5

func describe() -> String:
	return inner.describe() + " + Fire"

SharpEnchant is written the same way, but its get_damage() returns inner.get_damage() * 2 and its describe() adds " + Sharp".

# main.gd
var weapon: Weapon = Weapon.new()
print(weapon.describe(), ": ", weapon.get_damage())
weapon = FireEnchant.new(weapon)
weapon = SharpEnchant.new(weapon)
print(weapon.describe(), ": ", weapon.get_damage())
Sword: 10
Sword + Fire + Sharp: 30

The calls travel inward: SharpEnchant asks FireEnchant, which asks the sword. The sword returns 10, fire makes it 15, and sharp doubles it to 30. In the other order, FireEnchant.new(SharpEnchant.new(Weapon.new())), the damage is 10 * 2 + 5, which is 25.

Without decorators, every combination would need its own subclass: FireSword, SharpSword, FireSharpSword and so on. With them, two small classes cover every combination, and a new enchantment is one more file.

challenge icon

Challenge

Medium

A coffee shop builds drinks with decorators. Beverage (beverage.gd) declares cost() and description(); Espresso (espresso.gd, $3) and Tea (tea.gd, $2) are written. Each decorator stores the drink it wraps in inner (the constructors are written). Override cost() and description() in:

  • Milk: costs 1 more and adds + Milk to the description.
  • Mocha: costs 2 more and adds + Mocha.
  • Large: doubles the cost of everything inside it and puts Large in front of the description.

The supplied code reads how many orders follow, then one order per line: the drink (espresso or tea) and its extras, applied from left to right. For espresso milk large and tea mocha milk the output is:

Large Espresso + Milk: $8
Tea + Mocha + Milk: $5

Your code goes in milk.gd, mocha.gd and large.gd. main.gd holds the supplied input/output code and cannot be edited.

Try it yourself

extends Node

func _ready():
	# Supplied input/output code: keep it as it is
	var count := int(OS.read_string_from_stdin())
	for i in count:
		var words := OS.read_string_from_stdin().split(" ")
		var drink: Beverage = Tea.new()
		if words[0] == "espresso":
			drink = Espresso.new()
		for extra in words.slice(1):
			match extra:
				"milk":
					drink = Milk.new(drink)
				"mocha":
					drink = Mocha.new(drink)
				"large":
					drink = Large.new(drink)
		print("%s: $%d" % [drink.description(), drink.cost()])
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Online GDScript compiler