Exploring Structural Design Patterns in Java: From Adapter to Flyweight

Introduction:

In the realm of software design, Structural Design Patterns play a crucial role in organizing code that focuses on relationships between objects. These patterns are essential for achieving flexibility, modularity, and extensibility in complex systems. This article dives into several key Structural Design Patterns in Java, illustrating their definitions, use cases, and practical examples. Let's explore how these patterns can empower your software architecture and design decisions.

1. Adapter Pattern:

Definition: The Adapter Pattern allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by converting the interface of a class into another interface that a client expects.

When to Use:

  • When you want to reuse existing code with an interface that is not compatible with the rest of your system.
  • When you want to integrate legacy or third-party code into your system without modifying their interfaces.

Example:

/// Target Interface
public interface ModernPayment {
    void pay(String account, double amount);
}

// Adaptee
public class LegacyPaymentSystem {
    public void makePayment(String accountNumber, double amount) {
        System.out.println("Payment made to account " + accountNumber + " with amount " + amount);
    }
}

// Adapter
public class PaymentAdapter implements ModernPayment {
    private LegacyPaymentSystem legacyPaymentSystem;

    public PaymentAdapter(LegacyPaymentSystem legacyPaymentSystem) {
        this.legacyPaymentSystem = legacyPaymentSystem;
    }

    @Override
    public void pay(String account, double amount) {
        legacyPaymentSystem.makePayment(account, amount);
    }
}

// Spring Boot Application
@SpringBootApplication
public class AdapterPatternApplication {
    public static void main(String[] args) {
        SpringApplication.run(AdapterPatternApplication.class, args);

        LegacyPaymentSystem legacyPayment = new LegacyPaymentSystem();
        ModernPayment modernPayment = new PaymentAdapter(legacyPayment);

        modernPayment.pay("12345", 100.0);
    }
}        


Why Used: The Adapter Pattern allows you to integrate legacy or third-party code into your system without modifying their interfaces, thus promoting code reuse.


2. Bridge Pattern:

Definition: The Bridge Pattern separates an abstraction from its implementation so that both can vary independently. It decouples an abstraction from its implementation, allowing the two to vary independently.

When to Use:

  • When you want to avoid a permanent binding between an abstraction and its implementation.
  • When changes in the implementation need to be reflected without affecting the clients.

Example:

// Implementor
public interface PaymentGateway {
    void processPayment(double amount);
    void refundPayment(double amount);
}

// Concrete Implementors
public class PayPalGateway implements PaymentGateway {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing payment through PayPal: " + amount);
    }

    @Override
    public void refundPayment(double amount) {
        System.out.println("Refunding payment through PayPal: " + amount);
    }
}

public class StripeGateway implements PaymentGateway {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing payment through Stripe: " + amount);
    }

    @Override
    public void refundPayment(double amount) {
        System.out.println("Refunding payment through Stripe: " + amount);
    }
}

// Abstraction
public abstract class PaymentService {
    protected PaymentGateway paymentGateway;

    protected PaymentService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    public abstract void makePayment(double amount);
    public abstract void makeRefund(double amount);
}

// Refined Abstraction
public class OnlinePaymentService extends PaymentService {

    public OnlinePaymentService(PaymentGateway paymentGateway) {
        super(paymentGateway);
    }

    @Override
    public void makePayment(double amount) {
        paymentGateway.processPayment(amount);
    }

    @Override
    public void makeRefund(double amount) {
        paymentGateway.refundPayment(amount);
    }
}

// Spring Boot Application
@SpringBootApplication
public class BridgePatternPaymentApplication {

    public static void main(String[] args) {
        SpringApplication.run(BridgePatternPaymentApplication.class, args);

        PaymentGateway paypalGateway = new PayPalGateway();
        PaymentService paymentService = new OnlinePaymentService(paypalGateway);

        paymentService.makePayment(100.0);
        paymentService.makeRefund(50.0);

        PaymentGateway stripeGateway = new StripeGateway();
        paymentService = new OnlinePaymentService(stripeGateway);

        paymentService.makePayment(200.0);
        paymentService.makeRefund(100.0);
    }
}        

Why Used: The Bridge Pattern helps in managing complexity by decoupling abstraction from implementation, making it easier to extend and maintain both independently. It's particularly useful when you have multiple variations of both abstraction and implementation and want to avoid an explosion of subclasses.


3. Composite Pattern:

