Index

Object-Oriented Design Patterns (Essentials)

Java Object-Oriented Design

14.1 What Are Design Patterns?

In software engineering, design patterns are proven, general-purpose solutions to common problems encountered in object-oriented design. Rather than providing finished code, a design pattern offers a template or blueprint for solving a particular issue in a flexible, reusable way. Just as architects use blueprints to design buildings, developers use design patterns to structure software systems that are scalable, maintainable, and robust.

The Origin: The Gang of Four

The concept of design patterns in software was popularized by the 1994 book Design Patterns: Elements of Reusable Object-Oriented Software, authored by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides—collectively known as the Gang of Four (GoF). Their book introduced 23 classic design patterns and grouped them into three major categories:

  1. Creational – Deal with object creation mechanisms.
  2. Structural – Focus on class and object composition.
  3. Behavioral – Concerned with communication between objects.

These patterns have since become foundational knowledge for object-oriented developers and are widely taught and applied in modern software engineering.

Why Use Design Patterns?

Design patterns bring multiple advantages to the development process:

When used appropriately, design patterns lead to more readable, modular, and adaptable codebases, which are easier to debug, refactor, and test.

The Three Main Categories of Patterns

Creational Patterns

These deal with object instantiation in ways that promote flexibility and reuse. Instead of instantiating classes directly, creational patterns abstract the instantiation process. Examples include:

Structural Patterns

These focus on how classes and objects are composed to form larger structures. Structural patterns help ensure that components are organized efficiently and can work together. Examples include:

Behavioral Patterns

These deal with object interaction and responsibility. Behavioral patterns help manage communication and control flow among objects. Examples include:

A Simple Example: The Singleton Pattern

One of the simplest design patterns is Singleton, which ensures that a class has only one instance and provides a global point of access to it. This is useful for managing shared resources such as a configuration manager or database connection.

public class ConfigurationManager {
    private static ConfigurationManager instance;

    private ConfigurationManager() { }

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

With Singleton, the getInstance() method always returns the same instance, ensuring consistency across the application.

Conclusion

Design patterns are essential tools in the object-oriented programmer’s toolbox. Far from being rigid formulas, they are flexible, proven strategies that help manage complexity and promote sound architectural decisions. By learning and applying design patterns thoughtfully, Java developers can write code that is easier to understand, reuse, and maintain—qualities that are critical in modern software development.

In the following sections, we’ll explore specific patterns from each category, how they’re implemented in Java, and when to apply them in real-world design scenarios.

Index

14.2 Creational Patterns: Singleton, Factory, Builder

Creational design patterns focus on how objects are created. They abstract the instantiation process, allowing systems to be more flexible and independent of the way objects are created, composed, and represented. In this section, we explore three essential creational patterns in Java: Singleton, Factory, and Builder.

Singleton Pattern

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

Use Cases

Java Example

public class Logger {
    private static Logger instance;

    private Logger() {
        // private constructor prevents instantiation
    }

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

    public void log(String message) {
        System.out.println("Log: " + message);
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        Logger logger = Logger.getInstance();
        logger.log("Application started.");
    }
}

Pros

Cons

Best Practice: Use lazy-loaded, thread-safe variants (e.g., using synchronized, or static inner class).

Factory Method Pattern

Purpose: Define an interface for creating an object but allow subclasses to alter the type of objects that will be created. The Factory Method lets a class defer instantiation to subclasses or encapsulate logic in a factory class.

Use Cases

Java Example

// Product interface
interface Notification {
    void notifyUser();
}

// Concrete implementations
class EmailNotification implements Notification {
    public void notifyUser() {
        System.out.println("Sending Email Notification");
    }
}

class SMSNotification implements Notification {
    public void notifyUser() {
        System.out.println("Sending SMS Notification");
    }
}

// Factory
class NotificationFactory {
    public static Notification createNotification(String type) {
        if (type.equalsIgnoreCase("EMAIL")) {
            return new EmailNotification();
        } else if (type.equalsIgnoreCase("SMS")) {
            return new SMSNotification();
        }
        throw new IllegalArgumentException("Unknown notification type");
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        Notification notification = NotificationFactory.createNotification("SMS");
        notification.notifyUser();
    }
}

Pros

Cons

Best Practice: Keep factory logic clean, consider abstract factories for families of related objects.

Builder Pattern

Purpose: Separate the construction of a complex object from its representation, so the same construction process can create different representations.

Use Cases

Java Example

public class Computer {
    // Required parameters
    private final String processor;
    private final int ram;

