codingInStyle Logo

Implementing the Strategy Pattern in Swift

Swift Design Patterns Programming

Introduction

In this tutorial, we'll explore how to implement the Strategy Pattern using Protocol-Oriented Programming (POP) in Swift. We'll use a simple example of calculating shipping costs using different strategies to demonstrate how the pattern works.

Requirements

This tutorial assumes you have a basic understanding of Swift programming and familiarity with concepts like protocols and structs. No prior knowledge of design patterns is required, as we'll explain the Strategy Pattern step by step.

The Strategy Pattern

The Strategy Pattern is a behavioral design pattern that enables selecting an algorithm's behavior at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable. This allows the algorithm to vary independently from clients that use it.

Implementation

We'll create an application that calculates shipping costs based on different shipping methods:

Our goal is to make the shipping cost calculation flexible, allowing us to add new shipping methods easily without modifying existing code.

1. Define the Strategy Protocol

We'll start by defining a protocol ShippingStrategy that declares a method for calculating the shipping cost:

protocol ShippingStrategy {
    func calculateShippingCost(for weight: Double) -> Double
}

2. Create Concrete Strategies

Next, we'll create concrete strategies that conform to the ShippingStrategy protocol.

Standard Shipping Strategy
struct StandardShippingStrategy: ShippingStrategy {
    func calculateShippingCost(for weight: Double) -> Double {
        return weight * 1.0 // $1 per kg
    }
}
Express Shipping Strategy
struct ExpressShippingStrategy: ShippingStrategy {
    func calculateShippingCost(for weight: Double) -> Double {
        return weight * 2.0 + 10.0 // $2 per kg plus $10 flat fee
    }
}
Free Shipping Strategy
struct FreeShippingStrategy: ShippingStrategy {
    func calculateShippingCost(for weight: Double) -> Double {
        return 0.0 // Free shipping
    }
}

3. Create the Context

We'll create a ShippingCostCalculator that uses a ShippingStrategy to calculate the shipping cost:

struct ShippingCostCalculator {
    var strategy: ShippingStrategy

    func calculateCost(for weight: Double) -> Double {
        return strategy.calculateShippingCost(for: weight)
    }
}

Usage Example

Let's see how we can use the ShippingCostCalculator with different strategies:

// Define the weight of the package
let packageWeight = 5.0 // in kilograms

// Use Standard Shipping
let standardStrategy = StandardShippingStrategy()
let standardCalculator = ShippingCostCalculator(strategy: standardStrategy)
let standardCost = standardCalculator.calculateCost(for: packageWeight)
print("Standard Shipping Cost: $\(standardCost)") // Output: $5.0

// Use Express Shipping
let expressStrategy = ExpressShippingStrategy()
let expressCalculator = ShippingCostCalculator(strategy: expressStrategy)
let expressCost = expressCalculator.calculateCost(for: packageWeight)
print("Express Shipping Cost: $\(expressCost)") // Output: $20.0

// Use Free Shipping
let freeStrategy = FreeShippingStrategy()
let freeCalculator = ShippingCostCalculator(strategy: freeStrategy)
let freeCost = freeCalculator.calculateCost(for: packageWeight)
print("Free Shipping Cost: $\(freeCost)") // Output: $0.0

Output

Standard Shipping Cost: $5.0
Express Shipping Cost: $20.0
Free Shipping Cost: $0.0

Explanation

By implementing the Strategy Pattern, we can switch between different shipping cost calculations easily. Each strategy conforms to the ShippingStrategy protocol and provides its own implementation of the calculateShippingCost method. The ShippingCostCalculator acts as the context and uses the selected strategy to compute the cost.

Benefits of the Strategy Pattern

Extending the Example

Suppose we want to add an International Shipping strategy that charges a base fee plus a rate per kilogram.

International Shipping Strategy

struct InternationalShippingStrategy: ShippingStrategy {
    func calculateShippingCost(for weight: Double) -> Double {
        let baseFee = 15.0
        let perKgRate = 3.0
        return baseFee + (weight * perKgRate)
    }
}

Usage:

let internationalStrategy = InternationalShippingStrategy()
let internationalCalculator = ShippingCostCalculator(strategy: internationalStrategy)
let internationalCost = internationalCalculator.calculateCost(for: packageWeight)
print("International Shipping Cost: $\(internationalCost)") // Output: $30.0

Conclusion

By using the Strategy Pattern, we can design our code to be flexible and maintainable. Protocol-Oriented Programming in Swift makes it easy to define strategies and switch between them. This pattern is not limited to shipping costs; it can be applied to various scenarios where behavior can change at runtime.

Further Exploration

You can experiment with the Strategy Pattern by creating more strategies or by applying it to different problems. For example, you could implement discount strategies for a shopping cart or different sorting algorithms for a list.

References