Backend & APIs Topic
What are Software Design Patterns?
Software Design Patterns are reusable solutions to common problems that occur in software design. They are templates for how to structure code to make it more maintainable, flexible, and understandable.
Key Characteristics:
-
Not finished code - they're templates or guidelines
-
Proven solutions to recurring problems
-
Language-agnostic - can be implemented in any OOP language
-
Improve code communication among developers
Categories of Design Patterns
1. Creational Patterns
Deal with object creation mechanisms.
2. Structural Patterns
Concerned with object composition and relationships.
3. Behavioral Patterns
Focus on communication between objects and responsibility assignment.
1. Creational Patterns
1.1 Singleton Pattern
Purpose: Ensure a class has only one instance and provide global access to it.
When to Use:
-
When you need exactly one instance of a class (database connections, logging, configuration)
-
When you need controlled access to a shared resource
Code Example:
java
public class DatabaseConnection {
// Static variable to hold the single instance
private static DatabaseConnection instance;
private Connection connection;
// Private constructor to prevent instantiation
private DatabaseConnection() {
try {
this.connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password");
} catch (SQLException e) {
throw new RuntimeException("Failed to create database connection", e);
}
}
// Public method to provide access to the instance
public static DatabaseConnection getInstance() {
if (instance == null) {
// Thread-safe implementation
synchronized (DatabaseConnection.class) {
if (instance == null) {
instance = new DatabaseConnection();
}
}
}
return instance;
}
public Connection getConnection() {
return connection;
}
}
// Usage
public class Application {
public static void main(String[] args) {
DatabaseConnection db1 = DatabaseConnection.getInstance();
DatabaseConnection db2 = DatabaseConnection.getInstance();
System.out.println(db1 == db2); // true - same instance
}
}
1.2 Factory Method Pattern
Purpose: Define an interface for creating objects, but let subclasses decide which class to instantiate.
When to Use:
-
When you don't know beforehand the exact types and dependencies of objects
-
When you want to provide a way to extend the internal components
-
When you want to delegate object creation to subclasses
Code Example:
java
// Product interface
public interface PaymentProcessor {
void processPayment(double amount);
boolean validate();
}
// Concrete Products
public class CreditCardProcessor implements PaymentProcessor {
private String cardNumber;
private String cvv;
public CreditCardProcessor(String cardNumber, String cvv) {
this.cardNumber = cardNumber;
this.cvv = cvv;
}
@Override
public void processPayment(double amount) {
System.out.println("Processing credit card payment of $" + amount);
// Actual credit card processing logic
}
@Override
public boolean validate() {
return cardNumber != null && cardNumber.length() == 16 && cvv != null && cvv.length() == 3;
}
}
public class PayPalProcessor implements PaymentProcessor {
private String email;
public PayPalProcessor(String email) {
this.email = email;
}
@Override
public void processPayment(double amount) {
System.out.println("Processing PayPal payment of $" + amount + " for " + email);
// Actual PayPal processing logic
}
@Override
public boolean validate() {
return email != null && email.contains("@");
}
}
// Creator abstract class
public abstract class PaymentProcessorFactory {
// Factory method
public abstract PaymentProcessor createPaymentProcessor(String... parameters);
// Common business logic
public void processOrder(double amount, String... parameters) {
PaymentProcessor processor = createPaymentProcessor(parameters);
if (processor.validate()) {
processor.processPayment(amount);
System.out.println("Payment processed successfully!");
} else {
System.out.println("Payment validation failed!");
}
}
}
// Concrete Creators
public class CreditCardProcessorFactory extends PaymentProcessorFactory {
@Override
public PaymentProcessor createPaymentProcessor(String... parameters) {
return new CreditCardProcessor(parameters[0], parameters[1]); // cardNumber, cvv
}
}
public class PayPalProcessorFactory extends PaymentProcessorFactory {
@Override
public PaymentProcessor createPaymentProcessor(String... parameters) {
return new PayPalProcessor(parameters[0]); // email
}
}
// Usage
public class ECommerceApp {
public static void main(String[] args) {
PaymentProcessorFactory creditCardFactory = new CreditCardProcessorFactory();
creditCardFactory.processOrder(100.0, "1234567812345678", "123");
PaymentProcessorFactory paypalFactory = new PayPalProcessorFactory();
paypalFactory.processOrder(75.50, "user@example.com");
}
}
1.3 Builder Pattern
Purpose: Construct complex objects step by step, separating construction from representation.
When to Use:
-
When creating objects with many optional parameters
-
When the construction process should allow different representations
-
When you want to create immutable objects
Code Example:
java
public class Computer {
// Required parameters
private final String CPU;
private final String RAM;
// Optional parameters
private final String storage;
private final String graphicsCard;
private final String motherboard;
private final boolean isBluetoothEnabled;
private final boolean isWifiEnabled;
// Private constructor
private Computer(ComputerBuilder builder) {
this.CPU = builder.CPU;
this.RAM = builder.RAM;
this.storage = builder.storage;
this.graphicsCard = builder.graphicsCard;
this.motherboard = builder.motherboard;
this.isBluetoothEnabled = builder.isBluetoothEnabled;
this.isWifiEnabled = builder.isWifiEnabled;
}
// Getters
public String getCPU() { return CPU; }
public String getRAM() { return RAM; }
public String getStorage() { return storage; }
public String getGraphicsCard() { return graphicsCard; }
public String getMotherboard() { return motherboard; }
public boolean isBluetoothEnabled() { return isBluetoothEnabled; }
public boolean isWifiEnabled() { return isWifiEnabled; }
// Builder class
public static class ComputerBuilder {
// Required parameters
private final String CPU;
private final String RAM;
// Optional parameters - initialized to default values
private String storage = "256GB SSD";
private String graphicsCard = "Integrated";
private String motherboard = "Standard";
private boolean isBluetoothEnabled = false;
private boolean isWifiEnabled = false;
public ComputerBuilder(String CPU, String RAM) {
this.CPU = CPU;
this.RAM = RAM;
}
public ComputerBuilder setStorage(String storage) {
this.storage = storage;
return this;
}
public ComputerBuilder setGraphicsCard(String graphicsCard) {
this.graphicsCard = graphicsCard;
return this;
}
public ComputerBuilder setMotherboard(String motherboard) {
this.motherboard = motherboard;
return this;
}
public ComputerBuilder enableBluetooth() {
this.isBluetoothEnabled = true;
return this;
}
public ComputerBuilder enableWifi() {
this.isWifiEnabled = true;
return this;
}
public Computer build() {
return new Computer(this);
}
}
}
// Usage
public class ComputerShop {
public static void main(String[] args) {
// Building a basic computer
Computer basicComputer = new Computer.ComputerBuilder("Intel i5", "8GB")
.build();
// Building a gaming computer with all features
Computer gamingComputer = new Computer.ComputerBuilder("Intel i9", "32GB")
.setStorage("1TB NVMe SSD")
.setGraphicsCard("NVIDIA RTX 4080")
.setMotherboard("Gaming Motherboard")
.enableBluetooth()
.enableWifi()
.build();
// Building a computer with specific requirements
Computer officeComputer = new Computer.ComputerBuilder("Intel i7", "16GB")
.setStorage("512GB SSD")
.enableWifi()
.build();
}
}
2. Structural Patterns
2.1 Adapter Pattern
Purpose: Convert the interface of a class into another interface clients expect.
When to Use:
-
When you want to use an existing class but its interface doesn't match what you need
-
When you want to create reusable classes that cooperate with unrelated classes
Code Example:
java
// Target interface (what the client expects)
public interface ModernPrinter {
void printDocument(String document);
void scanDocument();
void faxDocument(String document);
}
// Adaptee (existing class with incompatible interface)
public class LegacyPrinter {
public void print(String text) {
System.out.println("Legacy Printing: " + text);
}
public void scan() {
System.out.println("Legacy Scanning...");
}
// Note: No fax capability in legacy printer
}
// Adapter
public class PrinterAdapter implements ModernPrinter {
private LegacyPrinter legacyPrinter;
public PrinterAdapter(LegacyPrinter legacyPrinter) {
this.legacyPrinter = legacyPrinter;
}
@Override
public void printDocument(String document) {
legacyPrinter.print(document);
}
@Override
public void scanDocument() {
legacyPrinter.scan();
}
@Override
public void faxDocument(String document) {
// Legacy printer doesn't support fax, so we simulate it
System.out.println("Adapter: Fax simulation for - " + document);
System.out.println("Document sent via email instead of fax");
}
}
// Client code
public class Office {
public static void main(String[] args) {
LegacyPrinter oldPrinter = new LegacyPrinter();
ModernPrinter adaptedPrinter = new PrinterAdapter(oldPrinter);
// Client can use the modern interface
adaptedPrinter.printDocument("Important Report");
adaptedPrinter.scanDocument();
adaptedPrinter.faxDocument("Urgent Memo");
}
}
2.2 Decorator Pattern
Purpose: Attach additional responsibilities to an object dynamically.
When to Use:
-
When you want to add responsibilities to individual objects without affecting others
-
When extension by subclassing is impractical
Code Example:
java
// Component interface
public interface Coffee {
String getDescription();
double getCost();
}
// Concrete Component
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double getCost() {
return 2.0;
}
}
// Decorator abstract class
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
}
// Concrete Decorators
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Milk";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.5;
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Sugar";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.2;
}
}
public class WhippedCreamDecorator extends CoffeeDecorator {
public WhippedCreamDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription() + ", Whipped Cream";
}
@Override
public double getCost() {
return decoratedCoffee.getCost() + 0.7;
}
}
// Usage
public class CoffeeShop {
public static void main(String[] args) {
// Simple coffee
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " $" + coffee.getCost());
// Coffee with milk and sugar
Coffee milkSugarCoffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
System.out.println(milkSugarCoffee.getDescription() + " $" + milkSugarCoffee.getCost());
// Fancy coffee with everything
Coffee fancyCoffee = new WhippedCreamDecorator(
new SugarDecorator(
new MilkDecorator(
new SimpleCoffee())));
System.out.println(fancyCoffee.getDescription() + " $" + fancyCoffee.getCost());
}
}
3. Behavioral Patterns
3.1 Observer Pattern
Purpose: Define a one-to-many dependency between objects so when one changes state, all dependents are notified.
When to Use:
-
When a change to one object requires changing others, and you don't know how many need to be changed
-
When an object should be able to notify others without knowing who they are
Code Example:
java
import java.util.ArrayList;
import java.util.List;
// Subject interface
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
// Concrete Subject
public class WeatherStation implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherStation() {
this.observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
private void measurementsChanged() {
notifyObservers();
}
}
// Observer interface
public interface Observer {
void update(float temperature, float humidity, float pressure);
}
// Display element interface
public interface DisplayElement {
void display();
}
// Concrete Observers
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private Subject weatherData;
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
@Override
public void display() {
System.out.println("Current conditions: " + temperature + "°C and " + humidity + "% humidity");
}
}
public class StatisticsDisplay implements Observer, DisplayElement {
private List<Float> temperatures;
private Subject weatherData;
public StatisticsDisplay(Subject weatherData) {
this.weatherData = weatherData;
this.temperatures = new ArrayList<>();
weatherData.registerObserver(this);
}
@Override
public void update(float temperature, float humidity, float pressure) {
temperatures.add(temperature);
display();
}
@Override
public void display() {
float avg = (float) temperatures.stream().mapToDouble(Float::doubleValue).average().orElse(0.0);
float max = (float) temperatures.stream().mapToDouble(Float::doubleValue).max().orElse(0.0);
float min = (float) temperatures.stream().mapToDouble(Float::doubleValue).min().orElse(0.0);
System.out.println("Avg/Max/Min temperature: " + avg + "/" + max + "/" + min);
}
}
// Usage
public class WeatherApp {
public static void main(String[] args) {
WeatherStation weatherStation = new WeatherStation();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherStation);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherStation);
// Simulate weather changes
weatherStation.setMeasurements(25.0f, 65.0f, 1013.1f);
weatherStation.setMeasurements(26.5f, 70.0f, 1012.5f);
weatherStation.setMeasurements(24.0f, 90.0f, 1014.0f);
}
}
3.2 Strategy Pattern
Purpose: Define a family of algorithms, encapsulate each one, and make them interchangeable.
When to Use:
-
When you have multiple variations of an algorithm
-
When you want to avoid exposing complex, algorithm-specific data structures
-
When a class defines many behaviors that appear as multiple conditional statements
Code Example:
java
// Strategy interface
public interface PaymentStrategy {
boolean pay(double amount);
boolean validate();
}
// Concrete Strategies
public class CreditCardStrategy implements PaymentStrategy {
private String cardNumber;
private String cvv;
private String expiryDate;
public CreditCardStrategy(String cardNumber, String cvv, String expiryDate) {
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expiryDate = expiryDate;
}
@Override
public boolean pay(double amount) {
System.out.println("Paid " + amount + " using Credit Card");
// Actual credit card processing logic
return true;
}
@Override
public boolean validate() {
return cardNumber != null && cardNumber.length() == 16 &&
cvv != null && cvv.length() == 3 &&
expiryDate != null;
}
}
public class PayPalStrategy implements PaymentStrategy {
private String email;
private String password;
public PayPalStrategy(String email, String password) {
this.email = email;
this.password = password;
}
@Override
public boolean pay(double amount) {
System.out.println("Paid " + amount + " using PayPal");
// Actual PayPal processing logic
return true;
}
@Override
public boolean validate() {
return email != null && email.contains("@") &&
password != null && password.length() >= 6;
}
}
public class CryptoStrategy implements PaymentStrategy {
private String walletAddress;
public CryptoStrategy(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public boolean pay(double amount) {
System.out.println("Paid " + amount + " using Cryptocurrency");
// Actual cryptocurrency processing logic
return true;
}
@Override
public boolean validate() {
return walletAddress != null && walletAddress.startsWith("0x") && walletAddress.length() == 42;
}
}
// Context class
public class PaymentProcessor {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public boolean processPayment(double amount) {
if (paymentStrategy == null) {
System.out.println("No payment strategy set!");
return false;
}
if (!paymentStrategy.validate()) {
System.out.println("Payment validation failed!");
return false;
}
return paymentStrategy.pay(amount);
}
}
// Usage
public class ShoppingCart {
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor();
// Pay with Credit Card
processor.setPaymentStrategy(new CreditCardStrategy("1234567812345678", "123", "12/25"));
processor.processPayment(100.0);
// Pay with PayPal
processor.setPaymentStrategy(new PayPalStrategy("user@example.com", "password123"));
processor.processPayment(75.50);
// Pay with Cryptocurrency
processor.setPaymentStrategy(new CryptoStrategy("0x742d35Cc6634C0532925a3b8D"));
processor.processPayment(200.0);
}
}
Summary Table
| Pattern | Category | Purpose | When to Use |
|---|---|---|---|
| Singleton | Creational | Ensure one instance | Logging, DB connections, config |
| Factory Method | Creational | Delegate instantiation | When object type isn't known beforehand |
| Builder | Creational | Construct complex objects | Many optional parameters, immutable objects |
| Adapter | Structural | Make interfaces compatible | Integrating legacy code, incompatible interfaces |
| Decorator | Structural | Add responsibilities dynamically | When subclassing is impractical |
| Observer | Behavioral | One-to-many dependency | Event handling, publish-subscribe scenarios |
| Strategy | Behavioral | Encapsulate algorithms | Multiple algorithm variations, avoid conditionals |
These patterns help create more maintainable, flexible, and understandable code by providing proven solutions to common design problems.
Comments (0)
No comments yet. Be the first to comment!