Design patterns are general reusable solutions to commonly occurring problems in software design. They are not a finished design that can be transformed directly into code but rather a description or template for how to solve a problem that can be used in many different situations.
There are three main categories of design patterns:
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
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.
// Interface
interface Shape {
void draw();
}
// Concrete classes
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
// 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("RECTANGLE")) {
return new Rectangle();
}
return null;
}
}
The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
// Abstract Factory
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
// Concrete Factories
class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
class MacFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacButton();
}
@Override
public Checkbox createCheckbox() {
return new MacCheckbox();
}
}
The Adapter pattern allows the interface of an existing class to be used as another interface.
// Target interface
interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee
class AdvancedMediaPlayer {
public void playVlc(String fileName) {
System.out.println("Playing vlc file: " + fileName);
}
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}
// Adapter
class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new AdvancedMediaPlayer();
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
}
}
}
// Concrete class implementing the target interface
class AudioPlayer implements MediaPlayer {
MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file: " + fileName);
} else if (audioType.equalsIgnoreCase("vlc")) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
}
}
}
The Decorator pattern attaches additional responsibilities to an object dynamically.
// Component interface
interface Beverage {
String getDescription();
double cost();
}
// Concrete component
class Espresso implements Beverage {
@Override
public String getDescription() {
return "Espresso";
}
@Override
public double cost() {
return 1.99;
}
}
// Decorator abstract class
abstract class CondimentDecorator implements Beverage {
protected Beverage beverage;
public CondimentDecorator(Beverage beverage) {
this.beverage = beverage;
}
}
// Concrete decorator
class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return beverage.getDescription() + ", Mocha";
}
@Override
public double cost() {
return beverage.cost() + 0.20;
}
}
The Facade pattern provides a simplified interface to a complex system.
// Subsystems
class CPU {
public void start() {
System.out.println("CPU started");
}
}
class Memory {
public void load() {
System.out.println("Memory loaded");
}
}
class HardDrive {
public void read() {
System.out.println("Hard drive read");
}
}
// Facade class
class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public ComputerFacade() {
cpu = new CPU();
memory = new Memory();
hardDrive = new HardDrive();
}
public void startComputer() {
cpu.start();
memory.load();
hardDrive.read();
}
}
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.
import java.util.ArrayList;
import java.util.List;
// Subject interface
interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// Concrete subject
class WeatherStation implements Subject {
private List<Observer> observers;
private float temperature;
public WeatherStation() {
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature);
}
}
public void setTemperature(float temperature) {
this.temperature = temperature;
notifyObservers();
}
}
// Observer interface
interface Observer {
void update(float temperature);
}
// Concrete observer
class TemperatureDisplay implements Observer {
private float temperature;
@Override
public void update(float temperature) {
this.temperature = temperature;
display();
}
public void display() {
System.out.println("Current temperature is: " + temperature + "°C");
}
}
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
// Strategy interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using credit card");
}
}
class PayPalPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal");
}
}
// Context class
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
The Template Method pattern defines the skeleton of an algorithm in the superclass but lets subclasses override specific steps of the algorithm without changing its structure.
// Abstract class with template method
abstract class Game {
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
// Template method
public final void play() {
initialize();
startPlay();
endPlay();
}
}
// Concrete class
class Cricket extends Game {
@Override
void initialize() {
System.out.println("Cricket game initialized");
}
@Override
void startPlay() {
System.out.println("Cricket game started");
}
@Override
void endPlay() {
System.out.println("Cricket game ended");
}
}
Java design patterns are powerful tools in a developer’s toolkit. They provide solutions to common design problems, improve code reusability, maintainability, and scalability. By understanding the different categories of design patterns and their usage, developers can write more efficient and robust Java applications. However, it is important to use design patterns judiciously and avoid over - engineering.