Definition: The Composite Pattern composes objects into tree structures to represent part-whole hierarchies. It allows clients to treat individual objects and compositions of objects uniformly.

When to Use:

  • When you need to represent part-whole hierarchies.
  • When clients should be able to ignore the difference between compositions of objects and individual objects.

Example:

// Component
public interface MenuComponent {
    void print();
}

// Leaf
public class MenuItem implements MenuComponent {
    private String name;
    private String description;
    private boolean vegetarian;
    private double price;

    public MenuItem(String name, String description, boolean vegetarian, double price) {
        this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }

    @Override
    public void print() {
        System.out.print("  " + name);
        if (vegetarian) {
            System.out.print("(v)");
        }
        System.out.println(", " + price);
        System.out.println("     -- " + description);
    }
}

// Composite
public class Menu implements MenuComponent {
    private List<MenuComponent> menuComponents = new ArrayList<>();
    private String name;
    private String description;

    public Menu(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }

    public MenuComponent getChild(int i) {
        return menuComponents.get(i);
    }

    @Override
    public void print() {
        System.out.print("\n" + name);
        System.out.println(", " + description);
        System.out.println("---------------------");

        for (MenuComponent menuComponent : menuComponents) {
            menuComponent.print();
        }
    }
}

// Spring Boot Application
@SpringBootApplication
public class CompositePatternApplication {
    public static void main(String[] args) {
        SpringApplication.run(CompositePatternApplication.class, args);

        MenuComponent pancakeHouseMenu = new Menu("PANCAKE HOUSE MENU", "Breakfast");
        MenuComponent dinerMenu = new Menu("DINER MENU", "Lunch");
        MenuComponent cafeMenu = new Menu("CAFE MENU", "Dinner");
        MenuComponent dessertMenu = new Menu("DESSERT MENU", "Dessert of course!");

        MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");

        allMenus.add(pancakeHouseMenu);
        allMenus.add(dinerMenu);
        allMenus.add(cafeMenu);

        dinerMenu.add(new MenuItem("Pasta", "Spaghetti with Marinara Sauce", true, 3.89));
        dinerMenu.add(dessertMenu);

        dessertMenu.add(new MenuItem("Apple Pie", "Apple pie with a flakey crust", true, 1.59));

        allMenus.print();
    }
}        

Why Used: The Composite Pattern simplifies the client code by allowing them to treat individual objects and compositions of objects uniformly. It's useful when you have objects that form tree-like structures and you want to perform operations on them uniformly.


4. Decorator Pattern:

Definition: The Decorator Pattern dynamically adds responsibilities to objects without altering their structure. It provides a flexible alternative to subclassing for extending functionality.

When to Use:

  • When you want to add functionality to objects dynamically without subclassing.
  • When you need to extend behavior without modifying existing code.

Example:

// Component
public interface Notification {
    String send();
}

// Concrete Component
public class EmailNotification implements Notification {
    @Override
    public String send() {
        return "Sending Email Notification";
    }
}

// Decorator
public abstract class NotificationDecorator implements Notification {
    protected Notification notification;

    public NotificationDecorator(Notification notification) {
        this.notification = notification;
    }

    public String send() {
        return notification.send();
    }
}

// Concrete Decorators
public class SMSNotification extends NotificationDecorator {
    public SMSNotification(Notification notification) {
        super(notification);
    }

    @Override
    public String send() {
        return notification.send() + " and SMS Notification";
    }
}

public class PushNotification extends NotificationDecorator {
    public PushNotification(Notification notification) {
        super(notification);
    }

    @Override
    public String send() {
        return notification.send() + " and Push Notification";
    }
}

// Spring Boot Application
@SpringBootApplication
public class DecoratorPatternApplication {
    public static void main(String[] args) {
        SpringApplication.run(DecoratorPatternApplication.class, args);

        Notification notification = new EmailNotification();
        System.out.println(notification.send());

        notification = new SMSNotification(notification);
        System.out.println(notification.send());

        notification = new PushNotification(notification);
        System.out.println(notification.send());
    }
}        


Why Used: The Decorator Pattern allows you to add responsibilities to objects dynamically without altering their structure. It promotes code reusability and flexibility.


5. Facade Pattern:

Definition: The Facade Pattern provides a unified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes the subsystem easier to use.

When to Use:

  • When you want to provide a simple interface to a complex subsystem.
  • When you need to decouple clients from the complexity of a subsystem.

Example:

// Subsystem classes
public class VideoFile {
    private String name;
    // Constructor, getters and setters
}

