Mastering the Strategy Pattern for Flexible Software Design in App Development

Unleashing Flexibility and Scalability

Shubham
8 min readJun 11, 2023

In the fast-paced world of mobile app development, where user preferences and business requirements evolve rapidly, finding the right balance between flexibility and scalability is crucial. Enter the Strategy pattern — a game-changing design approach that empowers app developers to build highly customisable and adaptable solutions. Join us as we embark on a comprehensive exploration of the Strategy pattern, unraveling its potential, and discovering real-world implementations tailored specifically for mobile app development.

Not only will we explore when and how to effectively use the Strategy pattern, but we will also shed light on scenarios where it may not be the most suitable choice.

What is in the name ?

The Strategy design pattern is a behavioural design pattern that allows you to define a family of algorithms, encapsulate each one as a separate class, and make them interchangeable at runtime. It enables the algorithm to vary independently from the clients that use it.

The common use case for the Strategy design pattern in mobile app development is when you have a set of related algorithms or behaviours that can be used interchangeably based on different conditions or requirements. By encapsulating each algorithm in a separate class and using a common interface, you can easily switch between different strategies at runtime.

Real World example

To illustrate these concepts, let’s dive into a real-world example where a client needs to support multiple payment methods and have the flexibility to switch between them at runtime. This practical scenario will help us grasp the power and applicability of the Strategy pattern in a tangible way. By exploring how the Strategy pattern enables seamless integration of various payment methods and dynamic behavior customization, we can gain a deeper understanding of its benefits and practical implementation in mobile app development

Here’s an example of how you can implement the Strategy design pattern in Swift:

// Define a protocol for the strategy
protocol PaymentStrategy {
func pay(amount: Double)
}

// Implement concrete strategies conforming to the protocol
class CreditCardStrategy: PaymentStrategy {
func pay(amount: Double) {
print("Paying $\(amount) with credit card.")
// Additional credit card payment logic here
}
}

class PayPalStrategy: PaymentStrategy {
func pay(amount: Double) {
print("Paying $\(amount) with PayPal.")
// Additional PayPal payment logic here
}
}

// Create a context class that uses the strategy
class PaymentContext {
private var paymentStrategy: PaymentStrategy

init(strategy: PaymentStrategy) {
self.paymentStrategy = strategy
}

func setStrategy(strategy: PaymentStrategy) {
self.paymentStrategy = strategy
}

func makePayment(amount: Double) {
paymentStrategy.pay(amount: amount)
}
}

// Usage example
let paymentContext = PaymentContext(strategy: CreditCardStrategy())
paymentContext.makePayment(amount: 100.0)

paymentContext.setStrategy(strategy: PayPalStrategy())
paymentContext.makePayment(amount: 50.0)

And here’s an example of how you can implement the Strategy design pattern in Kotlin:

// Define an interface for the strategy
interface PaymentStrategy {
fun pay(amount: Double)
}

// Implement concrete strategies implementing the interface
class CreditCardStrategy : PaymentStrategy {
override fun pay(amount: Double) {
println("Paying $$amount with credit card.")
// Additional credit card payment logic here
}
}

class PayPalStrategy : PaymentStrategy {
override fun pay(amount: Double) {
println("Paying $$amount with PayPal.")
// Additional PayPal payment logic here
}
}

// Create a context class that uses the strategy
class PaymentContext(private var paymentStrategy: PaymentStrategy) {
fun setStrategy(strategy: PaymentStrategy) {
paymentStrategy = strategy
}

fun makePayment(amount: Double) {
paymentStrategy.pay(amount)
}
}

// Usage example
val paymentContext = PaymentContext(CreditCardStrategy())
paymentContext.makePayment(100.0)

paymentContext.setStrategy(PayPalStrategy())
paymentContext.makePayment(50.0)

In these examples, the PaymentStrategy protocol or interface defines the common contract for different payment strategies. The concrete strategies (CreditCardStrategy and PayPalStrategy) implement the payment logic specific to each strategy. The PaymentContext class serves as the context in which the strategy is used and provides methods to set the strategy and make payments using the chosen strategy.

By using the Strategy design pattern, you can easily add new payment strategies in the future without modifying the existing code.

At this point, you might be wondering, ‘Couldn’t we achieve the same behavior using if-else or switch-case statements? What sets the Strategy pattern apart?’ The answer lies in the myriad benefits this pattern offers. By adopting the Strategy pattern, we unlock a multitude of advantages that extend beyond simple conditional statements. We gain improved code organization, enhanced maintainability, and the ability to introduce new behaviors without modifying existing code. Moreover, the Strategy pattern empowers us with runtime flexibility, allowing us to dynamically switch between strategies as per changing requirements.

Advantages:

The Strategy design pattern offers several advantages over using switch-case or if-else statements for handling different behaviors or algorithms:

  1. Separation of concerns: The Strategy pattern promotes separation of concerns by encapsulating each algorithm or behavior in its own class. This keeps the code modular and makes it easier to maintain and extend. With switch-case or if-else statements, all the different behaviors are typically implemented in a single method or block, leading to a larger and more complex code structure.
  2. Open for extension, closed for modification: The Strategy pattern follows the principle of open-closed design, which means that you can introduce new strategies without modifying the existing code. This is achieved by relying on polymorphism and interfaces/protocols, allowing you to add new strategies by simply implementing the common interface. In contrast, switch-case or if-else statements often require modifying the existing code to accommodate new behaviors, which can lead to code duplication and fragility.
  3. Readability and maintainability: The Strategy pattern improves the readability and maintainability of the code. Each strategy is represented by its own class, making the code more self-explanatory and easier to understand. Adding or modifying strategies doesn’t affect other parts of the code, reducing the risk of introducing bugs or unintended side effects.
  4. Runtime flexibility: With the Strategy pattern, you can easily switch between different strategies at runtime. This flexibility allows you to dynamically select the appropriate strategy based on certain conditions or user preferences. In contrast, switch-case or if-else statements are typically resolved at compile-time, and changing the behavior requires modifying and recompiling the code.

Overall, the Strategy design pattern provides a more flexible, maintainable, and extensible solution for handling different algorithms or behaviours compared to switch-case or if-else statements. It promotes good design principles and helps to keep the codebase clean, modular, and easier to work with in the long run.

Don’t Over-Engineer, Ever

While the Strategy design pattern can be beneficial in many scenarios, there are situations where it may not be the most suitable approach. Here are a few cases where you might consider alternative solutions:

  1. Simple or limited behavior: If you have a small number of fixed behaviors that are unlikely to change or expand in the future, using the Strategy pattern could introduce unnecessary complexity. In such cases, using switch-case or if-else statements might be more straightforward and easier to understand.
  2. Performance-sensitive scenarios: The Strategy pattern involves encapsulating behaviors in separate classes, which can introduce a slight performance overhead due to the additional indirection and method invocations. In performance-critical situations, where every microsecond counts, using a more direct approach like switch-case or if-else statements might be preferable.
  3. Overuse of patterns: Applying design patterns excessively without a clear justification can lead to over-engineering and unnecessary complexity in your codebase. If the use of the Strategy pattern does not provide clear benefits or does not align with the overall design and requirements of your application, it’s best to avoid it.
  4. Simple conditional logic: If you have a small number of conditions and the behavior can be easily determined by a simple if-else statement or a switch-case block, introducing the Strategy pattern might introduce unnecessary complexity and additional classes.
  5. Framework limitations: In some cases, the framework or libraries you are working with might already provide a built-in mechanism or pattern that addresses the same problem. In such situations, it’s recommended to follow the established patterns and practices of the framework rather than introducing the Strategy pattern separately.

Here are a few examples where the Strategy pattern might not be the most appropriate choice:

  1. Boolean Conditions: If you have a simple boolean condition that determines the behavior, using the Strategy pattern might introduce unnecessary complexity. In such cases, using a simple if-else statement or a switch-case block is often more straightforward and readable.
// Example without Strategy pattern
if condition {
// Do something
} else {
// Do something else
}

2. Single Behavior: When you have only one behavior or algorithm that doesn’t require flexibility or interchangeability, using the Strategy pattern is overkill. In such cases, it’s more practical to directly implement the behavior without introducing additional classes or indirection.

// Example without Strategy pattern
public void performAction() {
// Single behavior implementation
}

3. Framework Patterns: If you are working within a framework or library that already provides a well-established pattern or mechanism to handle specific behaviors or algorithms, it’s generally recommended to follow the established conventions rather than introducing the Strategy pattern separately.

// Example using framework pattern (Android View.OnClickListener)
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Handle button click
}
});

4. Static Behaviors: If the behaviors or algorithms are static and known at compile-time, using the Strategy pattern might introduce unnecessary complexity. In such cases, it’s more practical to directly invoke the appropriate behavior or algorithm based on the compile-time knowledge.

# Example without Strategy pattern
def processInput(input):
if input == "A":
# Handle case A
elif input == "B":
# Handle case B
else:
# Handle other cases

Remember, the suitability of the Strategy pattern depends on the specific requirements and complexity of your application. It’s important to evaluate whether the pattern brings benefits in terms of flexibility, maintainability, and extensibility before deciding to apply it.

As we conclude our deep dive into the Strategy pattern, we have only scratched the surface of design patterns’ vast world. In the next parts of this series, we will continue our quest by unraveling the Decorator and Observer patterns, expanding our arsenal of tools to create even more elegant and robust software designs. Stay tuned for the next chapter, where we unravel the secrets of the Decorator pattern and explore its role in enhancing code modularity and extensibility.

If you found this content valuable, I invite you to follow me for more insights, tips, and updates. Stay connected and be among the first to access new content. Let’s embark on this knowledge journey together. Click the follow button and stay informed!

--

--

Shubham

I speak for the new world. Seasoned Software Engineering Expert | Guiding Developers Towards Excellence | 12 Years of Industry Insight