    // Optional parameters
    private final boolean graphicsCard;
    private final boolean bluetooth;

    private Computer(Builder builder) {
        this.processor = builder.processor;
        this.ram = builder.ram;
        this.graphicsCard = builder.graphicsCard;
        this.bluetooth = builder.bluetooth;
    }

    public static class Builder {
        private final String processor;
        private final int ram;

        private boolean graphicsCard = false;
        private boolean bluetooth = false;

        public Builder(String processor, int ram) {
            this.processor = processor;
            this.ram = ram;
        }

        public Builder graphicsCard(boolean value) {
            this.graphicsCard = value;
            return this;
        }

        public Builder bluetooth(boolean value) {
            this.bluetooth = value;
            return this;
        }

        public Computer build() {
            return new Computer(this);
        }
    }

    public void displayConfig() {
        System.out.println("Processor: " + processor + ", RAM: " + ram +
            "GB, Graphics Card: " + graphicsCard + ", Bluetooth: " + bluetooth);
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        Computer customPC = new Computer.Builder("Intel i7", 16)
                .graphicsCard(true)
                .bluetooth(true)
                .build();
        customPC.displayConfig();
    }
}
Click to view full runnable Code

public class Main {
    public static void main(String[] args) {
        Computer customPC = new Computer.Builder("Intel i7", 16)
                .graphicsCard(true)
                .bluetooth(true)
                .build();
        customPC.displayConfig();
    }
}

class Computer {
    // Required parameters
    private final String processor;
    private final int ram;

    // Optional parameters
    private final boolean graphicsCard;
    private final boolean bluetooth;

    private Computer(Builder builder) {
        this.processor = builder.processor;
        this.ram = builder.ram;
        this.graphicsCard = builder.graphicsCard;
        this.bluetooth = builder.bluetooth;
    }

    public static class Builder {
        private final String processor;
        private final int ram;

        private boolean graphicsCard = false;
        private boolean bluetooth = false;

        public Builder(String processor, int ram) {
            this.processor = processor;
            this.ram = ram;
        }

        public Builder graphicsCard(boolean value) {
            this.graphicsCard = value;
            return this;
        }

        public Builder bluetooth(boolean value) {
            this.bluetooth = value;
            return this;
        }

        public Computer build() {
            return new Computer(this);
        }
    }

    public void displayConfig() {
        System.out.println("Processor: " + processor + ", RAM: " + ram +
                "GB, Graphics Card: " + graphicsCard + ", Bluetooth: " + bluetooth);
    }
}

Pros

Cons

Best Practice: Use for complex objects or those with many optional parameters (e.g., configuration, GUI components, data transfer objects).

Choosing the Right Pattern

Pattern Best For Avoid When...
Singleton Single shared instance Multiple instances are required
Factory Flexible, decoupled object creation Object creation is trivial
Builder Complex object construction Only simple constructors are needed

Conclusion

Creational patterns offer flexible and reusable solutions to object creation problems in Java. Whether you need controlled instantiation (Singleton), decoupled creation logic (Factory), or step-by-step configuration (Builder), these patterns help build more modular, testable, and maintainable systems. By applying the appropriate creational pattern in the right context, developers can write cleaner and more expressive code that scales gracefully as complexity grows.

Index

14.3 Structural Patterns: Adapter, Decorator, Composite

Structural design patterns are focused on how classes and objects are composed to form larger structures. These patterns simplify the relationships between entities, allowing developers to create flexible and extensible systems by enabling different parts of a program to work together more effectively.

This section covers three widely used structural patterns in Java: Adapter, Decorator, and Composite.

Adapter Pattern

Intent: Convert the interface of a class into another interface that clients expect. The Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.

Use Case

When integrating legacy code or third-party libraries that don’t match your current interface expectations.

Example Scenario

Suppose you have a legacy AudioPlayer that plays .mp3 files, and a new media library supports .mp4. You want your system to adapt the new format without modifying the existing player.

Java Example

// Existing interface
interface MediaPlayer {
    void play(String audioType, String fileName);
}

// New library with a different interface
class AdvancedMediaPlayer {
    public void playMp4(String fileName) {
        System.out.println("Playing MP4 file: " + fileName);
    }
}

// Adapter to bridge both
class MediaAdapter implements MediaPlayer {
    private AdvancedMediaPlayer advancedPlayer = new AdvancedMediaPlayer();

    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("mp4")) {
            advancedPlayer.playMp4(fileName);
        } else {
            System.out.println("Unsupported format: " + audioType);
        }
    }
}

