KapreSoft
From Coding to Cooking to Coins:
Antonio Lagnada's Blog on Software, Music, Food, and Finance

When to Choose Strategy Pattern Over Polymorphism

Post Date: 27 Oct 2023
 
 

Overview

Navigating through the intricate landscape of software design, one often encounters the pivotal decision of whether to employ the strategy pattern or leverage polymorphism. As cornerstone concepts of object-oriented programming (OOP), each brings its unique advantages and potential pitfalls. This comprehensive guide is thoughtfully curated to elucidate the situations where the strategy pattern holds the upper hand over polymorphism, all the while maintaining a steadfast adherence to esteemed design principles.

Embed from Getty Images

Strategy Pattern in a Nutshell

The strategy pattern, a prestigious design pattern within the OOP domain, empowers the selection of an algorithm’s behavior dynamically at runtime. Rather than cementing commitment to a single algorithm, a diverse array of algorithms is meticulously carved out, with each residing in its distinct class. These classes are designed to be interchangeable, thereby bestowing upon the context the liberty to oscillate between them as necessitated by the situation at hand. This judicious encapsulation of behavior not only amplifies code re-usability but also _ flexibility and testability.

public interface PaymentStrategy {
    void pay(int amount);
}

public class CreditCardPayment implements PaymentStrategy {
    private String name;
    private String cardNumber;

    public CreditCardPayment(String name, String cardNumber) {
        this.name = name;
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(int amount) {
        // Implementation for credit card payment
    }
}

public class PaypalPayment implements PaymentStrategy {
    private String emailId;

    public PaypalPayment(String emailId) {
        this.emailId = emailId;
    }

    @Override
    public void pay(int amount) {
        // Implementation for PayPal payment
    }
}

Polymorphism at a Glance

At the other end of the spectrum, polymorphism stands tall as another crucial concept in OOP. It extends the privilege to distinct classes to interpret a common method in their unique, bespoke manner. This is seamlessly actualized through inheritance, wherein a superclass lays down a method, subsequently allowing its progeny, the subclasses, to furnish specific implementations as they see fit.

Figure 1. Design Diagram of an Animal Polymorphism

Design Diagram of an Animal Polymorphism

Also available in: SVG | PlantText

This diagram visually represents the inheritance relationship where Dog and Cat classes extend the Animal class, with each of the three classes containing a sound method.

Here is a java code that represents this design:

public class Animal {
    public void sound() {
        System.out.println("Animal makes a sound");
    }
}

public class Dog extends Animal {
    public void sound() {
        System.out.println("Dog barks");
    }
}

public class Cat extends Animal {
    public void sound() {
        System.out.println("Cat meows");
    }
}

When to Choose Strategy Pattern

Opting for the strategy pattern proves itself invaluable when confronted with a myriad of algorithms or behaviors pertaining to a particular task, each demanding the elasticity to be supplanted interchangeably at runtime. This methodology aligns seamlessly with the design principles advocating for program to interface, not implementation, while also championing the cause of composition over inheritance. The thoughtful encapsulation of behavior not only propels code re-usability but also ensures the maintenance of pristine, unblemished code.

Redesigning the Animal Hierarchy using the Strategy Pattern

Here’s a redesign of your classes using the strategy pattern:

Figure 2. Design Diagram of an Animal Sound Strategy

Design Diagram of an Animal Sound Strategy

Also available in: SVG | PlantText

The diagram shows that DogSound, CatSound, and DefaultSound all implement the SoundStrategy interface and that the Animal class uses the SoundStrategy interface.

In this refactored design, the separate Dog and Cat classes are no longer necessary. Their distinct behaviors are encapsulated within their respective strategy classes, DogSound and CatSound, both of which implement the SoundStrategy interface. This approach aligns with the strategy pattern, allowing for flexible and interchangeable behavior at runtime. By encapsulating the sound-making behavior within these strategy classes, the design adheres to the principle of composition over inheritance, reducing complexity and enhancing maintainability.

Here is a java code that represents this design:

public interface SoundStrategy {
    void makeSound();
}

public class Animal {
    private SoundStrategy soundStrategy;

