Software Design

Swift Strategy Pattern: Swapping Algorithms with Protocols and Closures

Have you ever dealt with code where if-else branches kept multiplying for each payment method?

4 min read
Cover image for Swift Strategy Pattern: Swapping Algorithms with Protocols and Closures

Have you ever dealt with code where if-else branches kept multiplying for each payment method?

Once sorting options, discount calculations, and filtering logic become tangled in one massive switch statement, touching the code gets increasingly intimidating.

That is when Swift’s Strategy Pattern comes in handy.

Today, we will explore from a practical perspective how to swap algorithms like components using protocols and closures.

Let’s start with the key point.

The Strategy Pattern separates what to do from how to do it, allowing algorithms to be swapped externally.

In Swift, you can implement this in two ways: protocols, the heavier approach, or closures, the lighter approach.

Let’s look at each one below.


What Is the Strategy Pattern? A Three-Line Summary

Let’s establish the structure before getting into complicated theory.

  1. Group the algorithms to be swapped under one common specification, a protocol
  2. Implement the actual behavior as separate strategy objects or closures
  3. The caller only needs to know the specification, not the concrete details

Think of it as switching weapons for a game character.

The character only needs to know how to attack; whether it is a sword, bow, or magic, the equipped weapon handles the details.

You change only the weapon without touching the character code.

This swapping is essentially the entire Strategy Pattern.


Implementing It with a Protocol (The Conventional Approach)

Let’s start with the most textbook protocol-based approach.

Let’s use discount calculation as an example. There are several calculation methods, such as regular members, VIP members, and coupon application.

The code below defines the common specification every strategy must follow and one concrete strategy.

// Common specification that every discount strategy must follow
protocol DiscountStrategy {
    func discount(for price: Int) -> Int
}

// VIP Strategy: 20% discount
struct VIPDiscount: DiscountStrategy {
    func discount(for price: Int) -> Int { price * 20 / 100 }
}

Now let’s look at the code that holds and uses this strategy.

The client object does not care which strategy it receives.

Checkout knows only the specification, not the implementation
Checkout knows only the specification, not the implementation
struct Checkout {
    var strategy: DiscountStrategy   // Place where the strategy can be swapped

    func finalPrice(_ price: Int) -> Int {
        price - strategy.discount(for: price)
    }
}

// Swap only the strategy at payment time
let cart = Checkout(strategy: VIPDiscount())

Even when a new discount policy is introduced, you do not need to touch Checkout.

You only need to create one new structure conforming to DiscountStrategy.

Extending functionality without touching existing code—that is the biggest benefit you feel in practice.


Implementing It with Closures (The Swift Way)

But creating a new structure every time can feel cumbersome when the strategy itself is simple.

Wrapping every small piece of logic in a protocol can actually make the code verbose.

In cases like this, closures are much lighter and more natural in Swift.

struct Checkout {
    // Receive the strategy itself as a function
    var discount: (Int) -> Int

    func finalPrice(_ price: Int) -> Int {
        price - discount(price)
    }
}

// Define the strategy inline
let cart = Checkout(discount: { $0 * 20 / 100 })

You can write the logic directly when passing it, without declaring a separate type.

This approach is much cleaner for swapping lightweight algorithms such as sort criteria, filter conditions, and simple transformations.

Even sorted(by:) turns out to be a Strategy Pattern we use every day
Even sorted(by:) turns out to be a Strategy Pattern we use every day

In fact, the standard library’s sorted(by:) is this very closure-based Strategy Pattern.

The sorting structure stays fixed while the comparison criterion is swapped in as a closure.

So we have already been using it every day.


Protocols and Closures: When Should You Use Which?

Here is a table summarizing the criteria I use in practice.

Category Protocol approach Closure approach
Suitable situations Complex strategy or holds state Short and simple strategy
Reusability Easy to reuse in multiple places Good for one-off use
Code size Requires type declarations; larger Defined inline; smaller
Testing Easy to verify as an individual type Simply verify the logic
Readability Intent is clear from the name Clear when short, obscure when long

Here is my rule of thumb.

Use a protocol when the strategy has internal state, is reused across screens, or needs its intent to be clear by name.

If you are swapping one- or two-line logic on the fly, a closure is the right choice.

There is no problem mixing both. I use protocols for broad policies and closures for detailed options.


Frequently Asked Questions (Q&A)

Q. How is the Strategy Pattern different from inheritance (overriding)?

Inheritance is tied to a parent class, so behavior is fixed at compile time.

The Strategy Pattern, on the other hand, lets you freely swap strategies at runtime without being bound to a class hierarchy.

Q. How is it different from handling cases with an enum’s switch statement?

With switch, you must open and modify existing code whenever a new case is added.

With the Strategy Pattern, you only need to add a new strategy, reducing the need to touch existing code.


The essence of the Strategy Pattern is simple: move changing parts outside so they are easy to swap.

Use the protocols and closures you learned today according to the situation, and you can surely escape that exhausting if-else hell. Try refactoring a small piece of code first!