Skip to content

Program 12 : Design Patterns

Implement a simple Java program to demonstrate common design patterns such as Singleton, Factory, Observer, and Strategy.

A) Singleton Design Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. The getInstance() method in this example is made synchronized to ensure thread safety.

Printer.java

java
// Singleton class
public class Printer {

    // The single instance of the class
    private static Printer instance;

    // Private constructor to prevent instantiation from outside
    private Printer() {
        System.out.println("Printer is ready.");
    }

    // Public static method to get the single instance of the class
    // Synchronized to ensure thread safety
    public static synchronized Printer getInstance() {
        if (instance == null) {
            instance = new Printer();
        }
        return instance;
    }

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

SingleDP.java

java
public class SingleDP {
    public static void main(String[] args) {
        
        // Get the single instance of Printer
        Printer p1 = Printer.getInstance();
        p1.print("Hello, World!");

        // Get the same instance again
        Printer p2 = Printer.getInstance();
        p2.print("Java Design Patterns");
        
        // Verify that both variables point to the same object
        System.out.println("p1 & p2 refer the same object: " 
	        + (p1 == p2));
    }
}

B) Factory Design Pattern

The Factory Method pattern defines an interface for creating an object but lets subclasses alter the type of objects that will be created.

FactoryMethodExample.java

java
// Abstract Product Class
abstract class Product {
    public abstract void display();
}

// Concrete Products
class ConcreteProductA extends Product {
    @Override
    public void display() {
        System.out.println("This is Concrete Product A.");
    }
}

class ConcreteProductB extends Product {
    @Override
    public void display() {
        System.out.println("This is Concrete Product B.");
    }
}

// Creator Abstract Class
abstract class Creator {
    public abstract Product factoryMethod();
}

// Concrete Creators
class ConcreteCreatorA extends Creator {
    @Override
    public Product factoryMethod() {
        return new ConcreteProductA();
    }
}

class ConcreteCreatorB extends Creator {
    @Override
    public Product factoryMethod() {
        return new ConcreteProductB();
    }
}

// Client Code
public class FactoryMethodExample {
    public static void main(String[] args) {
        Creator creatorA = new ConcreteCreatorA();
        Product productA = creatorA.factoryMethod();
        productA.display();

        Creator creatorB = new ConcreteCreatorB();
        Product productB = creatorB.factoryMethod();
        productB.display();
    }
}

C) Observer Design 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.

ObserverDemo.java

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

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

// Concrete Observers
class MobileApp implements Observer {
    @Override
    public void update(String weatherData) {
        System.out.println("Mobile App: Weather updated - " 
	        + weatherData);
    }
}

class Website implements Observer {
    @Override
    public void update(String weatherData) {
        System.out.println("Website: Weather updated - " 
	        + weatherData);
    }
}

// Subject (or Publisher)
class WeatherStation {
    private List<Observer> observers = new ArrayList<>();
    private String weather;

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

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

    public void setWeather(String newWeather) {
        this.weather = newWeather;
        notifyObservers();
    }

    private void notifyObservers() {
        for (Observer o : observers) {
            o.update(weather);
        }
    }
}

// Client Code
public class ObserverDemo {
    public static void main(String[] args) {
        WeatherStation station = new WeatherStation();
        Observer mobile = new MobileApp();
        Observer site = new Website();

        station.addObserver(mobile);
        station.addObserver(site);

        station.setWeather("Sunny 32°C");
        station.setWeather("Rainy 24°C");
    }
}

D) Strategy Design Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

StrategyTravelDemo.java

java
@FunctionalInterface
interface TravelStrategy {
    void travel(String destination);
}

// Client code that defines and uses strategies
public class StrategyTravelDemo {

    public static void main(String[] args) {
        
        // 1. Define strategies using lambda expressions instead of separate classes
        TravelStrategy car = destination -> 
            System.out.println("Driving to " + destination + " by car.");

        TravelStrategy train = destination -> 
            System.out.println("Going to " + destination + " by train.");

        TravelStrategy plane = destination -> 
            System.out.println("Flying to " + destination + " by plane.");
            
        // 2. Execute the strategies directly
        System.out.println("Trip 1:");
        car.travel("Goa");

        System.out.println("\nTrip 2:");
        train.travel("Delhi");
        
        System.out.println("\nTrip 3:");
        plane.travel("London");
    }
}

Longer Version

java
// Strategy interface
interface TravelStrategy {
    void travel(String destination);
}

// Concrete Strategies
class CarTravel implements TravelStrategy {
    @Override
    public void travel(String destination) {
        System.out.println("Driving to " + destination 
	        + " by car. Takes more time but gives flexibility.");
    }
}

class TrainTravel implements TravelStrategy {
    @Override
    public void travel(String destination) {
        System.out.println("Going to " + destination 
	        + " by train. Comfortable and affordable.");
    }
}

class PlaneTravel implements TravelStrategy {
    @Override
    public void travel(String destination) {
        System.out.println("Flying to " + destination 
	        + " by plane. Fastest but most expensive.");
    }
}

// Context class
class TravelPlanner {
    private TravelStrategy strategy;

    // Set the travel strategy (can be changed at runtime)
    public void setStrategy(TravelStrategy strategy) {
        this.strategy = strategy;
    }

    // Execute the selected travel strategy
    public void goTo(String destination) {
        if (strategy == null) {
            System.out.println("Select a travel strategy first!");
        } else {
            strategy.travel(destination);
        }
    }
}

// Client code
public class StrategyTravelDemo {
    public static void main(String[] args) {
        TravelPlanner planner = new TravelPlanner();

        System.out.println("Trip 1:");
        planner.setStrategy(new CarTravel());
        planner.goTo("Goa");

        System.out.println("\nTrip 2:");
        planner.setStrategy(new TrainTravel());
        planner.goTo("Delhi");
        
        System.out.println("\nTrip 3:");
        planner.setStrategy(new PlaneTravel());
        planner.goTo("London");
    }
}

Made with ❤️ for students, by a fellow learner.