    public Animal(SoundStrategy soundStrategy) {
        this.soundStrategy = soundStrategy;
    }

    public void setSoundStrategy(SoundStrategy soundStrategy) {
        this.soundStrategy = soundStrategy;
    }

    public void performSound() {
        soundStrategy.makeSound();
    }
}

public class DogSound implements SoundStrategy {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}

public class CatSound implements SoundStrategy {
    @Override
    public void makeSound() {
        System.out.println("Cat meows");
    }
}

public class DefaultSound implements SoundStrategy {
    @Override
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

In this example, we have an interface SoundStrategy with a method makeSound. This method is implemented by DogSound, CatSound, and DefaultSound, each providing a specific implementation for the sound the animal makes. The Animal class has a SoundStrategy field, and its constructor accepts a SoundStrategy object to initialize this field. The setSoundStrategy method allows changing the sound strategy dynamically. The performSound method in Animal delegates the responsibility of making a sound to the current soundStrategy object. This redesign aligns with the strategy pattern, encapsulating the sound-making behavior in separate classes and providing flexibility in changing the behavior at runtime.

Shipping Strategy

Another example for instance, is a logistics operation in the throes of grappling with varying shipping strategies. Instead of entrenching each method within the codebase, it is astutely encapsulated within its dedicated class, thereby rendering them interchangeable based on the shifting sands of requirements.

Figure 2. Design Diagram of a Shipping Strategy

Design Diagram of a Shipping Strategy

Also available in: SVG | PlantText

In this diagram, the Order class has an association with the ShippingStrategy interface, indicating that it uses the interface. The FedExShipping and UPSShipping classes implement the ShippingStrategy interface.

Here is a java code that represents this design:

public interface ShippingStrategy {
    double calculateShippingCost(Order order);
}

public class FedExShipping implements ShippingStrategy {
    @Override
    public double calculateShippingCost(Order order) {
        // Implementation for FedEx shipping cost calculation
        return // FedEx specific calculation;
    }
}

public class UPSShipping implements ShippingStrategy {
    @Override
    public double calculateShippingCost(Order order) {
        // Implementation for UPS shipping cost calculation
        return // UPS specific calculation;
    }
}

public class Order {
    private ShippingStrategy shippingStrategy;

    public Order(ShippingStrategy shippingStrategy) {
        this.shippingStrategy = shippingStrategy;
    }

    public void setShippingStrategy(ShippingStrategy shippingStrategy) {
        this.shippingStrategy = shippingStrategy;
    }

