Design Patterns in Java
Question
Explain the key design patterns in Java, focusing on their implementation, use cases, and best practices. Include examples of Singleton, Factory, and Observer patterns.
Answer
Design patterns are reusable solutions to common problems in software design. They provide proven approaches to solving specific design problems and help create more maintainable, flexible, and scalable code.
1. Singleton Pattern
The Singleton pattern ensures a class has only one instance and provides a global point of access to it.
Implementation:
public class Singleton {
// Private static instance
private static volatile Singleton instance;
// Private constructor to prevent instantiation
private Singleton() {}
// Public static method to get instance
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Thread-Safe Implementation:
public class ThreadSafeSingleton {
private static final ThreadSafeSingleton instance = new ThreadSafeSingleton();
private ThreadSafeSingleton() {}
public static ThreadSafeSingleton getInstance() {
return instance;
}
}
Use Cases:
public class ConfigurationManager {
private static ConfigurationManager instance;
private Properties properties;
private ConfigurationManager() {
properties = new Properties();
loadProperties();
}
public static ConfigurationManager getInstance() {
if (instance == null) {
synchronized (ConfigurationManager.class) {
if (instance == null) {
instance = new ConfigurationManager();
}
}
}
return instance;
}
public String getProperty(String key) {
return properties.getProperty(key);
}
}
2. Factory Pattern
The Factory pattern provides an interface for creating objects but lets subclasses decide which class to instantiate.
Simple Factory:
public interface Animal {
void makeSound();
}
public class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
public class Cat implements Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
}
public class AnimalFactory {
public static Animal createAnimal(String type) {
switch (type.toLowerCase()) {
case "dog":
return new Dog();
case "cat":
return new Cat();
default:
throw new IllegalArgumentException("Unknown animal type");
}
}
}
Factory Method:
public abstract class Creator {
public abstract Product factoryMethod();
public void operation() {
Product product = factoryMethod();
product.operation();
}
}
public class ConcreteCreator extends Creator {
@Override
public Product factoryMethod() {
return new ConcreteProduct();
}
}
Abstract Factory:
public interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
public class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
public class MacFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacButton();
}
@Override
public Checkbox createCheckbox() {
return new MacCheckbox();
}
}
3. Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Implementation:
public interface Observer {
void update(String message);
}
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
@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(news);
}
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
}
public class NewsChannel implements Observer {
private String name;
public NewsChannel(String name) {
this.name = name;
}
@Override
public void update(String news) {
System.out.println(name + " received news: " + news);
}
}
4. Builder Pattern
The Builder pattern separates the construction of a complex object from its representation.
Implementation:
public class Computer {
private String cpu;
private String ram;
private String storage;
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
}
public static class Builder {
private String cpu;
private String ram;
private String storage;
public Builder cpu(String cpu) {
this.cpu = cpu;
return this;
}
public Builder ram(String ram) {
this.ram = ram;
return this;
}
public Builder storage(String storage) {
this.storage = storage;
return this;
}
public Computer build() {
return new Computer(this);
}
}
}
5. Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Implementation:
public interface PaymentStrategy {
void pay(int amount);
}
public class CreditCardStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card");
}
}
public class PayPalStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal");
}
}
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
6. Decorator Pattern
The Decorator pattern attaches additional responsibilities to an object dynamically.
Implementation:
public interface Coffee {
double getCost();
String getDescription();
}
public class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 1;
}
@Override
public String getDescription() {
return "Simple coffee";
}
}
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost();
}
@Override
public String getDescription() {
return coffee.getDescription();
}
}
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return coffee.getCost() + 0.5;
}
@Override
public String getDescription() {
return coffee.getDescription() + ", milk";
}
}
Best Practices
-
Choose the Right Pattern
// Use Singleton for global configuration public class Configuration { private static Configuration instance; private Properties props; private Configuration() { loadProperties(); } public static Configuration getInstance() { if (instance == null) { instance = new Configuration(); } return instance; } } // Use Factory for object creation public class DocumentFactory { public static Document createDocument(String type) { switch (type) { case "PDF": return new PDFDocument(); case "Word": return new WordDocument(); default: throw new IllegalArgumentException(); } } }
-
Keep Patterns Simple
// Simple Observer implementation public class EventEmitter { private List<Consumer<String>> listeners = new ArrayList<>(); public void addListener(Consumer<String> listener) { listeners.add(listener); } public void emit(String event) { listeners.forEach(listener -> listener.accept(event)); } }
Common Pitfalls
-
Overusing Singleton
// Bad - Global state public class GlobalState { private static GlobalState instance; private Map<String, Object> state = new HashMap<>(); public static GlobalState getInstance() { if (instance == null) { instance = new GlobalState(); } return instance; } } // Better - Dependency Injection public class Service { private final Configuration config; public Service(Configuration config) { this.config = config; } }
-
Complex Factory Methods
// Bad - Too many parameters public class ComplexFactory { public static Product createProduct(String type, String color, int size, boolean isPremium) { // Complex creation logic } } // Better - Builder pattern public class ProductBuilder { private String type; private String color; private int size; private boolean isPremium; public ProductBuilder type(String type) { this.type = type; return this; } // ... other methods public Product build() { return new Product(type, color, size, isPremium); } }
Modern Java Features
-
Records for Data Transfer Objects
public record User(String name, String email, int age) {}
-
Sealed Classes for Type Safety
public sealed interface Shape permits Circle, Rectangle { double getArea(); } public record Circle(double radius) implements Shape { @Override public double getArea() { return Math.PI * radius * radius; } }