In the world of software development, design patterns are reusable solutions to common problems that occur while designing software systems. They provide a proven way to solve common architectural and design challenges and promote code reusability and maintainability. In this article, we will explore some commonly used design patterns and their implementation in Java.
The Singleton pattern is used to restrict the instantiation of a class to a single object. Its purpose is to ensure that only one instance of a class exists throughout the system.
public class Singleton {
private static Singleton instance;
private Singleton() {
// private constructor to prevent instantiation
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
The Factory Method pattern is a creational pattern that provides an interface for creating objects, but allows the subclasses to decide which class to instantiate.
public interface Shape {
void draw();
}
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Square");
}
}
public interface ShapeFactory {
Shape createShape();
}
public class CircleFactory implements ShapeFactory {
@Override
public Shape createShape() {
return new Circle();
}
}
public class SquareFactory implements ShapeFactory {
@Override
public Shape createShape() {
return new Square();
}
}
The Adapter pattern converts the interface of a class into another interface that clients expect. It allows classes with incompatible interfaces to work together.
public interface MediaPlayer {
void play(String fileName);
}
public class AudioPlayer implements MediaPlayer {
@Override
public void play(String fileName) {
// Play audio file
}
}
public interface AdvancedMediaPlayer {
void playVideo(String fileName);
}
public class VideoPlayer implements AdvancedMediaPlayer {
@Override
public void playVideo(String fileName) {
// Play video file
}
}
public class MediaPlayerAdapter implements MediaPlayer {
private final AdvancedMediaPlayer advancedMediaPlayer;
public MediaPlayerAdapter(AdvancedMediaPlayer advancedMediaPlayer) {
this.advancedMediaPlayer = advancedMediaPlayer;
}
@Override
public void play(String fileName) {
advancedMediaPlayer.playVideo(fileName);
}
}
The Decorator pattern attaches additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality.
public interface Image {
void display();
}
public class BasicImage implements Image {
@Override
public void display() {
System.out.println("Displaying basic image");
}
}
public abstract class ImageDecorator implements Image {
protected Image decoratedImage;
public ImageDecorator(Image decoratedImage) {
this.decoratedImage = decoratedImage;
}
public void display() {
decoratedImage.display();
}
}
public class BorderDecorator extends ImageDecorator {
public BorderDecorator(Image decoratedImage) {
super(decoratedImage);
}
@Override
public void display() {
super.display();
System.out.println("Adding border to the image");
}
}
public class FilterDecorator extends ImageDecorator {
public FilterDecorator(Image decoratedImage) {
super(decoratedImage);
}
@Override
public void display() {
super.display();
System.out.println("Adding filter to the image");
}
}
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.
public interface Observer {
void update();
}
public class ConcreteObserver implements Observer {
@Override
public void update() {
// Update logic
}
}
public interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers();
}
public class ConcreteSubject implements Subject {
private final List<Observer> observers = new ArrayList<>();
@Override
public void attach(Observer observer) {
observers.add(observer);
}
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from the clients that use it.
public interface SortingStrategy {
void sort(int[] array);
}
public class BubbleSortStrategy implements SortingStrategy {
@Override
public void sort(int[] array) {
// Bubble sort logic
}
}
public class QuickSortStrategy implements SortingStrategy {
@Override
public void sort(int[] array) {
// Quick sort logic
}
}
public class SortContext {
private SortingStrategy strategy;
public void setStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
public void sortArray(int[] array) {
strategy.sort(array);
}
}
Design patterns are powerful tools in the hands of software developers. They provide solutions to common design problems, ensure code reusability, maintainability, and promote good software engineering practices. By understanding and utilizing these patterns in your Java projects, you can improve code organization, enhance flexibility, and create a robust software system.
In this article, we covered some popular design patterns - Singleton, Factory Method, Adapter, Decorator, Observer, and Strategy - along with their implementation examples in Java. Consider exploring and implementing these patterns in your software projects to write cleaner, maintainable, and scalable code.