How to Implement Design Patterns in Java
Table of Contents
- Fundamental Concepts of Design Patterns in Java
- Creational Design Patterns
- Singleton Pattern
- Factory Pattern
- Structural Design Patterns
- Decorator Pattern
- Adapter Pattern
- Behavioral Design Patterns
- Observer Pattern
- Strategy Pattern
- Common Practices and Best Practices
- Conclusion
- References
Fundamental Concepts of Design Patterns in Java
Design patterns in Java are classified into three main categories: creational, structural, and behavioral.
- Creational Design Patterns: These patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.
- Structural Design Patterns: They are concerned with how classes and objects are composed to form larger structures.
- Behavioral Design Patterns: These patterns are focused on the interaction between objects and the distribution of responsibilities.
Creational Design Patterns
Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.
// Singleton class
class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
// Usage
public class SingletonExample {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // Output: true
}
}
Factory Pattern
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 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;
}
}
// Usage
public class FactoryExample {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();
Shape circle = shapeFactory.getShape("CIRCLE");
circle.draw();
Shape square = shapeFactory.getShape("SQUARE");
square.draw();
}
}
Structural Design Patterns
Decorator Pattern
The Decorator pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
// 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;
}
}
// Usage
public class DecoratorExample {
public static void main(String[] args) {
Beverage espresso = new Espresso();
System.out.println(espresso.getDescription() + " $" + espresso.cost());
Beverage mochaEspresso = new Mocha(espresso);
System.out.println(mochaEspresso.getDescription() + " $" + mochaEspresso.cost());
}
}
Adapter Pattern
The Adapter pattern allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code.
// Target interface
interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee class
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
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMediaPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMediaPlayer = new AdvancedMediaPlayer();
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMediaPlayer.playVlc(fileName);
}
}
}
// Concrete media player
class AudioPlayer implements MediaPlayer {
private 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);
}
}
}
// Usage
public class AdapterExample {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "test.mp3");
audioPlayer.play("vlc", "test.vlc");
}
}
Behavioral Design Patterns
Observer Pattern
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 observer);
void removeObserver(Observer observer);
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 observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@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 {
@Override
public void update(float temperature) {
System.out.println("Temperature updated: " + temperature);
}
}
// Usage
public class ObserverExample {
public static void main(String[] args) {
WeatherStation weatherStation = new WeatherStation();
TemperatureDisplay temperatureDisplay = new TemperatureDisplay();
weatherStation.registerObserver(temperatureDisplay);
weatherStation.setTemperature(25.0f);
}
}
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.
// Strategy interface
interface PaymentStrategy {
void pay(int amount);
}
// Concrete strategies
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paying " + amount + " using credit card");
}
}
class PayPalPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paying " + 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);
}
}
// Usage
public class StrategyExample {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(100);
cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(200);
}
}
Common Practices and Best Practices
- Understand the Problem: Before applying a design pattern, make sure you understand the problem thoroughly. Not every problem requires a design pattern.
- Follow the Principles: Adhere to design principles such as the Single Responsibility Principle, Open-Closed Principle, etc.
- Use Interfaces and Abstract Classes: Interfaces and abstract classes provide a high level of flexibility and extensibility.
- Avoid Overusing Patterns: Using too many design patterns can make the code complex and hard to understand.
Conclusion
Design patterns in Java are powerful tools that can help you write more maintainable, scalable, and efficient code. By understanding the fundamental concepts, different types of design patterns, and following common and best practices, you can effectively implement design patterns in your Java projects. Remember to use design patterns judiciously and only when they are truly needed.
References
- “Design Patterns: Elements of Reusable Object-Oriented Software” by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
- Oracle Java Documentation: https://docs.oracle.com/javase/8/docs/
This blog provides a comprehensive overview of implementing design patterns in Java. You can further explore other design patterns and their applications in different scenarios.