public class CodecFactory {
    public static String extract(VideoFile file) {
        // Logic to extract codec
        return "Extracted codec";
    }
}

public class BitrateReader {
    public static String read(String codec) {
        // Logic to read bitrate
        return "Read bitrate";
    }

    public static String convert(String codec) {
        // Logic to convert bitrate
        return "Converted bitrate";
    }
}

public class AudioMixer {
    public static String fix(String result) {
        // Logic to fix audio
        return "Audio fixed";
    }
}

// Facade
public class VideoConversionFacade {
    public String convertVideo(String fileName, String format) {
        VideoFile file = new VideoFile(fileName);
        String sourceCodec = CodecFactory.extract(file);
        String bitrate = BitrateReader.read(sourceCodec);
        String result = BitrateReader.convert(bitrate);
        result = AudioMixer.fix(result);
        return result;
    }
}

// Spring Boot Application
@SpringBootApplication
public class FacadePatternApplication {
    public static void main(String[] args) {
        SpringApplication.run(FacadePatternApplication.class, args);
        VideoConversionFacade converter = new VideoConversionFacade();
        String result = converter.convertVideo("example.mp4", "avi");
        System.out.println(result);
    }
}        

Why Used: The Facade Pattern simplifies the complexity of a subsystem by providing a higher-level interface. It promotes loose coupling and encapsulation.


6. Flyweight Pattern:

Definition: The Flyweight Pattern minimizes memory usage or computational expenses by sharing as much as possible with similar objects. It allows the use of shared objects to support large numbers of fine-grained objects efficiently.

When to Use:

  • When you need to support a large number of objects that share common properties.
  • When the overhead of creating individual objects is high.

Example:

// Flyweight Interface
public interface Shape {
    void draw(Graphics g, int x, int y, int width, int height);
}

// Concrete Flyweight
public class Circle implements Shape {
    private Color color;

    public Circle(Color color) {
        this.color = color;
    }

    @Override
    public void draw(Graphics g, int x, int y, int width, int height) {
        g.setColor(color);
        g.fillOval(x, y, width, height);
    }
}

// Flyweight Factory
public class ShapeFactory {
    private static final Map<Color, Shape> circleMap = new HashMap<>();

    public static Shape getCircle(Color color) {
        Circle circle = (Circle) circleMap.get(color);

        if (circle == null) {
            circle = new Circle(color);
            circleMap.put(color, circle);
            System.out.println("Creating circle of color : " + color);
        }
        return circle;
    }
}

// Spring Boot Application
@SpringBootApplication
public class FlyweightPatternApplication extends JFrame {
    private static final int WIDTH = 800;
    private static final int HEIGHT = 600;
    private static final Color[] colors = {Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW, Color.ORANGE, Color.CYAN, Color.MAGENTA};

    public FlyweightPatternApplication() {
        setTitle("Flyweight Pattern Example");
        setSize(WIDTH, HEIGHT);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
    }

    @Override
    public void paint(Graphics g) {
        for (int i = 0; i < 1000; i++) {
            Shape circle = ShapeFactory.getCircle(getRandomColor());
            circle.draw(g, getRandomX(), getRandomY(), 50, 50);
        }
    }

    private Color getRandomColor() {
        return colors[(int) (Math.random() * colors.length)];
    }

    private int getRandomX() {
        return (int) (Math.random() * WIDTH);
    }

    private int getRandomY() {
        return (int) (Math.random() * HEIGHT);
    }

    public static void main(String[] args) {
        SpringApplication.run(FlyweightPatternApplication.class, args);
        new FlyweightPatternApplication();
    }
}        

Why Used: The Flyweight Pattern reduces memory consumption and improves performance by sharing common state among multiple objects. It's suitable for situations where large numbers of objects need to be created.


Structural Design Patterns: Rules of Thumb

Adapter

  • Rule of Thumb: Adapter makes things work after they're designed; Bridge makes them work before they are.
  • Interface: Adapter provides a different interface to its subject. Proxy provides the same interface. Decorator provides an enhanced interface.

Bridge

  • Rule of Thumb: Bridge is designed up-front to let the abstraction and the implementation vary independently. Adapter is retrofitted to make unrelated classes work together.

Adapter & Decorator

  • Rule of Thumb: Adapter changes an object's interface, while Decorator enhances an object's responsibilities. Decorator is thus more transparent to the client. As a consequence, Decorator supports recursive composition, which isn't possible with pure Adapters.