// Client
class AudioPlayer implements MediaPlayer {
    private MediaAdapter adapter = new MediaAdapter();

    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing MP3 file: " + fileName);
        } else {
            adapter.play(audioType, fileName);
        }
    }
}

Usage:

AudioPlayer player = new AudioPlayer();
player.play("mp3", "song.mp3");
player.play("mp4", "video.mp4");

Benefits

Decorator Pattern

Intent: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Use Case

When you want to add functionality to objects without changing their class or creating a large inheritance hierarchy.

Java Example

// Core interface
interface Coffee {
    String getDescription();
    double cost();
}

// Concrete component
class SimpleCoffee implements Coffee {
    public String getDescription() { return "Simple Coffee"; }
    public double cost() { return 5.0; }
}

// Decorator base class
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;
    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }
}

// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Milk";
    }

    public double cost() {
        return decoratedCoffee.cost() + 1.5;
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Sugar";
    }

    public double cost() {
        return decoratedCoffee.cost() + 0.5;
    }
}

Usage:

Coffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);

System.out.println(coffee.getDescription()); // Simple Coffee, Milk, Sugar
System.out.println("Cost: $" + coffee.cost()); // Cost: $7.0

Benefits

Click to view full runnable Code

// Core interface
interface Coffee {
    String getDescription();
    double cost();
}

// Concrete component
class SimpleCoffee implements Coffee {
    public String getDescription() { return "Simple Coffee"; }
    public double cost() { return 5.0; }
}

// Decorator base class
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;
    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }
}

// Concrete decorators
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Milk";
    }

    public double cost() {
        return decoratedCoffee.cost() + 1.5;
    }
}

class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Sugar";
    }

    public double cost() {
        return decoratedCoffee.cost() + 0.5;
    }
}

public class DecoratorDemo {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new MilkDecorator(coffee);
        coffee = new SugarDecorator(coffee);

        System.out.println(coffee.getDescription()); // Simple Coffee, Milk, Sugar
        System.out.println("Cost: $" + coffee.cost()); // Cost: $7.0
    }
}

Composite Pattern

Intent: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions uniformly.

Use Case

When building hierarchies such as graphical components (buttons inside panels), file systems (folders containing files), or organizational charts.

Java Example

// Component
interface FileSystem {
    void ls();
}

// Leaf
class File implements FileSystem {
    private String name;
    public File(String name) { this.name = name; }

    public void ls() {
        System.out.println("File: " + name);
    }
}

// Composite
class Directory implements FileSystem {
    private String name;
    private List<FileSystem> contents = new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

    public void add(FileSystem fs) {
        contents.add(fs);
    }

    public void ls() {
        System.out.println("Directory: " + name);
        for (FileSystem fs : contents) {
            fs.ls();
        }
    }
}

Usage:

FileSystem file1 = new File("readme.txt");
FileSystem file2 = new File("data.csv");
Directory dir = new Directory("MyDocs");
dir.add(file1);
dir.add(file2);

Directory root = new Directory("Root");
root.add(dir);
root.ls();

