Structural Design Patterns

 

🚀 What are Structural Design Patterns?

Structural patterns are about how classes and objects are combined to form larger structures.
Think of them as blueprints for connecting components together efficiently.

They help when:

  • You want to reuse code without rewriting it.

  • You want to adapt incompatible interfaces.

  • You want to simplify complex structures.

📑 List of Structural Design Patterns

There are 7 key patterns:

  1. Adapter → Convert one interface into another the client expects.

  2. Bridge → Decouple abstraction from implementation.

  3. Composite → Tree structures (part-whole hierarchy).

  4. Decorator → Add responsibilities to objects dynamically.

  5. Facade → Provide a simplified interface to a complex system.

  6. Flyweight → Reuse objects to save memory.

  7. Proxy → Control access to an object.


⚡ How we’ll learn them

To make it easy, let’s take real-world examples + Java code:

  • Adapter → Charger plug adapter (110V to 220V).

  • Bridge → Remote control works with different TVs.

  • Composite → Folders & files in a file system.

  • Decorator → Adding toppings to a pizza.

  • Facade → Home theater system (one button → many subsystems).

  • Flyweight → Characters in a text editor.

  • ProxyATM machine as proxy for bank.




Problem:

An e-commerce platform supports multiple payment providers (PayPal, Stripe, Razorpay, PhonePe, GPay, Jupiter), each with a different interface. We need a standardized way to integrate them.


Solution:Use the Adapter Pattern to convert the interface of a third-party payment gateway into a common interface expected by the application.

Benefits:

✔ Integrates incompatible interfaces without modifying code.
✔ Adds new payment providers without changing the existing system.


💳 Adapter Pattern – Payment Gateway Example



.


🧩 What is Adapter Pattern?

👉 The Adapter Pattern is a structural design pattern.
It allows two incompatible interfaces to work together without changing their code.

Think of it as a translator 🗣 between two people speaking different languages.

🔎 Why do we need it (Use)?

  • You have some existing code (e.g., your e-commerce system expects a PaymentProcessor).

  • You want to use 3rd-party libraries (PayPal, Stripe, Razorpay, etc.), but their APIs (methods) are different.

  • Instead of rewriting everything, you create an Adapter that makes those APIs look the same to your system.

👉 This way:

  • Your system stays clean.

  • You can add new payment providers easily without touching the client code.


📌 Example in Plain Words

Imagine:

  • You know only Hindi.

  • PayPal speaks English, Stripe speaks French, Razorpay speaks Japanese.

Without adapter: You must learn English, French, and Japanese. (Too much work).

With adapter (translator):

  • You always speak Hindi.

  • Translator (adapter) translates Hindi → English/French/Japanese.

  • You don’t care what language they speak.

👉 You only remember Hindi, not every other language.
Same in code → client only remembers pay(amount).


🛒 E-Commerce Payments – Before & After Adapter Pattern



Before Adapter (messy integration)

Problem:

  • Each provider has its own unique method name.

  • Client code has to know every API → tightly coupled, not extensible.


package org.example.design.StructuralDesign.Adapter;

public class BeforeAdapterDemo {
public static void main(String[] args) {

// Direct integration with third-party APIs

// PayPal
PayPalGateway paypal = new PayPalGateway();
paypal.makePayment(250.0);

// Stripe
StripeGateway stripe = new StripeGateway();
stripe.doPayment(499.0);

// Razorpay
RazorpayGateway razorpay = new RazorpayGateway();
razorpay.processRazorpayPayment(999.0);

// If tomorrow we add GPay → client must learn its new API 😓
}
}

❌ Problems:

  • Client must remember different method names:

    • makePayment() for PayPal

    • doPayment() for Stripe

    • processRazorpayPayment() for Razorpay

  • Adding a new provider → modify client code everywhere.

  • Tight coupling with vendors.


package org.example.design.StructuralDesign.Adapter;

public class PayPalGateway {

public void makePayment(double money)
{

System.out.println("Payment done through paypal Gateway");
}
}

package org.example.design.StructuralDesign.Adapter;

public class StripeGateway {
public void doPayment(double money)
{
System.out.println("payment done through Stripe Gateway");
}
}

package org.example.design.StructuralDesign.Adapter;

public class RazorpayGateway {
public void processRazorpayPayment(double amountInINR) {
System.out.println("payment done through Razorpay Gateway");
}
}


Main Class -problem Tight Coupling (api name-Method name)


