Strategy Pattern in iOS

abdul ahad
5 min readMay 15, 2024

--

Photo by Maha Khairy on Unsplash

what is strategy pattern?

The Strategy Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable. This pattern lets the algorithm vary independently from clients that use it. In simpler terms, the Strategy Pattern is like having a set of tools (algorithms) that you can choose from to perform a task, and you can switch between these tools as needed without changing the code that uses them.

The strategy Pattern is particularly useful when you have multiple ways to perform a task, and you want to choose the implementation at runtime. For example, you might have different algorithms for sorting data, encrypting data, or fetching data from a network. By using the Strategy Pattern, you can easily switch between these algorithms without changing the code that uses them.

Example

Let’s assume you have a Person object with name and dateOfBirth properties, and you want to sort an array of Person objects based on either their name or date of birth.


struct Person {
let name: String
let dateOfBirth: Date
}

//Sorting Strategy Interface
protocol SortingStrategy {
func sort(array: [Person]) -> [Person]
}

// Concrete Sorting Strategy implementation used for sorting by Name
class NameSortStrategy: SortingStrategy {
func sort(array: [Person]) -> [Person] {
return array.sorted { $0.name < $1.name }
}
}

// Concrete Sorting Strategy implementation used for sorting by DOB
class DateOfBirthSortStrategy: SortingStrategy {
func sort(array: [Person]) -> [Person] {
return array.sorted { $0.dateOfBirth < $1.dateOfBirth }
}
}

// This class uses the sorting strategy interface to perform sorting
class PersonSorter {
private var strategy: SortingStrategy

init(strategy: SortingStrategy) {
self.strategy = strategy
}

func sort(array: [Person]) -> [Person] {
return strategy.sort(array: array)
}

func setStrategy(strategy: SortingStrategy) {
self.strategy = strategy
}
}

let persons = [
Person(name: "abdul ahad", dateOfBirth: Date()),
Person(name: "fahad", dateOfBirth: Date().addingTimeInterval(60)),
Person(name: "usman", dateOfBirth: Date().addingTimeInterval(30))
]

// Sort by name
let personSorterOld = PersonSorter(strategy: NameSortStrategy())
let sortedByName = personSorter.sort(array: persons)
print(sortedByName)
// Output: Sorted by name

// Sort by date of birth
let personSorterNew = PersonSorter(strategy: DateOfBirthSortStrategy())
let sortedByDateOfBirth = dobSorter.sort(array: persons)
print(sortedByDateOfBirth)
// Output: Sorted by date of birth


// we can switch strategies during runtime using method injection

personSorterOld.setStrategy(strategy:DateOfBirthSortStrategy())
let sortedByDateOfBirthNew = personSorterOld.sort(array: persons)
print(sortedByDateOfBirthNew)
// Output: Sorted by date of birth

This code defines a Person struct with name and dateOfBirth properties, and implements the Strategy Pattern to sort an array of Person objects by either their name or date of birth. The SortingStrategy protocol declares a sorting method, and two classes (NameSortStrategy and DateOfBirthSortStrategy) implement this protocol to provide specific sorting algorithms. The PersonSorter class uses a SortingStrategy to sort Person objects, allowing the sorting strategy to be changed at runtime.

By applying the Strategy Pattern, you’ve created a flexible and extensible system for sorting Person objects. You can easily add new sorting strategies without changing the existing code that uses the SortingStrategy protocol. This approach promotes code reusability and makes your code more maintainable and easier to extend with new sorting algorithms in the future.

Real-World Example in iOS

let's look at a real-world example where we use the strategy pattern to load data either remotely or locally using async await

import Foundation
// Protocol defining the loader strategy
public protocol LoaderStrategy {
func load() async -> [String]
}

