Introduction
In Week 06 Task 02, I continued learning Design Patterns in JavaScript.
For this task, I implemented two important design patterns:
- Strategy Pattern
- Adapter Pattern
The goal was to understand how design patterns can help make code more flexible, reusable, and easier to maintain.
I also created tests using Node.js's built-in assert module to verify that the implementations worked correctly.
1. Strategy Pattern
What is the Strategy Pattern?
The Strategy Pattern allows us to define multiple ways of performing an operation and choose between them at runtime.
Instead of putting all the logic inside one large if-else or switch statement, we create separate strategies.
For example, an online shopping application may have different discount rules:
Checkout
|
Select Discount Strategy
|
┌──────────┼──────────┐
↓ ↓ ↓
No Discount Percentage Flat
Each strategy handles its own discount calculation.
My Implementation
I created three discount strategies:
NoDiscountPercentageDiscountFlatDiscount
No Discount
class NoDiscount {
apply(price) {
return price;
}
}
This strategy returns the original price.
Percentage Discount
class PercentageDiscount {
constructor(percent) {
this.percent = percent;
}
apply(price) {
return price - (price * this.percent) / 100;
}
}
For example, a 10% discount on ₹1000 gives:
₹1000 - ₹100 = ₹900
Flat Discount
class FlatDiscount {
constructor(amount) {
this.amount = amount;
}
apply(price) {
return Math.max(0, price - this.amount);
}
}
For example, a ₹200 discount on ₹1000 gives:
₹1000 - ₹200 = ₹800
Checkout Context
I created a Checkout class that uses a discount strategy.
class Checkout {
constructor(discountStrategy) {
this.discountStrategy = discountStrategy;
}
setDiscountStrategy(strategy) {
this.discountStrategy = strategy;
}
getFinalPrice(price) {
return this.discountStrategy.apply(price);
}
}
Now the discount strategy can be changed without changing the Checkout class.
Example
const checkout = new Checkout(
new PercentageDiscount(10)
);
console.log(checkout.getFinalPrice(1000));
Output:
900
We can also change the strategy:
checkout.setDiscountStrategy(
new FlatDiscount(200)
);
console.log(checkout.getFinalPrice(1000));
Output:
800
Why is the Strategy Pattern Useful?
Without the Strategy Pattern, we might write something like:
if (type === "percentage") {
// percentage logic
} else if (type === "flat") {
// flat discount logic
} else {
// no discount logic
}
As more discount types are added, this code becomes harder to maintain.
With the Strategy Pattern, each discount algorithm has its own class.
This provides:
- Better separation of responsibilities
- Easier testing
- Easier extension
- Less conditional logic
- Runtime strategy selection
2. Adapter Pattern
What is the Adapter Pattern?
The Adapter Pattern allows two incompatible interfaces to work together.
A simple real-world example is a travel plug adapter.
Device Plug
↓
Adapter
↓
Wall Socket
The device and socket may use different interfaces, but the adapter allows them to communicate.
The same idea can be used in software.
The Problem
Suppose our application expects a payment gateway with this method:
pay(amount)
But an old payment system provides:
makePayment(amountInCents)
The interfaces are different.
Instead of changing the entire application or rewriting the old gateway, we can create an adapter.
Application
|
↓
Payment Adapter
|
↓
Legacy Payment Gateway
My Implementation
I created a legacy payment gateway:
class LegacyPaymentGateway {
makePayment(amountInCents) {
return {
success: true,
amountInCents
};
}
}
The application expects a simpler pay() method.
So I created a PaymentAdapter:
class PaymentAdapter {
constructor(legacyGateway) {
this.legacyGateway = legacyGateway;
}
pay(amount) {
return this.legacyGateway.makePayment(amount * 100);
}
}
The adapter converts the application's expected input into the format required by the legacy gateway.
Example
const legacyGateway = new LegacyPaymentGateway();
const paymentAdapter =
new PaymentAdapter(legacyGateway);
console.log(paymentAdapter.pay(10));
The application provides:
10
The adapter converts it to:
1000 cents
and calls:
makePayment(1000)
The application does not need to know about the legacy gateway's interface.
Checkout Payment Service
I also created a service that works with the expected payment interface.
class CheckoutPaymentService {
constructor(paymentGateway) {
this.paymentGateway = paymentGateway;
}
checkout(amount) {
return this.paymentGateway.pay(amount);
}
}
Now the service can work with the adapter:
const service = new CheckoutPaymentService(
paymentAdapter
);
console.log(service.checkout(50));
The flow becomes:
CheckoutPaymentService
|
↓
PaymentAdapter
|
↓
LegacyPaymentGateway
Strategy vs Adapter
Although both patterns help organize code, they solve different problems.
| Pattern | Main Purpose |
|---|---|
| Strategy | Choose between different algorithms or behaviors |
| Adapter | Make incompatible interfaces work together |
Strategy
Same problem
↓
Different algorithms
↓
Choose one at runtime
Example:
Discount
├── No Discount
├── Percentage
└── Flat
Adapter
Different interfaces
↓
Adapter
↓
Compatible interface
Example:
Application
↓
PaymentAdapter
↓
Legacy Gateway
Testing
I used Node.js's built-in assert module to test both patterns.
Strategy Pattern Tests
I tested:
- No discount
- Percentage discount
- Flat discount
- Changing strategies
- Final price calculation
Example:
assert.strictEqual(
checkout.getFinalPrice(1000),
900
);
Adapter Pattern Tests
I tested:
- The adapter correctly calls the legacy gateway.
- The amount is converted correctly.
- The checkout service can use the adapted gateway.
Example:
assert.deepStrictEqual(
service.checkout(10),
{
success: true,
amountInCents: 1000
}
);
Project Structure
The final Task 02 structure was:
week-06/
└── Task-02/
├── README.md
├── discount-strategies.js
├── payment-adapter.js
└── test.js
Each file had a specific responsibility:
| File | Purpose |
|---|---|
discount-strategies.js |
Strategy Pattern implementation |
payment-adapter.js |
Adapter Pattern implementation |
test.js |
Tests |
README.md |
Documentation |
What I Learned
Strategy Pattern
I learned that the Strategy Pattern allows us to separate different algorithms into individual classes and choose the required behavior at runtime.
Instead of modifying one large class whenever a new algorithm is added, we can create a new strategy.
Adapter Pattern
I learned that the Adapter Pattern is useful when we need to integrate existing code with a new system that expects a different interface.
Instead of changing the existing implementation, we can place an adapter between the two systems.
Key Takeaways
The main concepts I learned from this task are:
Strategy → Choose between different behaviors
Adapter → Convert one interface into another
Strategy
Focuses on:
- Behavior
- Algorithms
- Runtime selection
- Flexibility
Adapter
Focuses on:
- Compatibility
- Interface conversion
- Legacy systems
- Integration
Conclusion
Week 06 Task 02 helped me understand two more important JavaScript design patterns: Strategy and Adapter.
The Strategy Pattern showed me how to make behavior interchangeable, while the Adapter Pattern showed me how to make incompatible interfaces work together.
Implementing both patterns and writing tests helped me understand not only what these patterns are, but also when and why they should be used.
These concepts are useful when building larger applications because they help keep code modular, flexible, and easier to maintain.
Top comments (0)