B3.2.5 Explain commonly used design patterns in OOP. (HL only)

B3.2.5 Explain commonly used design patterns in OOP. 
• The key design patterns such as singleton, factory and observer 
• The application of design patterns in solving recurring programming challenges

The big idea

In software development, the same kinds of problems appear over and over:

  • “I need only one instance of this class.”
  • “I want to create objects, but without hard-coding the exact type.”
  • “I want different parts of my program to react automatically when something changes.”

Design patterns are reusable solutions to these common problems. They are not code you copy-paste, but templates for solving problems in a consistent, proven way.

 

Why design patterns matter

  • Consistency: Team members recognize patterns and understand the approach quickly.
  • Maintainability: Patterns separate concerns and make changes safer.
  • Extensibility: Patterns often support adding new features without rewriting existing code.
  • Scalability: Patterns encourage designs that handle growth in code size and complexity.

 

1. Singleton Pattern

Purpose: Ensure that only one instance of a class exists, and provide a global point of access to it.

When to use:

  • Configuration managers
  • Logging services
  • Connection pools

Key characteristics:

  • Private constructor (prevents creating new instances from outside).
  • A static method or property that returns the single instance.
  • Often thread-safe in multi-threaded environments.

Java example:

public class Logger {
    private static Logger instance;

    private Logger() {} // private constructor

    public static Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }

    public void log(String message) {
        System.out.println("[LOG] " + message);
    }
}

Benefits:

  • Guarantees a single point of control.
  • Saves memory when only one instance is needed.

Drawbacks:

  • Can hide dependencies, making testing harder.
  • If overused, can act like global variables (tight coupling).

 

2. Factory Pattern

Purpose: Create objects without specifying the exact class in the client code. The factory method decides which subclass or object to return.

When to use:

  • You need to create different types of related objects.
  • You want to avoid tightly coupling code to specific classes.

Key characteristics:

  • Encapsulates object creation logic.
  • Returns objects that share a common interface or base class.

Java example:

interface Shape {
    void draw();
}

class Circle implements Shape {
    public void draw() { System.out.println("Drawing Circle"); }
}

class Square implements Shape {
    public void draw() { System.out.println("Drawing Square"); }
}

class ShapeFactory {
    public Shape getShape(String shapeType) {
        if (shapeType.equalsIgnoreCase("CIRCLE")) return new Circle();
        if (shapeType.equalsIgnoreCase("SQUARE")) return new Square();
        return null;
    }
}

Benefits:

  • Isolates object creation, making code easier to modify or extend.
  • Reduces duplication of instantiation logic.

Drawbacks:

  • Adds an extra layer of abstraction.
  • Can become complex if many object types are supported.

 

3. Observer Pattern

Purpose: Set up a one-to-many relationship between objects so that when one object changes state, all its dependents are notified automatically.

When to use:

  • Event systems (e.g., GUI buttons and click listeners)
  • Publish–subscribe messaging
  • Live data updates in applications

Key characteristics:

  • Subject (Publisher): Maintains a list of observers and notifies them when something changes.
  • Observer (Subscriber): Defines an update method to be called when the subject changes.

Java example:

import java.util.*;

interface Observer {
    void update(String message);
}

class NewsAgency {
    private List<Observer> observers = new ArrayList<>();

    public void addObserver(Observer o) { observers.add(o); }
    public void removeObserver(Observer o) { observers.remove(o); }

    public void notifyObservers(String news) {
        for (Observer o : observers) {
            o.update(news);
        }
    }
}

class NewsChannel implements Observer {
    private String news;
    public void update(String news) {
        this.news = news;
        System.out.println("Breaking news: " + news);
    }
}

Benefits:

  • Promotes loose coupling between objects.
  • Makes it easy to add new observers without modifying the subject.

Drawbacks:

  • Can lead to unexpected update sequences if many observers are involved.
  • Requires careful management to avoid memory leaks (observers not removed).

 

Applying design patterns effectively

  1. Recognize the problem type – Is it about object creation, controlling access, or coordinating changes?
  2. Select the right pattern category
    • Creational: control how objects are made (e.g., Singleton, Factory).
    • Structural: define how classes and objects are composed (e.g., Adapter, Composite).
    • Behavioral: manage communication and responsibilities (e.g., Observer, Strategy).
  3. Adapt, don’t adopt blindly – Use patterns as guidance, but modify for your project’s specific requirements.
  4. Document the pattern use – Make it clear to future maintainers why you chose a certain pattern.