Design Patterns in Java


Introduction

Design patterns are common solutions to recurring design problems in software development. In Java, design patterns are widely used to solve common problems efficiently, improve code readability, and promote reusability. In this tutorial, we will explore some fundamental design patterns: Singleton, Factory, and others.

We will provide step-by-step examples to understand these design patterns and their implementations.

Step 1: Singleton Pattern

The Singleton Pattern ensures that a class has only one instance and provides a global point of access to that instance. It is commonly used in situations where a single instance is needed to coordinate actions across the system, such as database connections or logging.

          // Singleton Pattern Example
          public class Singleton {
              private static Singleton instance;
      
              // Private constructor to prevent instantiation
              private Singleton() {}
      
              // Public method to get the instance
              public static Singleton getInstance() {
                  if (instance == null) {
                      instance = new Singleton();
                  }
                  return instance;
              }
      
              public void display() {
                  System.out.println("Singleton Instance: " + this);
              }
      
              public static void main(String[] args) {
                  // Access the Singleton instance
                  Singleton singleton1 = Singleton.getInstance();
                  singleton1.display();
      
                  Singleton singleton2 = Singleton.getInstance();
                  singleton2.display();
      
                  System.out.println("Are both instances equal? " + (singleton1 == singleton2));
              }
          }
        

In this example:

  • The constructor of the class Singleton is private to prevent creating instances directly.
  • The getInstance() method returns the only instance of the class.
  • We demonstrate that both references point to the same instance.

Step 2: Factory Pattern

The Factory Pattern provides an interface for creating objects, but allows subclasses to alter the type of objects that will be created. It is useful when the creation process of an object is complex or requires a specific subclass of a generic class.

          // Factory Pattern Example
          abstract class Animal {
              public abstract void speak();
          }
      
          class Dog extends Animal {
              public void speak() {
                  System.out.println("Woof");
              }
          }
      
          class Cat extends Animal {
              public void speak() {
                  System.out.println("Meow");
              }
          }
      
          class AnimalFactory {
              public Animal getAnimal(String animalType) {
                  if (animalType == null) {
                      return null;
                  }
                  if (animalType.equalsIgnoreCase("DOG")) {
                      return new Dog();
                  } else if (animalType.equalsIgnoreCase("CAT")) {
                      return new Cat();
                  }
                  return null;
              }
          }
      
          public class Main {
              public static void main(String[] args) {
                  AnimalFactory factory = new AnimalFactory();
      
                  // Create a Dog object
                  Animal dog = factory.getAnimal("DOG");
                  dog.speak();
      
                  // Create a Cat object
                  Animal cat = factory.getAnimal("CAT");
                  cat.speak();
              }
          }
        

In this example:

  • We define an abstract class Animal with an abstract method speak().
  • We create concrete classes Dog and Cat that implement the speak() method.
  • The AnimalFactory class provides the method getAnimal() to create instances of Dog or Cat.
  • The client uses the factory to create instances of different animals without directly calling their constructors.

Step 3: Observer Pattern

The Observer Pattern allows a subject to notify its observers when there is a change in its state. It is commonly used in event-driven systems, such as GUI applications or publishing/subscribing models.

          // Observer Pattern Example
          import java.util.ArrayList;
          import java.util.List;
      
          interface Observer {
              void update(String message);
          }
      
          class ConcreteObserver implements Observer {
              private String name;
      
              public ConcreteObserver(String name) {
                  this.name = name;
              }
      
              @Override
              public void update(String message) {
                  System.out.println(name + " received message: " + message);
              }
          }
      
          class Subject {
              private List<Observer> observers = new ArrayList<>();
      
              public void addObserver(Observer observer) {
                  observers.add(observer);
              }
      
              public void removeObserver(Observer observer) {
                  observers.remove(observer);
              }
      
              public void notifyObservers(String message) {
                  for (Observer observer : observers) {
                      observer.update(message);
                  }
              }
          }
      
          public class Main {
              public static void main(String[] args) {
                  Subject subject = new Subject();
      
                  // Create observers
                  Observer observer1 = new ConcreteObserver("Observer1");
                  Observer observer2 = new ConcreteObserver("Observer2");
      
                  // Add observers to the subject
                  subject.addObserver(observer1);
                  subject.addObserver(observer2);
      
                  // Notify observers
                  subject.notifyObservers("New update available!");
              }
          }
        

In this example:

  • The Observer interface defines the update() method that will be called when the subject changes.
  • The ConcreteObserver class implements the Observer interface and provides its own implementation of the update() method.
  • The Subject class maintains a list of observers and notifies them when there is a change.
  • We demonstrate how multiple observers are notified when the subject's state changes.

Step 4: Strategy Pattern

The Strategy Pattern allows the behavior of a class to be selected at runtime. It is useful when different algorithms can be applied to a problem, but the client does not need to know the specific algorithm being used.

          // Strategy Pattern Example
          interface Strategy {
              void execute();
          }
      
          class ConcreteStrategyA implements Strategy {
              public void execute() {
                  System.out.println("Executing Strategy A");
              }
          }
      
          class ConcreteStrategyB implements Strategy {
              public void execute() {
                  System.out.println("Executing Strategy B");
              }
          }
      
          class Context {
              private Strategy strategy;
      
              public Context(Strategy strategy) {
                  this.strategy = strategy;
              }
      
              public void setStrategy(Strategy strategy) {
                  this.strategy = strategy;
              }
      
              public void executeStrategy() {
                  strategy.execute();
              }
          }
      
          public class Main {
              public static void main(String[] args) {
                  Context context = new Context(new ConcreteStrategyA());
                  context.executeStrategy();
      
                  context.setStrategy(new ConcreteStrategyB());
                  context.executeStrategy();
              }
          }
        

In this example:

  • The Strategy interface defines the execute() method that different strategies will implement.
  • We create two concrete strategies: ConcreteStrategyA and ConcreteStrategyB.
  • The Context class holds a reference to a strategy and provides a method to execute the strategy.
  • We demonstrate how the strategy can be changed at runtime using the setStrategy() method.

Conclusion

Design patterns are proven solutions to common software design problems. In this tutorial, we have covered:

  • The Singleton Pattern: Ensures a class has only one instance.
  • The Factory Pattern: Provides a way to create objects without specifying the exact class.
  • The Observer Pattern: Allows a subject to notify multiple observers about changes.
  • The Strategy Pattern: Allows the behavior of a class to be selected at runtime.
Understanding and implementing these design patterns can improve the flexibility, maintainability, and scalability of your code.





Advertisement