// Concrete strategy for loading data from a remote API
public class RemoteLoaderStrategy:LoaderStrategy{
public init(){}
public func load() async -> [String]{
//api
try? await Task.sleep(nanoseconds: 1_000_000_000)
let values = ["remoteData1","remoteData2"]
let date = Date().addingTimeInterval(60)
return values
}
}

// Concrete strategy for loading data from a local cache
public class LocalLoaderStrategy:LoaderStrategy{
public init(){}
public func load() async -> [String] {
//cache work
try? await Task.sleep(nanoseconds: 5)
let values = ["localData1","localData2"]
return values

}
}
// viewmodel that uses the strategy dependency but
// is agnositc of the provinence of data
public class StrategyViewModel{
let loader:LoaderStrategy
public var data = [String]()

public init(loader: LoaderStrategy) {
self.loader = loader
}

public func loadData() async {
let values = await loader.load()
self.data = values
}
}

LoaderStrategy to define the interface for different loading strategies. It declares a single method load that returns data asynchronously.

RemoteLoaderStrategy conforms to the LoaderStrategy protocol and represents a strategy for loading data from a remote API. It implements the load method asynchronously, simulating data retrieval from a remote server.

LocalLoaderStrategy similarly to RemoteLoaderStrategy, conforms to the LoaderStrategy protocol and represents a strategy for loading data from a local cache. It implements the load method asynchronously, simulating data retrieval from a local cache.

StrategyViewModel this class acts as a context for using the loader strategies. It holds a reference to an LoaderStrategy instance. The data property stores the loaded data. The loadData method is responsible for initiating the data-loading process

This is how the diagram would look like StrategyViewModelis agnostic of where the data comes from cause it is dependent on an interface called LoaderStrategywhich means we can replace it with any implementation in runtime so we can easily switch between the data sources. This also makes it testable cause we can also replace LoaderStrategywith a test double.

Testing the viewModel

This class is designed to test the behavior of StrategyViewModel under different loading strategies, specifically a remote loading strategy and a local loading strategy.

import Foundation
import DesignPatterns
import XCTest

class StrategyTests: XCTestCase {
func test_ViewModel_RemoteStrategy() async {
let (remoteStrategy,_) = makeSUT()

let viewModel = StrategyViewModel(loader: remoteStrategy)

await viewModel.loadData()

XCTAssertEqual(viewModel.data,["remoteData1", "remoteData2"])

}

func test_ViewModel_localStrategy() async {
let (_,localStrategy) = makeSUT()

let viewModel = StrategyViewModel(loader: localStrategy)

await viewModel.loadData()

XCTAssertEqual(viewModel.data,["localData1", "localData2"])


}

// MARK: - Helpers

private func makeSUT(file: StaticString = #filePath, line: UInt = #line) -> (LoaderStrategy,LocalLoaderStrategy){

let remoteStrategy = RemoteLoaderStrategy()
trackForMemoryLeaks(remoteStrategy,file: file,line: line)

let localStrategy = LocalLoaderStrategy()
trackForMemoryLeaks(localStrategy,file: file,line: line)

return (remoteStrategy,localStrategy)
}

}

Conclusion

Overall, the Strategy pattern offers invaluable advantages. It enables dynamic algorithm selection at runtime, allowing apps to quickly adapt to evolving requirements without extensive code modifications. Strategies encapsulate specific functionalities, fostering effortless reuse across different parts of an application, thus enhancing development efficiency and minimizing redundancy. By promoting clear separation between the Context and the Strategies, the pattern facilitates code comprehension and reduces the proliferation of complex conditional statements, ultimately improving code maintainability. Moreover, the Strategy pattern enhances testability by encapsulating behaviors, enabling more comprehensive and efficient testing processes, which leads to higher overall code quality and reliability in iOS applications.

Reference

Github: https://github.com/abdahad1996/DesignPatterns_Bootcamp

--

--

abdul ahad
abdul ahad

Written by abdul ahad

A software developer dreaming to reach the top and also passionate about sports and language learning

No responses yet