Menu
CoddyTech

Strategy Pattern

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

The Strategy pattern puts each version of an algorithm in its own object, so you can swap the algorithm while the program runs. The object that uses it, the context, calls one method and does not care which version it holds.

Here are two attack strategies, a parent class and a subclass with the same method damage():

# attack_style.gd
class_name AttackStyle
extends RefCounted

func damage(power: int) -> int:
	return power
# heavy_attack.gd
class_name HeavyAttack
extends AttackStyle

func damage(power: int) -> int:
	return power * 2

The context stores a strategy and hands the calculation to it:

# fighter.gd
class_name Fighter
extends RefCounted

var power := 10
var style: AttackStyle = AttackStyle.new()

func set_style(new_style: AttackStyle) -> void:
	style = new_style

func attack() -> void:
	print("Hit for ", style.damage(power))
# main.gd
var fighter := Fighter.new()
fighter.attack()
fighter.set_style(HeavyAttack.new())
fighter.attack()
Hit for 10
Hit for 20

Fighter never checks which style it holds. A new style is a new class, and fighter.gd stays as it is; an if style == "heavy" chain inside attack() would grow with every style.

When a strategy is one small function with no data of its own, a Callable is enough. The context stores the function and runs it with call():

# price_calculator.gd
class_name PriceCalculator
extends RefCounted

var discount: Callable = func(price): return price

func final_price(price: int) -> int:
	return discount.call(price)
# main.gd
var calc := PriceCalculator.new()
print(calc.final_price(80))
calc.discount = func(price): return price / 2
print(calc.final_price(80))
calc.discount = func(price): return max(price - 25, 0)
print(calc.final_price(80))
80
40
55

Use classes when a strategy has its own settings or several methods, and Callables for one short calculation. You have used this idea before: the comparison function you pass to sort_custom() is a sorting strategy.

challenge icon

Challenge

Medium

A shop cart can switch discount strategies at checkout. Discount in discount.gd is the base strategy: no discount. Write two more and the cart:

  • PercentOff (built with a percent): apply(total) returns total - total * percent / 100 (integer math), and label() returns 10% off for 10 percent.
  • FixedOff (built with an amount): apply(total) returns the total minus the amount, but never below 0, and label() returns 30 off for an amount of 30.
  • Cart: set_discount(new_discount) stores the strategy, and checkout() adds up the prices and returns Subtotal TOTAL, LABEL, pay PRICE, where PRICE is what the strategy's apply() returns.

The supplied code reads the prices on one line, how many checkouts follow, and one strategy per line: percent N, fixed N or none. For the prices 50 30 40 and the strategies percent 10, fixed 30, none the output is:

Subtotal 120, 10% off, pay 108
Subtotal 120, 30 off, pay 90
Subtotal 120, no discount, pay 120

Your code goes in cart.gd, percent_off.gd and fixed_off.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 cart := Cart.new()
	for price in OS.read_string_from_stdin().split(" "):
		cart.add_item(int(price))
	var count := int(OS.read_string_from_stdin())
	for i in count:
		var parts := OS.read_string_from_stdin().split(" ")
		if parts[0] == "percent":
			cart.set_discount(PercentOff.new(int(parts[1])))
		elif parts[0] == "fixed":
			cart.set_discount(FixedOff.new(int(parts[1])))
		else:
			cart.set_discount(Discount.new())
		print(cart.checkout())
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