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:
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:
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:
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:
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:
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:
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
Bridge
Adapter & Decorator
Composite & Decorator
Composite
Decorator & Strategy
Decorator & Composite
Decorator & Proxy
Facade & Adapter
Facade
Facade & Mediator
Facade & Abstract Factory
Flyweight & Facade
Flyweight & Composite
Flyweight