package org.example.design.StructuralDesign.Adapter;

public class BeforeAdapterDemo {
public static void main(String[] args) {

// Direct integration with third-party APIs

// PayPal
PayPalGateway paypal = new PayPalGateway();
paypal.makePayment(250.0);

// Stripe
StripeGateway stripe = new StripeGateway();
stripe.doPayment(499.0);

// Razorpay
RazorpayGateway razorpay = new RazorpayGateway();
razorpay.processRazorpayPayment(999.0);

// If tomorrow we add GPay → client must learn its new API 😓
}


}




After Adapter (clean & unified)

Solution:

  • Define a common interface (PaymentProcessor).

  • Write adapters for each provider.

  • Client only depends on PaymentProcessor.



With Adapter (solution)

  • You create one common interface: PaymentProcessor { pay(amount) }

  • For each provider, you write a small Adapter class that translates .pay(amount) into the provider’s method.

👉 Now the client always calls .pay(amount), no matter which provider is used.


Adapter class create 

package org.example.design.StructuralDesign.Adapter;

public class PayPalAdapter implements PaymentProcessor{

PayPalGateway payPalGateway;

public PayPalAdapter(PayPalGateway payPalGateway) {
this.payPalGateway = payPalGateway;
}

@Override
public void pay(double amount) {
payPalGateway.makePayment(amount);
}
}

(Same for StripeAdapter, RazorpayAdapter)......

package org.example.design.StructuralDesign.Adapter;

public class AfterAdapterTest {
public static void main(String[] args) {
// Client now works with a single interface PaymentProcessor

PaymentProcessor paypal=new PayPalAdapter(new PayPalGateway());
paypal.pay(200.00);

PaymentProcessor stripeProcessor = new StripeAdapter(new StripeGateway());
stripeProcessor.pay(499.0);

PaymentProcessor razorpayProcessor = new RazorpayAdapter(new RazorpayGateway());
razorpayProcessor.pay(999.0);

// Tomorrow if GPay or PhonePe comes → just add new adapter



}
}

🚀 Benefits (Now Super Clear)

  1. Client only calls .pay() — never cares about vendor APIs (upiPay, makePayment, etc).

  2. One unified flow — you don’t keep creating multiple adapter objects manually.

    • System picks correct adapter at runtime.

  3. Easily extensible — Add PhonePe tomorrow → just write PhonePeAdapter + update factory.

    • Client code stays untouched.


class PaymentFactory {
public static PaymentProcessor getPaymentProcessor(String type) {
return switch (type) {
case "PayPal" -> new PayPalAdapter(new PayPalGateway());
case "Stripe" -> new StripeAdapter(new StripeGateway());
case "GPay" -> new GPayAdapter(new GPayGateway());
default -> throw new IllegalArgumentException("Unknown payment type");
};
}
}


public class EcommerceClient {
public static void main(String[] args) {
String selected = "GPay"; // assume user selected GPay from UI

PaymentProcessor processor = PaymentFactory.getPaymentProcessor(selected);
processor.pay(500); // client still calls only .pay()
}
}



🎁 Decorator Pattern

🔎 Problem

  • An e-commerce platform sells products.

  • Customers want optional features:

    • Gift wrap

    • Extended warranty

    • Personalization (engraving, custom message, etc.)

  • These should be added dynamically, without changing the core Product class.



 Solution (Decorator Pattern)

  • Define a common interface for product.

  • Create a base product (e.g., Laptop, Phone).

  • Wrap the product with decorators (GiftWrapDecorator, WarrantyDecorator, PersonalizationDecorator).

  • Each decorator adds extra cost or behavior.


🎁 Before vs After: Decorator Pattern

❌ Before (Without Decorator)

Problem:

  • Every time we add a new feature (Gift Wrap, Warranty, Personalization), we would need a new subclass.

  • Example:

    • LaptopWithGiftWrap

    • LaptopWithWarranty

    • LaptopWithGiftWrapAndWarranty

    • LaptopWithGiftWrapAndPersonalization

  • This quickly leads to class explosion 💥.

Before Decorator

package org.example.design.structural.decoratorPattern;

public class Laptop {

public String getDescription()
{
return "Laptop";
}
public double getCost()
{
return 500.00;
}


}

package org.example.design.structural.decoratorPattern;

public class LaptopWithGiftWrap extends Laptop{

@Override
public String getDescription() {
return super.getDescription() + ", Gift Wrapped";
}

@Override
public double getCost() {
return super.getCost() + 200.0;
}

}


