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 methodspeak()
. - We create concrete classes
Dog
andCat
that implement thespeak()
method. - The
AnimalFactory
class provides the methodgetAnimal()
to create instances ofDog
orCat
. - 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 theupdate()
method that will be called when the subject changes. - The
ConcreteObserver
class implements theObserver
interface and provides its own implementation of theupdate()
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 theexecute()
method that different strategies will implement. - We create two concrete strategies:
ConcreteStrategyA
andConcreteStrategyB
. - 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.