DEV Community

koushikmaya
koushikmaya

Posted on

# Week 06 Task 02: Strategy and Adapter Patterns in JavaScript

Introduction

In Week 06 Task 02, I continued learning Design Patterns in JavaScript.

For this task, I implemented two important design patterns:

  1. Strategy Pattern
  2. 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
Enter fullscreen mode Exit fullscreen mode

Each strategy handles its own discount calculation.


My Implementation

I created three discount strategies:

  • NoDiscount
  • PercentageDiscount
  • FlatDiscount

No Discount

class NoDiscount {
  apply(price) {
    return price;
  }
}
Enter fullscreen mode Exit fullscreen mode

This strategy returns the original price.

Percentage Discount

class PercentageDiscount {
  constructor(percent) {
    this.percent = percent;
  }

  apply(price) {
    return price - (price * this.percent) / 100;
  }
}
Enter fullscreen mode Exit fullscreen mode

For example, a 10% discount on ₹1000 gives:

₹1000 - ₹100 = ₹900
Enter fullscreen mode Exit fullscreen mode

Flat Discount

class FlatDiscount {
  constructor(amount) {
    this.amount = amount;
  }

  apply(price) {
    return Math.max(0, price - this.amount);
  }
}
Enter fullscreen mode Exit fullscreen mode

For example, a ₹200 discount on ₹1000 gives:

₹1000 - ₹200 = ₹800
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

Output:

900
Enter fullscreen mode Exit fullscreen mode

We can also change the strategy:

checkout.setDiscountStrategy(
  new FlatDiscount(200)
);

console.log(checkout.getFinalPrice(1000));
Enter fullscreen mode Exit fullscreen mode

Output:

800
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

But an old payment system provides:

makePayment(amountInCents)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

My Implementation

I created a legacy payment gateway:

class LegacyPaymentGateway {
  makePayment(amountInCents) {
    return {
      success: true,
      amountInCents
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

The application provides:

10
Enter fullscreen mode Exit fullscreen mode

The adapter converts it to:

1000 cents
Enter fullscreen mode Exit fullscreen mode

and calls:

makePayment(1000)
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the service can work with the adapter:

const service = new CheckoutPaymentService(
  paymentAdapter
);

console.log(service.checkout(50));
Enter fullscreen mode Exit fullscreen mode

The flow becomes:

CheckoutPaymentService
          |
          ↓
   PaymentAdapter
          |
          ↓
LegacyPaymentGateway
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Example:

Discount
 ├── No Discount
 ├── Percentage
 └── Flat
Enter fullscreen mode Exit fullscreen mode

Adapter

Different interfaces
       ↓
     Adapter
       ↓
Compatible interface
Enter fullscreen mode Exit fullscreen mode

Example:

Application
    ↓
PaymentAdapter
    ↓
Legacy Gateway
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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
  }
);
Enter fullscreen mode Exit fullscreen mode

Project Structure

The final Task 02 structure was:

week-06/
└── Task-02/
    ├── README.md
    ├── discount-strategies.js
    ├── payment-adapter.js
    └── test.js
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)