Skip to main content

Design Patterns in Java

Introduction​

Design patterns are proven solutions to common software design problems. They provide a template for how to solve a problem in various contexts. Understanding and using design patterns can help you write more robust, maintainable, and scalable code.

Types of Design Patterns​

Design patterns can be broadly classified into three categories:

  1. Creational Patterns: Deal with object creation mechanisms.
  2. Structural Patterns: Deal with object composition and structure.
  3. Behavioral Patterns: Deal with object interaction and responsibility distribution.

Creational Patterns​

Singleton Pattern​

Ensures a class has only one instance and provides a global point of access to it.

Example​

public class Singleton {
private static Singleton instance;

private Singleton() {
// Private constructor to prevent instantiation
}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

Factory Pattern​

Defines an interface for creating an object, but lets subclasses alter the type of objects that will be created.

Example​

public interface Shape {
void draw();
}

public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Circle");
}
}

public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing Rectangle");
}
}

public class ShapeFactory {
public Shape getShape(String shapeType) {
if (shapeType == null) {
return null;
}
if (shapeType.equalsIgnoreCase("CIRCLE")) {
return new Circle();
} else if (shapeType.equalsIgnoreCase("RECTANGLE")) {
return new Rectangle();
}
return null;
}
}

// Usage
public class Main {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();

Shape shape1 = shapeFactory.getShape("CIRCLE");
shape1.draw();

Shape shape2 = shapeFactory.getShape("RECTANGLE");
shape2.draw();
}
}

Structural Patterns​

Adapter Pattern​

Allows incompatible interfaces to work together by wrapping an existing class with a new interface.

Example​

public interface MediaPlayer {
void play(String audioType, String fileName);
}

public class AudioPlayer implements MediaPlayer {
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file: " + fileName);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}

public interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}

public class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file: " + fileName);
}

@Override
public void playMp4(String fileName) {
// Do nothing
}
}

public class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// Do nothing
}

@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}

public class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedMusicPlayer;

public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer = new Mp4Player();
}
}

@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer.playMp4(fileName);
}
}
}

// Usage
public class Main {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();

audioPlayer.play("mp3", "song.mp3");
audioPlayer.play("mp4", "video.mp4");
audioPlayer.play("vlc", "movie.vlc");
audioPlayer.play("avi", "clip.avi");
}
}

Behavioral Patterns​

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.

Example​

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

public interface Observer {
void update(String message);
}

public 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);
}
}

public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}

public class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
private String message;

@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}

@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}

@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(message);
}
}

public void setMessage(String message) {
this.message = message;
notifyObservers();
}
}

// Usage
public class Main {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();

Observer observer1 = new ConcreteObserver("Observer 1");
Observer observer2 = new ConcreteObserver("Observer 2");

subject.registerObserver(observer1);
subject.registerObserver(observer2);

subject.setMessage("Hello, Observers!");

subject.removeObserver(observer1);

subject.setMessage("Hello again!");
}
}

Conclusion​

Design patterns are essential for building efficient, reusable, and maintainable object-oriented software. By understanding and applying these patterns, you can solve common design problems and improve your code quality. The SOLID principles, combined with a good grasp of design patterns, will greatly enhance your ability to design robust Java applications.