The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is useful when you need to control access to a shared resource, such as a database connection or a configuration manager.
To implement the Singleton pattern in Java, you typically create a private constructor to prevent external instantiation and a public static method to provide access to the single instance.
public class Singleton {
private static Singleton instance;
private Singleton() {
// Private constructor to prevent external instantiation
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
In a multi - threaded environment, the above implementation is not thread - safe. A common practice is to use the double - checked locking mechanism or an enum to ensure thread - safety.
public class ThreadSafeSingleton {
private static volatile ThreadSafeSingleton instance;
private ThreadSafeSingleton() {
}
public static ThreadSafeSingleton getInstance() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
}
Using an enum is considered the best practice for implementing a Singleton in Java as it provides implicit thread - safety and protection against serialization and reflection attacks.
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("Doing something...");
}
}
The Factory pattern provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. It is useful when you need to decouple the object creation logic from the client code.
You create a factory class that has a method responsible for creating objects based on certain conditions.
// Interface
interface Shape {
void draw();
}
// Concrete classes
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing a square");
}
}
// Factory class
class ShapeFactory {
public Shape getShape(String shapeType) {
if (shapeType == null) {
return null;
}
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (shapeType.equalsIgnoreCase("SQUARE")) {
return new Square();
}
return null;
}
}
The factory method can be made static for simplicity, so you don’t need to create an instance of the factory class to use it.
class StaticShapeFactory {
public static Shape getShape(String shapeType) {
if (shapeType == null) {
return null;
}
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (shapeType.equalsIgnoreCase("SQUARE")) {
return new Square();
}
return null;
}
}
Use dependency injection to make the factory more flexible. For example, you can pass in a configuration object to the factory method to determine which objects to create.
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. It is commonly used in event handling systems.
You have a subject (the object that changes state) and observers (the objects that are notified of the change). The subject maintains a list of observers and provides methods to register, unregister, and notify them.
import java.util.ArrayList;
import java.util.List;
// Observer interface
interface Observer {
void update(String message);
}
// Subject interface
interface Subject {
void registerObserver(Observer observer);
void unregisterObserver(Observer observer);
void notifyObservers(String message);
}
// Concrete subject
class MessagePublisher implements Subject {
private List<Observer> observers = new ArrayList<>();
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void unregisterObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
public void createMessage(String message) {
notifyObservers(message);
}
}
// Concrete observer
class MessageSubscriber implements Observer {
private String name;
public MessageSubscriber(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " received message: " + message);
}
}
In Java, the java.util.Observable
class and java.util.Observer
interface were originally provided for implementing the Observer pattern. However, they have some limitations and are now considered legacy. It is more common to implement the pattern from scratch as shown above.
Use a thread - safe data structure to store the observers if the pattern is used in a multi - threaded environment. Also, consider using weak references to avoid memory leaks if the observers have a shorter lifespan than the subject.
The Decorator pattern allows you to attach additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality.
You have a base component interface, concrete component classes, and decorator classes that implement the same interface and wrap the base component.
// Component interface
interface Coffee {
double getCost();
String getDescription();
}
// Concrete component
class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 1.0;
}
@Override
public String getDescription() {
return "Simple coffee";
}
}
// Decorator abstract class
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public double getCost() {
return coffee.getCost();
}
@Override
public String getDescription() {
return coffee.getDescription();
}
}
// Concrete decorator
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
return super.getCost() + 0.5;
}
@Override
public String getDescription() {
return super.getDescription() + ", with milk";
}
}
Use composition instead of inheritance to add functionality. This allows you to combine multiple decorators easily.
Keep the decorator classes as lightweight as possible and focus on adding a single responsibility. This makes the code more maintainable and easier to understand.
Design patterns are essential tools for Java developers. They help in writing more organized, maintainable, and scalable code. By understanding real - world examples of design patterns such as the Singleton, Factory, Observer, and Decorator patterns, developers can make better design decisions and solve common problems more effectively. Each pattern has its own use cases, and choosing the right pattern for the right situation is crucial for the success of a software project.