Strategy Pattern
How strategy pattern helps in implementing Open/Closed Principle
Stragtegy Pattern (Object Oriented Programming)
One of the SOLID principles is Open/Close Principle, which simply means that no changes must be made in the main class i.e. it is closed for any modification. But in real world projects, as product evolves, changes need to be made and there nust be some way to accomodate new functionality without modifying original class. One way to do it is to use Stratgy Pattern.
Example of modifying class
Lets say we want to add payment method but there are multiple ways and there is a possibility that more payment methods can be added later.
class PaymentProcessor {
processPayment(method, amount) {
if (method === "paypal") {
console.log("Processing PayPal payment...");
} else if (method === "creditcard") {
console.log("Processing credit card payment...");
} else if (method === "banktransfer") {
console.log("Processing bank transfer...");
}
}
}Problem with this method is that if later additional payment methods need to be added, another if-else loop or another case in switch function would be added. This would in turn alter this main class, thereby violating SOLID principle (Open/Close). There is a more cleaner method which can be used here.
Strategy Pattern
Stratgey pattern is simply taking control or decision making from the main class and delegating it to other classes. Main class just acts as a "container" where other classes can be "injected" based on conditions. If the above case is revamped, it would required some changes:
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
processPayment(amount) {
if (!this.strategy) {
throw new Error("No payment strategy set.");
}
// This function: "pay" is important as this would be used in other created stratgeies
this.strategy.pay(amount);
}
}// Class 1:
class PayPalStrategy {
constructor(email) {
this.email = email;
}
pay(amount) {
console.log(Paying $${amount} using PayPal (Account: ${this.email}).);
}
}
// Class 2:
class CreditCardStrategy {
constructor(cardNumber) {
this.cardNumber = cardNumber;
}
pay(amount) {
console.log( Paying $${amount} using CreditCard (Card-No: ${this.cardNumber})., );
}
}
// Class 3:
class BankTransferStrategy {
constructor(accountNumber) {
this.accountNumber = accountNumber;
}
pay(amount) {
console.log( Paying $${amount} using Bank Transer (Bank-Account-No: ${this.accountNumber})., );
}
}
export {
PayPalStrategy,
CreditCardStrategy,
BankTransferStrategy
};
These classes in payment-stratgies are the one where actual implementation would take place, rather than the main payment-processor class. Having these other classes in payment-strategies allows more methods (i.e. more classes) to be added without changin the main class, hence maintaining open-closed principle.
The useage of these classes:
import PaymentProcessor from "./payment-processor.js";
import {
PayPalStrategy,
BankTransferStrategy,
CreditCardStrategy,
} from "./strategies.js";
const processor = new PaymentProcessor();
const paymentStrategy = "paypal";
const amountToPay = 100;
switch (paymentStrategy) {
case "paypal":
processor.setStrategy(new PayPalStrategy("user@example.com"));
break;
case "creditcard":
processor.setStrategy(new CreditCardStrategy("1234-5678-9012-4356"));
break;
case "banktransfer":
processor.setStrategy(new BankTransferStrategy("987654321"));
break;
default:
console.log("Invalid payment method.");
break;
}
processor.processPayment(amountToPay);In implemention we would inject the required instance of class with required parameter, the main class would set the strategy passeed an in turn pass the control to the actual classes where main processing would happen.
For React Developers
This pattern in useful in object-oriented programing but something similar can be implemented in functional programing, specificaly in React.
Lets say we want to render some components based on some condition. The most crude method that one can think of is using if-else/switch statements and directly embedding component or component call for that case. Something similar:
function Price({ userType, price }) {
if (userType === "regular") return <p>${price}</p>;
if (userType === "premium") return <p>${price * 0.8} (20% off)</p>;
if (userType === "admin")
return (
<p>
Base: ${price} | Tax: ${price * 0.1}
</p>
);
}A more sophisticated and scalable way would be to have these components render via a Context component which, based on condition passed, would call the suitable component:
const RegularPrice = ({ price }) => {
return <p>${price}</p>;
};
const PremiumPrice = ({ price }) => {
return <p>${price * 0.8} (20% off)</p>;
};
const AdminPrice = ({ price }) => {
return (
<p>
Base: ${price} | Tax: ${price * 0.1}
</p>
);
};There can be more components created and each component can also be created in a seperate file, based on how complex the component is.
Then comes the context component.
function Price({ strategy: StrategyComponent, price }) {
return <StrategyComponent price={price} />;
}This would be implemented such that pricing option can be selected at run time:
const priceStrategies = {
regular: RegularPrice,
premium: PremiumPrice,
admin: AdminPrice,
};
const ProductPage = ({ userType }) => {
const Strategy = priceStrategies[userType] || RegularPrice;
return <Price strategy={Strategy} price={100} />;
}What is being done here is that the name of the component is being passed based on the condition. The context component simply utilises the required component while passing the prop as the same time so that component can process. That component represents the presentation layer. This component can also be a function from utils lib.
Summary
- Strategy pattern helps to avoid violating Open/Close Principle (SOLID DESING PRINCIPLES).
- In react, similar stratgy can be implemented so that presentation layer can be seperated and if-else/switch statements can be avoided by a common context component.