Understanding Software Design Patterns with Java Examples
Software design patterns are proven solutions to common programming problems. They provide a shared language for developers and help make code more maintainable, reusable, and scalable.
In this post, we'll explore three popular design patterns and see how they can be implemented in Java.
1. Singleton Pattern
Purpose: Ensure only one instance of a class exists and provide a global point of access.
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void showMessage(){
System.out.println("Hello from Singleton!");
}
}
// Usage
public class Main {
public static void main(String[] args) {
Singleton obj = Singleton.getInstance();
obj.showMessage();
}
}
When to use: Configuration managers, logging services, or any shared resource like database connection.
2. Factory Method Pattern
Purpose: Define an interface for creating objects but let subclasses decide which class to instantiate.
// Product Interface
interface Shape {
void draw();
}
// Concrete Products
class Circle implements Shape {
public void draw() {
System.out.println("Drawing Circle");
}
}
class Square implements Shape {
public void draw() {
System.out.println("Drawing Square");
}
}
// Factory Class
class ShapeFactory {
public Shape getShape(String shapeType) {
if (shapeType == null) return null;
if (shapeType.equalsIgnoreCase("CIRCLE")) return new Circle();
if (shapeType.equalsIgnoreCase("SQUARE")) return new Square();
return null;
}
}
// Usage
public class Main {
public static void main(String[] args) {
ShapeFactory factory = new ShapeFactory();
Shape shape1 = factory.getShape("CIRCLE");
shape1.draw();
Shape shape2 = factory.getShape("SQUARE");
shape2.draw();
}
}
When to use: Object creation logic is complex or needs to be decoupled from client code.
3. Observer Pattern
Purpose: Define a one-to-many dependency so that when one object changes state, all dependents are notified automatically.
import java.util.ArrayList;
import java.util.List;
// Observer Interface
interface Observer {
void update(String message);
}
// Subject Class
class Subject {
private List<Observer> observers = new ArrayList<>();
public void attach(Observer observer) {
observers.add(observer);
}
public void notifyAllObservers(String message) {
for (Observer o : observers) {
o.update(message);
}
}
}
// Concrete Observers
class EmailSubscriber implements Observer {
public void update(String message) {
System.out.println("Email received: " + message);
}
}
class SMSSubscriber implements Observer {
public void update(String message) {
System.out.println("SMS received: " + message);
}
}
// Usage
public class Main {
public static void main(String[] args) {
Subject topic = new Subject();
topic.attach(new EmailSubscriber());
topic.attach(new SMSSubscriber());
topic.notifyAllObservers("New article published!");
}
}
When to use: Event-driven systems, publish-subscribe systems, or GUIs.
Why Patterns Matter
- Reusability: Proven solutions for common problems.
- Scalability: Easier to extend without rewriting existing code.
- Maintainability: Clear structure and reduced complexity.
Happy Coding 💻