Output:

Directory: Root
Directory: MyDocs
File: readme.txt
File: data.csv
Click to view full runnable Code

import java.util.ArrayList;
import java.util.List;

// Component
interface FileSystem {
    void ls();
}

// Leaf
class File implements FileSystem {
    private String name;
    public File(String name) { this.name = name; }

    public void ls() {
        System.out.println("File: " + name);
    }
}

// Composite
class Directory implements FileSystem {
    private String name;
    private List<FileSystem> contents = new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

    public void add(FileSystem fs) {
        contents.add(fs);
    }

    public void ls() {
        System.out.println("Directory: " + name);
        for (FileSystem fs : contents) {
            fs.ls();
        }
    }
}

public class CompositeDemo {
    public static void main(String[] args) {
        FileSystem file1 = new File("readme.txt");
        FileSystem file2 = new File("data.csv");
        Directory dir = new Directory("MyDocs");
        dir.add(file1);
        dir.add(file2);

        Directory root = new Directory("Root");
        root.add(dir);

        root.ls();
    }
}

Benefits

Summary

Pattern Intent When to Use
Adapter Convert incompatible interfaces Bridging legacy code or APIs
Decorator Add behavior dynamically Customize functionality without changing class
Composite Treat groups and objects uniformly Model tree-like structures (e.g., UI, filesystems)

Conclusion

Structural design patterns like Adapter, Decorator, and Composite offer powerful tools for organizing code, reducing coupling, and enhancing system flexibility. These patterns help developers build systems that are easier to extend, maintain, and scale—core goals of object-oriented design. By mastering these patterns, Java developers can craft more modular, adaptable, and reusable code architectures.

Index

14.4 Behavioral Patterns: Strategy, Observer, Command

Behavioral design patterns are concerned with how objects interact and communicate to fulfill responsibilities. These patterns increase flexibility in carrying out behaviors and decouple sender and receiver objects. In this section, we’ll explore three widely used behavioral patterns in Java: Strategy, Observer, and Command.

Strategy Pattern

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy allows an algorithm’s behavior to be selected at runtime.

Use Case

When different algorithms can be applied interchangeably without altering the context class using them—e.g., sorting strategies or payment methods.

Java Example

// Strategy interface
interface PaymentStrategy {
    void pay(int amount);
}

// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid $" + amount + " using Credit Card.");
    }
}

class PayPalPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid $" + amount + " using PayPal.");
    }
}

// Context
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}

Usage:

ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(100); // Paid $100 using PayPal
Click to view full runnable Code

// Strategy interface
interface PaymentStrategy {
    void pay(int amount);
}

// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid $" + amount + " using Credit Card.");
    }
}

class PayPalPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("Paid $" + amount + " using PayPal.");
    }
}

// Context
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(int amount) {
        if (paymentStrategy == null) {
            System.out.println("Payment strategy not set!");
        } else {
            paymentStrategy.pay(amount);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        cart.setPaymentStrategy(new PayPalPayment());
        cart.checkout(100); // Output: Paid $100 using PayPal

        cart.setPaymentStrategy(new CreditCardPayment());
        cart.checkout(250); // Output: Paid $250 using Credit Card
    }
}

Benefits

Observer Pattern

Intent: Define a one-to-many dependency so that when one object changes state, all its dependents are notified and updated automatically.

Use Case

Widely used in event-driven systems, GUIs, and model-view-controller (MVC) frameworks—e.g., updating multiple UI components when data changes.

Java Example

// Observer interface
interface Observer {
    void update(String message);
}

// Subject interface
interface Subject {
    void attach(Observer o);
    void detach(Observer o);
    void notifyObservers();
}

// Concrete subject
class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String news;

    public void setNews(String news) {
        this.news = news;
        notifyObservers();
    }

    public void attach(Observer o) { observers.add(o); }
    public void detach(Observer o) { observers.remove(o); }

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

// Concrete observer
class NewsChannel implements Observer {
    private String name;

    public NewsChannel(String name) {
        this.name = name;
    }

