This article is about a problem many of us encounter in React & Frontend development (sometimes even without realizing that it's a problem): Having a piece of logic implemented throughout different components, hooks, utils, etc.
Let's dive into the problem details and how to solve it. As the title suggests, we're going to use the Strategy Pattern to solve it.
The problem: Shotgun Surgery
Shotgun Surgery is a code smell where making any modifications requires making many small changes to many different places.
(image source: https://refactoring.guru/smells/shotgun-surgery)
How can this happen in a project? Let's imagine we need to implement pricing cards for a product, and we adjust the price, the currency, the discount strategy and the messages based on where the client is coming from:
In this contrived example, without the existence of localization, the pricing card might be implemented as follows:
- Components:
PricingCard
,PricingHeader
,PricingBody
. - Utility functions:
getDiscountMessage
(in utils/discount.ts),formatPriceByCurrency
(in utils/price.ts). - The
PricingBody
component also calculates the final price.
Here's the full implementation:
Now let's imagine we need to change the pricing plan for a country, or add a new pricing plan for another country. What will you have to do with the above implementation? You'll have to at least modify 3 places and add more conditionals to the already messy if-else
blocks:
- Modify the
PricingBody
component. - Modify the
getDiscountMessage
function. - Modify the
formatPriceByCurrency
function.
If you've already heard of S.O.L.I.D, we're already violating the first 2 principles: The Single Responsibility Principle & The Open-Closed Principle.
The solution: Strategy Pattern
The Strategy Pattern is quite straightforward. We can simply understand that each of our pricing plans for the countries is a strategy. And in that strategy class, we implement all the related logic for that strategy.
Suppose you are familiar with OOP, we can have an abstract class (PriceStrategy
) that implements the shared/common logic, and then a strategy with different logic will inherit that abstract class. The PriceStrategy
abstract class looks like this:
import { Country, Currency } from '../../types';
abstract class PriceStrategy {
protected country: Country = Country.AMERICA;
protected currency: Currency = Currency.USD;
protected discountRatio = 0;
getCountry(): Country {
return this.country;
}
formatPrice(price: number): string {
return [this.currency, price.toLocaleString()].join('');
}
getDiscountAmount(price: number): number {
return price * this.discountRatio;
}
getFinalPrice(price: number): number {
return price - this.getDiscountAmount(price);
}
shouldDiscount(): boolean {
return this.discountRatio > 0;
}
getDiscountMessage(price: number): string {
const formattedDiscountAmount = this.formatPrice(
this.getDiscountAmount(price)
);
return `It's lucky that you come from ${this.country}, because we're running a program that discounts the price by ${formattedDiscountAmount}.`;
}
}
export default PriceStrategy;
And we simply pass the instantiated strategy as a prop to the PricingCard
component:
<PricingCard price={7669} strategy={new JapanPriceStrategy()} />
with the props of PricingCard
defined as:
interface PricingCardProps {
price: number;
strategy: PriceStrategy;
}
Again, if you know OOP, not only we're using Inheritance, but we're also using Polymorphism here.
Here's the full implementation of the solution:
And let us ask the same question again: How do we add a new pricing plan for a new country? With this solution, we simply need to add a new strategy class, and we don't need to modify any of the existing code. By doing so, we're satisfying S.O.L.I.D as well.
Conclusion
So, by detecting a code smell - Shotgun Surgery - in our React codebase, we have applied a design pattern - Strategy Pattern - to solve it. Our code structure went from this:
to this:
Now our logic lives in one place and is no longer spread throughout many places anymore. Do note that this whole article revolves around a contrived example. Practically, the Strategy Pattern can be implemented in simpler ways (using objects instead of classes). Please check out part 2 of this series:
⚛️ Applying Strategy Pattern in React (Part 2)
Will T. ・ Mar 9
If you're interested in design patterns & architectures and how they can be used to solve problems in the Frontend world, make sure to give me a like & a follow.