package org.example.design.structural.decoratorPattern;

public class LaptopWithWarranty extends Laptop{
@Override
public String getDescription() {
return super.getDescription()+ ", Extended Warranty";
}

@Override
public double getCost() {
return super.getCost()+1500;
}
}

// If customer wants GiftWrap + Warranty + Personalization → new class needed

❌ Problems:

  • Too many subclasses.

  • Hard to maintain.

  • Any new feature = create more subclasses.

 After (With Decorator)

Solution:

  • We keep one base product class (Laptop).

  • Each feature (GiftWrap, Warranty, Personalization) is a decorator.

  • We can combine decorators dynamically at runtime → no explosion of subclasses.


🚀 Benefits of After (Decorator)

✔ Add/remove features dynamically (no new classes).
✔ Core Laptop class is unchanged.
✔ Avoids class explosion.
✔ Flexible: Any combination of features at runtime.


📌 Interview Summary:

  • Before: Inheritance → subclass explosion.

  • After: Composition with Decorator → flexible, scalable.



 Real-World Style: Dynamic Decorator Usage

Imagine a customer is shopping on an e-commerce site:

  • Selects Laptop as the base product.

  • Then ticks checkboxes:  Gift Wrap, Warranty, ❌ Personalization.

We can build decorators dynamically based on user input.


package org.example.design.structural.decoratorPattern;

public interface Product {
String getDescription();
double getCost();
}


package org.example.design.structural.decoratorPattern;

public class Laptop implements Product {

public String getDescription()
{
return "Laptop";
}
public double getCost()
{
return 500.00;
}


}

package org.example.design.structural.decoratorPattern;

abstract class ProductDecorator implements Product{

protected Product product;

public ProductDecorator(Product product) {
this.product = product;
}

@Override
public String getDescription() {
return product.getDescription();
}

@Override
public double getCost() {
return product.getCost();
}
}


package org.example.design.structural.decoratorPattern;

public class GiftWrapDecorator extends ProductDecorator{
public GiftWrapDecorator(Product product) {
super(product);
}

@Override
public String getDescription() {
return super.getDescription()+ ", Gift Wrapped";
}

@Override
public double getCost() {
return super.getCost() + 200.0;
}
}


package org.example.design.structural.decoratorPattern;

public class WarrantyDecorator extends ProductDecorator{
public WarrantyDecorator(Product product) {
super(product);
}

@Override
public String getDescription() {
return super.getDescription()+ ", Warranty Extended";
}

@Override
public double getCost() {
return super.getCost() + 1200.0;
}
}

Main class - Decorator decide at run time 
package org.example.design.structural.decoratorPattern;

import java.util.Scanner;

public class EcommerceClientTest {
public static void main(String[] args) {

Scanner sc = new Scanner(System.in);

Product product= new Laptop();

// Dynamic add-ons
System.out.println("Do you want Gift Wrap? (yes/no)");
if(sc.next().equalsIgnoreCase("yes"))
{
product=new GiftWrapDecorator(product);
}


System.out.println("Do you want Extended Warranty? (yes/no)");
if (sc.next().equalsIgnoreCase("yes")) {
product = new WarrantyDecorator(product);
}

// Final product
System.out.println("\nFinal Order: " + product.getDescription());
System.out.println("Total Cost: " + product.getCost());
}
}

Do you want Gift Wrap? (yes/no)
yes
Do you want Extended Warranty? (yes/no)
yes
Do you want Personalization? (yes/no)
no

Final Order: Laptop, Gift Wrapped, Extended Warranty
Total Cost: ₹1900.0

Do you want Gift Wrap? (yes/no)
no
Do you want Extended Warranty? (yes/no)
no

Final Order: Laptop
Total Cost: 500.0


Do you want Gift Wrap? (yes/no)
yes
Do you want Extended Warranty? (yes/no)
no

Final Order: Laptop, Gift Wrapped
Total Cost: 700.0

Process finished with exit code 0


🚀 Why this is Real-World?

  • Customer choices decide which decorators are applied.

  • You don’t pre-define all combinations → system builds it dynamically.

  • Very similar to Amazon/Flipkart “Add-on features”.


                 🎭 Facade Design Pattern

📌 Problem

In large systems (like an e-commerce checkout process), a client may need to interact with multiple subsystems:

  • InventoryService

  • PaymentService

  • ShippingService

  • NotificationService