Composite & Decorator

  • Rule of Thumb: Composite and Decorator have similar structure diagrams, reflecting the fact that both rely on recursive composition to organize an open-ended number of objects.

Composite

  • Traversal: Composite can be traversed with Iterator.
  • Visitor: Visitor can apply an operation over a Composite.
  • Chain of Responsibility: Composite could use Chain of Responsibility to let components access global properties through their parent.
  • Decorator: Composite could also use Decorator to override these properties on parts of the composition.
  • Observer: Composite could use Observer to tie one object structure to another and State to let a component change its behavior as its state changes.
  • Mediator: Composite can let you compose a Mediator out of smaller pieces through recursive composition.

Decorator & Strategy

  • Rule of Thumb: Decorator lets you change the skin of an object. Strategy lets you change the guts.

Decorator & Composite

  • Rule of Thumb: Decorator is designed to let you add responsibilities to objects without subclassing. Composite's focus is not on embellishment but on representation. These intents are distinct but complementary. Consequently, Composite and Decorator are often used in concert.

Decorator & Proxy

  • Rule of Thumb: Decorator and Proxy have different purposes but similar structures. Both describe how to provide a level of indirection to another object, and the implementations keep a reference to the object to which they forward requests.

Facade & Adapter

  • Rule of Thumb: Facade defines a new interface, whereas Adapter reuses an old interface. Remember that Adapter makes two existing interfaces work together as opposed to defining an entirely new one.

Facade

  • Singleton: Facade objects are often Singleton because only one Facade object is required.

Facade & Mediator

  • Rule of Thumb: Mediator is similar to Facade in that it abstracts functionality of existing classes. Mediator abstracts/centralizes arbitrary communication between colleague objects, routinely "adds value," and is known/referenced by the colleague objects. In contrast, Facade defines a simpler interface to a subsystem, doesn't add new functionality, and is not known by the subsystem classes.

Facade & Abstract Factory

  • Rule of Thumb: Abstract Factory can be used as an alternative to Facade to hide platform-specific classes.

Flyweight & Facade

  • Rule of Thumb: Whereas Flyweight shows how to make lots of little objects, Facade shows how to make a single object represent an entire subsystem.

Flyweight & Composite

  • Rule of Thumb: Flyweight is often combined with Composite to implement shared leaf nodes.

Flyweight

  • State Objects: Flyweight explains when and how State objects can be shared.

要查看或添加评论,请登录

Irfan Ahmed Mohammad的更多文章

  • Exploring Creational Design Patterns in Real-Life Applications: From Singleton to Prototype

    Exploring Creational Design Patterns in Real-Life Applications: From Singleton to Prototype

    Creational design patterns in Java provide ways to create objects while abstracting the instantiation process. This…

  • System Design Essentials: The Role of API Gateway

    System Design Essentials: The Role of API Gateway

    In the realm of system design interviews, understanding the pivotal role of API Gateways (AGs) is not just advantageous…

    2 条评论
  • System Design Essentials: The Role of Load Balancers

    System Design Essentials: The Role of Load Balancers

    In system design interviews, think of Load Balancers (LB) as gatekeepers at the entrance of a network. They're like…

    1 条评论
  • 268. Missing Number

    268. Missing Number

    Code: class Solution: def missingNumber(self, nums: List[int]) -> int: n = len(nums) res = n…

  • 190. Reverse Bits

    190. Reverse Bits

    Code class Solution: def reverseBits(self, n: int) - int: res = 0 for i in range(32):…

  • 191. Number of 1 Bits

    191. Number of 1 Bits

    Code class Solution: def hammingWeight(self, n: int) -> int: while n: n &= (n-1)…

  • 128. Longest Consecutive Sequence

    128. Longest Consecutive Sequence

    Code class Solution: def longestConsecutive(self, nums: List[int]) -> int: seen = set(nums) res = 0…

  • 238. Product of Array Except Self

    238. Product of Array Except Self

    Code class Solution: def productExceptSelf(self, nums: List[int]) -> List[int]: res = [1]*len(nums)…

  • 49. Group Anagrams

    49. Group Anagrams

    Approach 1 Using Sorted string as key class Solution def groupAnagrams(self, strs: List[str]) -> List[List[str]]:…

    1 条评论
  • 217. Contains Duplicate

    217. Contains Duplicate

    Code class Solution def containsDuplicate(self, nums: List[int]) -> bool: hash_set = set() for item…

社区洞察

其他会员也浏览了