    public double calculateShippingCost() {
        return shippingStrategy.calculateShippingCost(this);
    }
}

The given code snippet illustrates a strategy pattern implementation for calculating shipping costs in a flexible and interchangeable manner. The ShippingStrategy interface defines a single method, calculateShippingCost, which takes an Order as a parameter and returns a double representing the shipping cost.

Two concrete classes, FedExShipping and UPSShipping, implement this interface, each providing a specific method to calculate shipping costs according to FedEx and UPS respectively. The Order class has a ShippingStrategy field, and its constructor accepts a ShippingStrategy object to initialize this field. The setShippingStrategy method allows changing the shipping strategy dynamically.

The calculateShippingCost method in Order delegates the responsibility of calculating the shipping cost to the current shippingStrategy object, demonstrating the core principle of the strategy pattern where algorithms (shipping cost calculations in this case) are encapsulated in separate classes and can be easily switched at runtime.

Differences in Growth: Polymorphism vs. Strategy Pattern

When we consider the evolution and expansion of software applications, the difference in growth between polymorphism and the strategy pattern is quite distinct.

Polymorphism: Growth Through Inheritance

In the case of polymorphism, growth predominantly occurs through the mechanism of inheritance. As new functionalities or variants of existing functionalities are introduced, they are typically incorporated into the system by creating new subclasses that inherit from a common superclass. Each of these subclasses then provides its own specific implementation of the behavior dictated by the superclass. This approach tends to grow the class hierarchy vertically, adding more branches to the inheritance tree. However, this can sometimes result in a rigid structure that can be difficult to modify or extend as the system evolves.

Design Advantages:

Testability Advantages:

public class Animal {
    public void sound() {
        System.out.println("Animal makes a sound");
    }
}

public class Dog extends Animal {
    public void sound() {
        System.out.println("Dog barks");
    }
}

public class Cat extends Animal {
    public void sound() {
        System.out.println("Cat meows");
    }
}

Strategy Pattern: Growth Through Behavioral Implementations

On the flip side, the strategy pattern facilitates growth through the addition of new behavioral implementations. As the need for new behaviors arises, new classes encapsulating these behaviors are created and made interchangeable with existing behaviors. This approach grows the system horizontally, adding more options for behavior without modifying the existing class hierarchy. This results in a more flexible and maintainable structure that can easily adapt to changes over time.

Design Advantages:

Testability Advantages:

public interface ShippingStrategy {
    double calculateShippingCost(Order order);
}

public class FedExShipping implements ShippingStrategy {
    @Override
    public double calculateShippingCost(Order order) {
        // Implementation for FedEx shipping cost calculation
        return // FedEx specific calculation;
    }
}

public class UPSShipping implements ShippingStrategy {
    @Override
    public double calculateShippingCost(Order order) {
        // Implementation for UPS shipping cost calculation
        return // UPS specific calculation;
    }
}

In summary, while polymorphism tends to grow the system through inheritance, resulting in a potentially rigid vertical structure, the strategy pattern promotes horizontal growth by adding new behavioral implementations. This fundamental difference highlights the flexibility and maintainability advantages offered by the strategy pattern in adapting to the ever-evolving landscape of software applications. The strategy pattern also tends to be more test-friendly, offering ease of testing different behaviors in isolation.

Testing Polymorphic vs Strategy Designs

When testing polymorphic classes, you often find yourself writing separate tests for each subclass, which can lead to duplicate assertions. For example:

@Test
public void testDogSound() {
    Animal dog = new Dog();
    assertEquals("Dog barks", dog.sound());
}

@Test
public void testCatSound() {
    Animal cat = new Cat();
    assertEquals("Cat meows", cat.sound());
}

@Test
public void testDefaultAnimalSound() {
    Animal animal = new Animal();
    assertEquals("Animal makes a sound", animal.sound());
}

In the above example, each subclass of Animal requires a separate test, even though the assertion logic is quite similar. This can result in redundant code and increase the maintenance burden as the number of subclasses grows.

Testing Behavioral Classes

Conversely, when using the strategy pattern, we can test the behaviors independently and then test their integration with the Animal class separately:

@Test
public void testAnimalWithDogSound() {
    SoundStrategy dogSound = new DogSound();
    Animal animal = new Animal(dogSound);
    assertEquals("Dog barks", animal.performSound());
}

@Test
public void testDogSound() {
    SoundStrategy dogSound = new DogSound();
    assertEquals("Dog barks", dogSound.makeSound());
}

@Test
public void testCatSound() {
    SoundStrategy catSound = new CatSound();
    assertEquals("Cat meows", catSound.makeSound());
}

In this example, the DogSound and CatSound behaviors are tested independently of the Animal class. We then have a single test for the Animal class with the DogSound behavior, significantly reducing duplicate assertions. This approach simplifies the testing process and ensures scalability as new behaviors are introduced.

The ease of testing is a good indicator that the strategy pattern provides a clear advantage in scenarios where behavior needs to be changed dynamically. By isolating behaviors and making them interchangeable, the strategy pattern facilitates a more modular and maintainable design. This translates to simpler and more effective testing procedures, as each behavior can be tested in isolation, and their integration with the main class can be verified with minimal tests.

Compared to the redundancy in testing polymorphic classes, this serves as a strong testament that a strategy pattern is best for this case. The modular nature of the strategy pattern allows for independent testing of behaviors and their integration, significantly reducing the complexity and duplication often found in testing polymorphic classes.

In Conclusion

The intricate tapestry of software design is masterfully woven from myriad threads of strategic decisions, among which the pivotal choice between the strategy pattern and polymorphism prominently stands.

In cases where your design finds itself entangled in the complexities of polymorphism, it might be judicious to shift towards a more behavioral approach, as encapsulated by the strategy pattern. This method doesn’t just bring clarity and flexibility to your design process, effectively easing the traversal through multifaceted design challenges, but it also significantly bolsters the testability of your system. Such an enhancement in testability proves invaluable, as it facilitates the independent validation of each behavior or strategy, thereby streamlining the testing process.

The strategy pattern emerges as a paragon of flexibility, proffering a resilient framework that deftly accommodates the interchangeability of algorithms or behaviors in real-time. Deeply rooted in time-honored design principles, it stands as a monumental tribute to the indispensability of code re-usability and the unwavering pursuit of impeccable, flawless code. The utility of the strategy pattern is abundantly clear, particularly in scenarios that demand the meticulous encapsulation of behavior, as eloquently illustrated by the previously discussed example of shipping cost calculations. Ultimately, this approach doesn’t merely solve problems; it resolves them with unparalleled elegance and efficiency, epitomizing the very essence of clean, maintainable code.


Related Articles:

The Mock Object Design Pattern Post Date: 26 Nov 2023
The Mock Object Design Pattern is an essential aspect of modern software development, pivotal for enhancing the efficiency and reliability of software testing. It focuses on creating mock objects that simulate the behavior of real objects in a controlled environment, aimed at isolating the system under test. This isolation ensures that unit tests are independent of external elements and solely focused on the code being tested.
Understanding Deep Linking in SEO Post Date: 26 Nov 2023
In the intricate world of Search Engine Optimization (SEO), mastering the art of deep linking strategy is akin to discovering a hidden pathway to success. At its core, deep linking is not merely a set of actions but a philosophy that redefines how we perceive and structure our websites. It’s a journey into the depths of your website, unlocking the potential of each page and transforming them into powerful entities in their own right.
Agile • Best Practices and Strategies when Splitting User Stories Post Date: 25 Nov 2023
In Agile project management, User Stories play a pivotal role as fundamental building blocks. These short, simple descriptions of a software feature from the perspective of the end user are crucial in guiding teams toward creating value-driven, user-centric solutions. However, as projects evolve and complexities mount, these user stories can often become unwieldy or too broad, making them difficult to manage and execute effectively.
Agile • Why I Prefer Story Cards And Sticky Notes Post Date: 25 Nov 2023
In the dynamic realm of Agile software development, the tools and techniques we employ play a pivotal role in shaping our success. Among the many strategies that Agile practitioners use, story cards and sticky notes have proven themselves as timeless assets.
Treat Test Code As Production Code Post Date: 24 Nov 2023
In the ever-evolving landscape of software development, Java stands as a stalwart, powering a myriad of applications across diverse industries. But beneath the surface of this robust and versatile language lies a fundamental aspect often overlooked yet crucial for its success: the quality and integrity of test code.
Refactor Monolithic Code in Agile Post Date: 24 Nov 2023
In the context of software development, adaptability and scalability are the keys to staying ahead of the curve. Enter Agile development, a methodology that champions flexibility and iterative improvement. But what happens when your project inherits a monolithic codebase, where change is akin to navigating a labyrinth? It’s here that the art of refactoring comes into play.
WEBP vs PNG vs JPG Post Date: 23 Nov 2023
In the fast-paced realm of digital content, where visual appeal and speedy performance are paramount, choosing the right image format can make a world of difference. This overview sets the stage for our exploration of two formidable contenders: WebP, PNG and JPG.
Software • Code Cohesion Post Date: 23 Nov 2023
In the dynamic landscape of software development, the concept of code cohesiveness stands as a cornerstone of creating efficient and maintainable applications. Especially in Java, a language renowned for its robustness and scalability, understanding and applying cohesiveness principles can significantly elevate the quality of software projects.
Design Patterns • Composition vs Aggregation Post Date: 17 Nov 2023
When it comes to software engineering and design, understanding the concepts of composition and aggregation is crucial for creating robust and efficient domain models. These terms, often used in object-oriented programming, represent relationship patterns between objects that dictate how they interact and depend on each other.
ReST HATEOAS Best Practices Post Date: 09 Nov 2023
Hypertext As The Engine Of Application State (HATEOAS) is a constraint of the REST application architecture that keeps the RESTful style architecture unique. It enables the server to dynamically guide clients through the application by including hypermedia links with the responses.
HTML Anchor Tag Post Date: 09 Nov 2023
The HTML anchor tag, defined by the <a> element, is a cornerstone in web development, pivotal for creating hyperlinks. These hyperlinks are the lifelines of the internet, connecting various resources and allowing users to navigate between them seamlessly.
Advanced Strategies for Content Negotiation in RESTful APIs Post Date: 08 Nov 2023
Mastering content negotiation is essential for developing ReST APIs that excel in performance, flexibility, and user-centricity. This nuanced aspect of API design ensures that services are not only operational but are finely attuned to the diverse requirements of clients, offering a more tailored and resilient interaction.
Core Principles of ReSTful API Design - A Deep Dive Post Date: 08 Nov 2023
In the dynamic world of web development and system architecture, the design of APIs (Application Programming Interfaces) plays a crucial role in shaping the interaction between different software components. ReSTful API, standing for Representational State Transfer, has emerged as a leading standard in creating efficient, scalable, and flexible web services.
Docker Compose Best Practices Post Date: 29 Oct 2023
Docker Compose is an essential tool for developers who want to define and manage multi-container Docker applications. With its simple YAML configuration file, you can automate the deployment of your application’s services, networks, and volumes, ensuring a seamless integration and functioning of your entire system.
Leveraging Abstractions in Software Development Post Date: 27 Oct 2023
Abstractions play a crucial role in simplifying complex systems and making them more manageable, especially in the realm of software development. By understanding and implementing abstraction in software development, developers can create cleaner, more efficient, and more maintainable code.
Agile • How Code Complexity Affects Story Points Post Date: 26 Oct 2023
Software development has been revolutionized by the Agile process, which has significantly changed how projects are managed and executed. A crucial aspect of this methodology is the utilization of stories and story points, instrumental in determining the complexity and estimated time required to complete a feature or task.
Loose Coupling in Software Engineering Post Date: 25 Oct 2023
In the realm of software engineering, the concept of loose coupling represents a golden standard in design paradigms, championing a modular and flexible system that enhances software extensibility and adaptability. By embracing loose integration and prioritizing decoupled components, developers are better equipped to foster an environment conducive to growth, scalability, and long-term success.
Single Responsibility Principle in Software Development Post Date: 25 Oct 2023
The software development realm is vast, and with its expanse comes an array of techniques and methodologies that software professionals leverage to ensure the creation of robust, enterprise-grade software. At the forefront of these methodologies is the concept of object-oriented programming (OOP), a paradigm that brings a suite of design principles to the table.
The Singleton Design Pattern Post Date: 08 Jun 2023
The Singleton design pattern is a widely used and important pattern in software development. It allows the creation of a single instance of a class, ensuring that this instance is globally accessible throughout the application. In this article, we will explore various approaches to implementing Singletons in Java, examining their advantages, disadvantages, and use cases.
Is REST API Stateless? Post Date: 11 May 2023
The Representational State Transfer (REST) architectural style has become the foundation for building scalable and distributed web services. At the core of REST lies the concept of statelessness, which implies that each request sent to a RESTful API should contain all the necessary information for the server to process it, without relying on any previous interactions.
Common Misunderstandings of HTTP Status Codes Post Date: 10 May 2023
In the world of web development and API design, HTTP status codes play a crucial role in communicating the outcome of client-server interactions. However, there are several common misunderstandings surrounding these status codes that can lead to confusion and misinterpretation.
Cryptographic Algorithms: A Comparison of Security and Strength Post Date: 07 May 2023
When it comes to encryption algorithms, the question of which one is the most secure is not a straightforward one. The answer depends on a variety of factors, such as the intended use case and the context in which the algorithm is being used.
10 Best Attributes of a Software Developer Post Date: 13 Apr 2023
The sentence “What are the 10 best attributes of a software developer?” is a question that seeks to identify the key qualities that make a great software developer.