👉 If the client calls each of these directly:

  • The code becomes complex.

  • Changes in subsystems ripple into the client.


📌 Solution

Use a Facade → a single class that provides a simplified interface to the client.

  • The facade internally calls all subsystems in the right sequence.

  • The client just uses one unified method like placeOrder().


❌ Before Facade (Problem)


package org.example.design.stru.facade;

public class ClientBeforeFacade {

public static void main(String[] args) {

// Client talking to multiple services directly
InventoryService inventory = new InventoryService();
PaymentService payment = new PaymentService();
ShippingService shipping = new ShippingService();
NotificationService notification = new NotificationService();


// Too many steps exposed to client
if (inventory.checkStock("Laptop")) {
if (payment.makePayment("Laptop", 50000)) {
shipping.shipProduct("Laptop");
notification.sendConfirmation("Laptop");
}
}
}
}


👉 Problem: Client knows too much about order flow and dependencies.

 After Facade (Solution)

package org.example.design.stru.facade;

public class InventoryService {
public boolean checkStock(String Product) {
System.out.println("Checking stock for " + Product);
return true;
}
}

package org.example.design.stru.facade;

public class PaymentService {
public boolean makePayment(String Product, double amount) {
System.out.println("Processing payment of " + amount + " for " + Product);
return true;
}
}

package org.example.design.stru.facade;

public class ShippingService {
public void shipProduct(String Product) {
System.out.println("Shipping " + Product);
}
}


package org.example.design.stru.facade;

public class NotificationService {


public void sendConfirmation(String Product) {
System.out.println("Confirmation sent for " + Product);
}
}

package org.example.design.stru.facade;

public class FacadeClientAfterfacadeTest {
public static void main(String[] args) {
OrderFacade orderFacade=new OrderFacade();
orderFacade.placeOrder("Laptop",50000);
}
}


Output

Checking stock for Laptop
Processing payment of 50000.0 for Laptop
Shipping Laptop
Confirmation sent for Laptop
Order placed successfully


📌 In real-world e-commerce, OrderFacade could be a service layer class that orchestrates:

  • Inventory, Payment, Shipping, Notification services.

  • Client (like a Controller or API) just calls orderFacade.placeOrder().



🎯 Main Benefit of Facade Pattern

👉 It hides complexity and provides one simple entry point for the client.

Instead of the client knowing:

  • What order to call services in

  • How they depend on each other

  • Which validations are required

…the Facade does all the orchestration.
The client just calls one method (like placeOrder()).


❌ Without Facade (Bad Way)

Yes, in the non-facade world the client (UI / API caller) would have to manually:

  1. Call InventoryService.checkStock()

  2. Then call PaymentService.makePayment()

  3. Then call ShippingService.shipProduct()

  4. Then call NotificationService.sendConfirmation()

👉 That means the client is responsible for knowing the sequence and business flow.
If tomorrow you change the flow (e.g., payment before stock check), you’d have to change every client (mobile app, web app, admin panel). Painful ❌.


With Facade

The client just clicks “Place Order” (or calls orderFacade.placeOrder()).
The facade handles everything internally:

  • Checks stock

  • Processes payment

  • Ships product

  • Sends notification

👉 So the client doesn’t need to click step by step or know the order of operations.
It only does one actionplaceOrder().


🛒 Example in real life (Amazon):

  • You click “Place Order” once.

  • Amazon internally:

    • Reserves stock

    • Charges your card

    • Books courier

    • Sends SMS/email confirmation

You don’t click “Check Stock” → “Pay” → “Ship” → “Notify” separately.
That’s exactly what the Facade Pattern models.



🎭 Proxy Design Pattern

📌 Problem

Sometimes, we don’t want clients to access the real object directly because:

  • It may be expensive to create (e.g., huge object, remote connection).

  • We may want to control access (e.g., authentication, logging, caching).

  • We may want lazy loading (load only when needed).

👉 Example:
In a video streaming platform (Netflix/YouTube), a client should not directly load a heavy video file immediately. Instead, use a Proxy to control when/how the video is loaded.


🚀 Benefits of Proxy

Lazy loading → Load heavy objects only when needed.
Access control → Add security checks (like login required).
Logging / caching → Track usage or cache results.
Same interface → Client doesn’t know whether it’s using real object or proxy.






.................

Comments

Popular posts from this blog

Two Sum II - Input Array Is Sorted

Comparable Vs. Comparator in Java

Increasing Triplet Subsequence