    public void update(String message) {
        System.out.println(name + " received: " + message);
    }
}

Usage:

NewsAgency agency = new NewsAgency();
agency.attach(new NewsChannel("Channel A"));
agency.attach(new NewsChannel("Channel B"));
agency.setNews("Breaking: Strategy Pattern Deployed!");

Output:

Channel A received: Breaking: Strategy Pattern Deployed!
Channel B received: Breaking: Strategy Pattern Deployed!
Click to view full runnable Code

import java.util.ArrayList;
import java.util.List;

// Observer interface
interface Observer {
    void update(String message);
}

// Subject interface
interface Subject {
    void attach(Observer o);
    void detach(Observer o);
    void notifyObservers();
}

// Concrete subject
class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String news;

    public void setNews(String news) {
        this.news = news;
        notifyObservers();
    }

    public void attach(Observer o) { observers.add(o); }
    public void detach(Observer o) { observers.remove(o); }

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

// Concrete observer
class NewsChannel implements Observer {
    private String name;

    public NewsChannel(String name) {
        this.name = name;
    }

    public void update(String message) {
        System.out.println(name + " received: " + message);
    }
}

public class ObserverDemo {
    public static void main(String[] args) {
        NewsAgency agency = new NewsAgency();
        agency.attach(new NewsChannel("Channel A"));
        agency.attach(new NewsChannel("Channel B"));
        agency.setNews("Breaking: Strategy Pattern Deployed!");
    }
}

Benefits

Command Pattern

Intent: Encapsulate a request as an object, thereby allowing for parameterization of clients with queues, logs, and undo/redo operations.

Use Case

When you want to parameterize operations (e.g., buttons triggering commands), support undo functionality, or queue operations.

Java Example

// Command interface
interface Command {
    void execute();
}

// Receiver
class Light {
    public void turnOn() {
        System.out.println("Light turned ON");
    }

    public void turnOff() {
        System.out.println("Light turned OFF");
    }
}

// Concrete commands
class TurnOnCommand implements Command {
    private Light light;

    public TurnOnCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.turnOn();
    }
}

class TurnOffCommand implements Command {
    private Light light;

    public TurnOffCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.turnOff();
    }
}

// Invoker
class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }
}

Usage:

Light light = new Light();
Command on = new TurnOnCommand(light);
Command off = new TurnOffCommand(light);

RemoteControl remote = new RemoteControl();
remote.setCommand(on);
remote.pressButton(); // Light turned ON
remote.setCommand(off);
remote.pressButton(); // Light turned OFF
Click to view full runnable Code

// Command interface
interface Command {
    void execute();
}

// Receiver
class Light {
    public void turnOn() {
        System.out.println("Light turned ON");
    }

    public void turnOff() {
        System.out.println("Light turned OFF");
    }
}

// Concrete commands
class TurnOnCommand implements Command {
    private Light light;

    public TurnOnCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.turnOn();
    }
}

class TurnOffCommand implements Command {
    private Light light;

    public TurnOffCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.turnOff();
    }
}

// Invoker
class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }
}

public class CommandDemo {
    public static void main(String[] args) {
        Light light = new Light();
        Command on = new TurnOnCommand(light);
        Command off = new TurnOffCommand(light);

        RemoteControl remote = new RemoteControl();
        remote.setCommand(on);
        remote.pressButton(); // Light turned ON
        remote.setCommand(off);
        remote.pressButton(); // Light turned OFF
    }
}

Benefits

Summary

Pattern Intent Ideal For
Strategy Choose algorithm dynamically Behavior switching (e.g., different logics)
Observer Notify subscribers when subject changes Event-driven systems, UI updates
Command Encapsulate requests as objects Queues, undo/redo, remote execution

Conclusion

Behavioral design patterns such as Strategy, Observer, and Command are essential tools for organizing object communication in a clean and scalable way. They help developers decouple objects, promote flexibility, and design systems that are easier to extend and maintain. When used appropriately, these patterns can significantly enhance the responsiveness, modularity, and clarity of Java applications.

Index