Skip to Content
Course content

167: The Strategy Pattern with Protocols

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why can't I just use a switch statement to handle different behaviors?

You absolutely can, and for a tiny project, you probably should. But here's the problem: as your app grows, that switch statement becomes a magnet for every single change in your business logic. If you're handling payment methods—say, Credit Card, PayPal, and Apple Pay—every time the marketing team wants to add "Klarna" or "Crypto," you have to go back into your core checkout logic and add another case.

That's a violation of the Open/Closed Principle (entities should be open for extension, but closed for modification). I've spent way too many hours debugging "regression bugs" where adding a new case to a massive switch statement accidentally broke an existing one because they shared some weird local state. The Strategy Pattern lets you pull those behaviors out into their own isolated objects. You stop asking "What type is this?" and start saying "I don't care what you are, just perform your version of this action."

How do I actually implement this using protocols?

In Swift, the protocol is your "Strategy" interface. It defines the contract that every specific algorithm must follow. Let's look at a payment example. First, we define what it means to be a payment strategy, then we create the concrete implementations.

protocol PaymentStrategy {
    func collectPayment(amount: Double)
}

struct CreditCardPayment: PaymentStrategy {
    let cardNumber: String
    
    func collectPayment(amount: Double) {
        print("Charging $\(amount) to card \(cardNumber)...")
    }
}

struct PayPalPayment: PaymentStrategy {
    let email: String
    
    func collectPayment(amount: Double) {
        print("Redirecting to PayPal for \(email) to pay $\(amount)...")
    }
}

Now, you need a "Context" object. This is the class that actually uses the strategy. The key here is that the CheckoutManager doesn't know which payment method it's using; it only knows that whatever it has conforms to PaymentStrategy.

class CheckoutManager {
    private var strategy: PaymentStrategy
    
    init(strategy: PaymentStrategy) {
        self.strategy = strategy
    }
    
    func setStrategy(_ strategy: PaymentStrategy) {
        self.strategy = strategy
    }
    
    func executePayment(amount: Double) {
        strategy.collectPayment(amount: amount)
    }
}

Can I swap these strategies while the app is running?

Yes, and that's where this pattern really shines. Because the CheckoutManager holds the strategy as a property rather than hard-coding the logic, you can swap it on the fly based on user input.

Imagine a user is on the checkout screen. They start with Credit Card selected, but then realize they have a PayPal balance. You don't need to destroy your checkout object and recreate it; you just inject a different strategy.

// Start with Credit Card
let cardPayment = CreditCardPayment(cardNumber: "1234-5678-9012")
let checkout = CheckoutManager(strategy: cardPayment)
checkout.executePayment(amount: 49.99) 

// User changes their mind to PayPal
let paypalPayment = PayPalPayment(email: "dev@example.com")
checkout.setStrategy(paypalPayment)
checkout.executePayment(amount: 49.99)

I personally love this approach because it makes unit testing trivial. You can create a MockPaymentStrategy that doesn't actually hit a network API, inject it into your manager, and test your checkout flow without spending real money or relying on a flaky sandbox server.




📋 Practical Task

Build a Dynamic Discount Engine

You are building a shopping cart for an e-commerce app. The app needs to apply different discount strategies based on the customer's status: VIP (20% off), Seasonal (10% off), and NoDiscount (0% off).

  • Create a protocol named DiscountStrategy with a method applyDiscount(to amount: Double) -> Double.
  • Implement three structs conforming to this protocol: VIPDiscount, SeasonalDiscount, and NoDiscount.
  • Create a ShoppingCart class that maintains a DiscountStrategy property.
  • Add a method to ShoppingCart called calculateTotal(originalPrice: Double) that uses the current strategy to return the final price.
  • In your code, instantiate a ShoppingCart with NoDiscount, calculate a total, then switch the strategy to VIPDiscount and calculate the total again for the same price.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.