Strategy Design Pattern in Swift


Greetings, traveler!

We continue to talk about Design Patterns. Next up is a behavioral design pattern, which is the Strategy. Strategy is a design pattern that defines a set of related algorithms and encapsulates them into their own objects. This allows the algorithms to be easily swapped out, making it easier to customize the app’s behavior.

Example of usage

There will be no delay, and we will analyze the example immediately. Imagine we have an audio streaming service that offers both free and premium options. The free version provides lower-quality audio, while the premium version offers a higher-quality listening experience. Users can choose between these two options based on their preferences and budget. If users want to enjoy premium audio, they can subscribe to the premium plan and enjoy the benefits of higher sound quality.

Let’s create an object — our User. It will have the isPremium property. 

struct User {
    var isPremium: Bool
}

Now, let’s create a class that will handle audio streaming. To start, we will implement it without using the Strategy Design Pattern.

final class RadioClient {
    
    private let user: User
    
    init(user: User) {
        self.user = user
    }
    
    func streamAudio() {
        if user.isPremium {
            print("Streaming audio in high quality")
        } else {
            print("Streaming audio in low quality")
        }
    }
    
}

As we can see, the single responsibility principle is violated in this case. The class responsible for audio streaming must also determine whether a user is a premium subscriber and, depending on this information, broadcast audio of the appropriate quality to them.

Let’s see how we can improve our audio streaming class using the Strategy design pattern. First, we’ll define a StreamingStrategy protocol with a single function for streaming audio. This protocol will serve as the blueprint for our different audio streaming strategies.

protocol StreamingStrategy {
    func streamAudio()
}

Then, let’s create two implementations of this protocol. One will be responsible for streaming audio at a lower quality, and the other will allow our premium users to enjoy the best possible quality of music.

final class DefaultStrategy: StreamingStrategy {
    func streamAudio() {
        print("Streaming audio in high quality")
    }
}

final class PremiumStrategy: StreamingStrategy {
    func streamAudio() {
        print("Streaming audio in low quality")
    }
}

The next step is to create a strategy property inside our audio streaming class. We will inject the value for this property during initialization. And now, we can simply rely on our strategy instead of trying to fit all the logic into a single function.

final class RadioClient {
    
    private let strategy: StreamingStrategy
    
    init(strategy: StreamingStrategy) {
        self.strategy = strategy
    }
    
    func streamAudio() {
        strategy.streamAudio()
    }
    
}

Ready! We can use it.

let strategy = PremiumStrategy()
let radio = RadioClient(strategy: strategy)

radio.streamAudio()

Conclusion

We have just learned about a handy tool for encapsulating behavioral logic. The Strategy Design Pattern fits nicely into the principles of protocol-driven programming. Moreover, it allows you to change the type of algorithm behavior at runtime. This is a very efficient and elegant solution.

This concludes the discussion of this design pattern. In the following article, we will discuss the Template Method pattern.