Design patterns are commonly used solutions to recurring software design problems. They provide a standard approach to solving complex programming challenges and allow developers to build robust, reusable, and maintainable code. In Java, there are several design patterns available that help developers to write more efficient and effective code.
Implementing design patterns in Java requires an understanding of the patterns themselves, as well as a familiarity with Java syntax and programming concepts. In this guide, we will provide an overview of some commonly used design patterns in Java and show you how to implement them in your code. We will cover the creational, structural, and behavioral design patterns, and provide practical examples of each pattern in action. By the end of this guide, you will have a solid understanding of how to implement design patterns in Java and how they can help you to write better code.
What are Design Patterns in Java?
Design patterns are reusable solutions to common software design problems. They are best practices or templates that developers can use to solve problems that occur repeatedly in software development. They provide a framework for solving problems that occur in software design by offering a set of guidelines to follow when approaching a particular problem.
Design patterns emerged from the field of architecture, where architects faced similar problems in building structures that required the use of common solutions. In software engineering, design patterns can help in creating software that is flexible, maintainable, and scalable.
In object-oriented programming, code is organized into objects that interact with each other. A design pattern provides a blueprint for creating these objects and their interactions. It can help to simplify and clarify the design of a system, making it easier to understand, maintain, and modify.
Design patterns have several benefits, including:
- Reusability: Design patterns can be reused in multiple projects, saving development time and effort.
- Scalability: Design patterns help to create scalable code, making it easier to maintain and modify as the project grows.
- Maintainability: Design patterns make it easier to maintain code, as they provide a clear structure and organization for the code.
- Consistency: Design patterns provide a consistent approach to solving problems, making it easier for developers to understand and work with each other's code.
There are many design patterns, each with its own purpose and usage. Creational patterns are used to create objects and manage their creation. Structural patterns are used to organize code into larger structures, making it easier to manage and modify. Behavioral patterns are used to manage communication between objects and control the flow of data.
Types of Design Patterns
Design patterns can be broadly categorized into three main types: creational patterns, structural patterns, and behavioral patterns. Each type serves a different purpose and provides a different set of guidelines for developers to follow.
Creational Patterns
Creational patterns are used to create objects and manage their creation. They provide a set of guidelines for creating objects in a way that is flexible, scalable, and reusable. Some examples of creational patterns include:
- Singleton: This pattern ensures that only one instance of a class is created and provides a global point of access to that instance.
- Factory Method: This pattern provides an interface for creating objects, allowing subclasses to decide which class to instantiate.
- Abstract Factory: This pattern provides an interface for creating families of related objects without specifying their concrete classes.
- Builder: Separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
- Prototype: Specifies the kind of object to create using a prototypical instance and creates new objects by cloning this prototype
Structural Patterns
Structural patterns are used to organize code into larger structures, making it easier to manage and modify. They provide a set of guidelines for creating objects that work together to form larger, more complex structures. Some examples of structural patterns include:
- Adapter: Converts the interface of a class into another interface that clients expect, allowing classes with incompatible interfaces to work together.
- Bridge: Decouples an abstraction from its implementation, allowing the two to vary independently.
- Composite: Composes objects into tree structures to represent part-whole hierarchies, allowing clients to treat individual objects and compositions of objects uniformly.
- Decorator: Attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing for extending functionality.
- Facade: Provides a unified interface to a set of interfaces in a subsystem, simplifying client interactions with the subsystem.
- Flyweight: Reduces memory usage and object creation overhead by sharing and reusing objects with a shared state, supporting large numbers of fine-grained objects efficiently.
Behavioral Patterns
Behavioral patterns are used to manage communication between objects and control the flow of data. They provide a set of guidelines for creating objects that interact with each other in a specific way. Some examples of behavioral patterns include:
- Observer: This pattern allows an object to notify other objects when its state changes, maintaining consistency across the system.
- Command: This pattern encapsulates a request as an object, allowing it to be passed as a parameter and executed at a later time.
- Template: This pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses, allowing subclasses to redefine certain steps without changing the algorithm's structure.
Creational Design Pattern
1. Singleton Pattern
The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. This is useful when a single object is needed to coordinate actions across the system. Here's an example of how to implement Singleton in Java:
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;
}
}
In the Singleton
class, we have a private static instance variable instance
. The constructor is marked as private to prevent other classes from creating instances. The getInstance()
method checks whether the instance is null, and if it is, it creates a new instance. If an instance already exists, it returns the existing instance. This ensures that only one instance of the Singleton
class is created.
2. Factory Method Pattern
The Factory Method pattern defines an interface for creating an object but allows subclasses to decide which class to instantiate. It lets a class defer instantiation to subclasses. Here's an example of how to implement the Factory Method in Java:
public interface Animal {
void speak();
}
public class Dog implements Animal {
public void speak() {
System.out.println("Woof!");
}
}
public class Cat implements Animal {
public void speak() {
System.out.println("Meow!");
}
}
public abstract class AnimalFactory {
public abstract Animal createAnimal();
public static AnimalFactory getFactory(String type) {
if ("Dog".equalsIgnoreCase(type)) {
return new DogFactory();
} else if ("Cat".equalsIgnoreCase(type)) {
return new CatFactory();
}
throw new IllegalArgumentException("Invalid animal type");
}
}
public class DogFactory extends AnimalFactory {
public Animal createAnimal() {
return new Dog();
}
}
public class CatFactory extends AnimalFactory {
public Animal createAnimal() {
return new Cat();
}
}
In the example, we define an Animal
interface with a speak()
method. Two classes, Dog
and Cat
, implement the Animal
interface. The AnimalFactory
is an abstract class with an abstract method createAnimal()
and a static method getFactory()
that takes a string and returns a specific factory instance based on the input. The DogFactory
and CatFactory
classes extend AnimalFactory
and override the createAnimal()
method to return instances of Dog
and Cat
, respectively.
3. Abstract Factory
The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes.
public interface Button {
void render();
}
public class WindowsButton implements Button {
public void render() {
System.out.println("Rendering Windows button");
}
}
public class MacButton implements Button {
public void render() {
System.out.println("Rendering Mac button");
}
}
public interface GUIFactory {
Button createButton();
}
public class WindowsFactory implements GUIFactory {
public Button createButton() {
return new WindowsButton();
}
}
public class MacFactory implements GUIFactory {
public Button createButton() {
return new MacButton();
}
}
We define a Button
interface with a render()
method. Two classes, WindowsButton
and MacButton
, implement the Button
interface. The GUIFactory
interface has a createButton()
method. The WindowsFactory
and MacFactory
classes implement the GUIFactory
interface and override the createButton()
method to return instances of WindowsButton
and MacButton
, respectively.
4. Builder Pattern
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
public class Car {
private String engine;
private String transmission;
private String color;
public void setEngine(String engine) { this.engine = engine; }
public void setTransmission(String transmission) { this.transmission = transmission; }
public void setColor(String color) { this.color = color; }
}
public interface CarBuilder {
CarBuilder buildEngine(String engine);
CarBuilder buildTransmission(String transmission);
CarBuilder buildColor(String color);
Car build();
}
public class CarBuilderImpl implements CarBuilder {
private Car car;
public CarBuilderImpl() {
this.car = new Car();
}
public CarBuilder buildEngine(String engine) {
car.setEngine(engine);
return this;
}
public CarBuilder buildTransmission(String transmission) {
car.setTransmission(transmission);
return this;
}
public CarBuilder buildColor(String color) {
car.setColor(color);
return this;
}
public Car build() {
return car;
}
}
5. Prototype Pattern
The Prototype pattern specifies the kind of object to create using a prototypical instance and creates new objects by cloning this prototype.
public interface Shape extends Cloneable {
Shape clone();
}
public class Rectangle implements Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public Rectangle(Rectangle source) {
this.width = source.width;
this.height = source.height;
}
@Override
public Shape clone() {
return new Rectangle(this);
}
}
public class Circle implements Shape {
private int radius;
public Circle(int radius) {
this.radius = radius;
}
public Circle(Circle source) {
this.radius = source.radius;
}
@Override
public Shape clone() {
return new Circle(this);
}
}
In the Shape
interface, we define a clone()
method. Two classes, Rectangle
and Circle
, implement the Shape
interface and override the clone()
method. The clone()
method creates a new instance of the class and copies the properties of the original object. When using the prototype pattern, you can create a new object by simply calling the clone()
method on an existing object, avoiding the need for complex object initialization logic.
Structural Design Patterns
1. Adapter Pattern
The Adapter pattern converts the interface of a class into another interface that clients expect. It allows classes with incompatible interfaces to work together.
Use Case: Integrating a third-party library with a different interface or making a legacy code work with a new system.
// Target Interface
interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee Class
class AdvancedMediaPlayer {
void playVlc(String fileName) {
System.out.println("Playing VLC file: " + fileName);
}
void playMp4(String fileName) {
System.out.println("Playing MP4 file: " + fileName);
}
}
// Adapter Class
class MediaPlayerAdapter implements MediaPlayer {
private AdvancedMediaPlayer adaptee;
public MediaPlayerAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
adaptee = new AdvancedMediaPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
adaptee = new AdvancedMediaPlayer();
}
}
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
adaptee.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
adaptee.playMp4(fileName);
}
}
}
We define a MediaPlayer
interface (target) with a play()
method. The AdvancedMediaPlayer
class (adaptee) has two methods, playVlc()
and playMp4()
. We create an adapter class, MediaPlayerAdapter
, that implements the MediaPlayer
interface and has an instance of AdvancedMediaPlayer
as a private member. In the play()
method, we delegate the calls to the appropriate methods of the AdvancedMediaPlayer
class based on the audio type.
2. Bridge Pattern
The Bridge pattern decouples an abstraction from its implementation, allowing the two to vary independently.
Use Case: Developing cross-platform applications with different UI controls or connecting remote services using different communication protocols.
// Abstraction
abstract class Shape {
protected Renderer renderer;
public Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
}
// Refined Abstraction
class Circle extends Shape {
public Circle(Renderer renderer) {
super(renderer);
}
public void draw() {
renderer.renderCircle();
}
}
// Implementor
interface Renderer {
void renderCircle();
}
// Concrete Implementor
class VectorRenderer implements Renderer {
public void renderCircle() {
System.out.println("Drawing a vector circle");
}
}
// Concrete Implementor
class RasterRenderer implements Renderer {
public void renderCircle() {
System.out.println("Drawing a raster circle");
}
}
We define an abstract Shape
class with a Renderer
member. The Shape
class has an abstract draw()
method. The Circle
class (refined abstraction) extends Shape
and overrides the draw()
method, delegating the call to the Renderer
member. The Renderer
interface (implementor) has a renderCircle()
method. The VectorRenderer
and RasterRenderer
classes (concrete implementors) implement the Renderer
interface and override the renderCircle()
method.
3. Composite Pattern
The Composite pattern allows treating a group of objects as a single instance of an object. It composes objects into tree structures to represent part-whole hierarchies.
Use Case: Implementing a file system with files and directories, or creating a GUI with complex nested UI components.
// Component
abstract class Graphic {
public abstract void draw();
public void add(Graphic graphic) {
throw new UnsupportedOperationException();
}
public void remove(Graphic graphic) {
throw new UnsupportedOperationException();
}
}
// Leaf
class Circle extends Graphic {
public void draw() {
System.out.println("Drawing a circle");
}
}
// Composite
class GraphicGroup extends Graphic {
private List<Graphic> graphics = new ArrayList<>();
public void draw() {
for (Graphic graphic : graphics) {
graphic.draw();
}
}
public void add(Graphic graphic) {
graphics.add(graphic);
}
public void remove(Graphic graphic) {
graphics.remove(graphic);
}
}
We define an abstract Graphic
class with a draw()
method and add()
and remove()
methods that throw UnsupportedOperationException
by default. The Circle
class (leaf) extends Graphic
and overrides the draw()
method. The GraphicGroup
class (composite) extends Graphic
, overrides the draw()
method to draw all its child graphics, and also overrides the add()
and remove()
methods to manage child components.
4. Decorator Pattern
The Decorator pattern attaches additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality.
Use Case: Adding features to an existing class without modifying its code, or extending a class when subclassing is not practical.
// Component
interface Coffee {
double cost();
String getDescription();
}
// Concrete Component
class Espresso implements Coffee {
public double cost() {
return 1.99;
}
public String getDescription() {
return "Espresso";
}
}
// Decorator
<a title="Difference between Interface and Abstract class [SOLVED]" href="https://www.golinuxcloud.com/difference-between-interface-abstract-class/" target="_blank" rel="noopener noreferrer">abstract class</a> CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
public abstract double cost();
public abstract String getDescription();
}
// Concrete Decorator
class Milk extends CoffeeDecorator {
public Milk(Coffee coffee) {
super(coffee);
}
public double cost() {
return coffee.cost() + 0.30;
}
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
}
We define a Coffee
interface (component) with cost()
and getDescription()
methods. The Espresso
class (concrete component) implements the Coffee
interface and overrides the cost()
and getDescription()
methods. The CoffeeDecorator
abstract class (decorator) implements the Coffee
interface and has a Coffee
member. The Milk
class (concrete decorator) extends CoffeeDecorator
and overrides the cost()
and getDescription()
methods to add the cost and description of milk to the base coffee.
5. Facade Pattern
The Facade pattern provides a unified interface to a set of interfaces in a subsystem. It defines a higher-level interface that makes the subsystem easier to use.
Use Case: Simplifying interactions with a complex system, or providing a simpler API to a third-party library.
class SubsystemA {
public void operationA() {
System.out.println("Subsystem A operation");
}
class SubsystemB {
public void operationB() {
System.out.println("Subsystem B operation");
}
}
class SubsystemC {
public void operationC() {
System.out.println("Subsystem C operation");
}
}
class Facade {
private SubsystemA subsystemA;
private SubsystemB subsystemB;
private SubsystemC subsystemC;
public Facade() {
subsystemA = new SubsystemA();
subsystemB = new SubsystemB();
subsystemC = new SubsystemC();
}
public void performOperation() {
subsystemA.operationA();
subsystemB.operationB();
subsystemC.operationC();
}
}
We define three subsystem classes, SubsystemA
, SubsystemB
, and SubsystemC
, each with their respective operation methods. The Facade
class has instances of all three subsystems and provides a performOperation()
method, which calls the operation methods of all subsystems. Clients interact with the Facade
class, which simplifies the interaction with the subsystems.
6. Flyweight Pattern
The Flyweight pattern uses sharing to support large numbers of fine-grained objects efficiently, reducing memory usage and object creation overhead.
Use Case: Creating many similar objects with a shared state, such as rendering characters in a text editor or game objects in a video game.
// Flyweight Interface
interface Shape {
void draw();
}
// Concrete Flyweight
class Circle implements Shape {
private String color;
public Circle(String color) {
this.color = color;
}
public void draw() {
System.out.println("Drawing a " + color + " circle");
}
}
// Flyweight Factory
class ShapeFactory {
private static final Map<String, Shape> shapeMap = new HashMap<>();
public static Shape getCircle(String color) {
Shape shape = shapeMap.get(color);
if (shape == null) {
shape = new Circle(color);
shapeMap.put(color, shape);
System.out.println("Creating a new " + color + " circle");
}
return shape;
}
}
We define a Shape
interface (flyweight) with a draw()
method. The Circle
class (concrete flyweight) implements the Shape
interface and has a color
property. The ShapeFactory
class provides a static getCircle()
method that returns a Circle
instance with the specified color. If a circle with the given color doesn't exist, it creates a new one, stores it in the shapeMap
, and returns it. This way, Circle
instances with the same color are shared and reused.
Behavioral Design Patterns
Behavioral design patterns define the ways in which objects communicate and interact with one another. They improve the flexibility and maintainability of code by decoupling components. Here are three commonly used behavioral design patterns, with code examples and explanations:
1. Observer Pattern
The Observer pattern allows an object to notify other objects when its state changes, maintaining consistency across the system. Here's an example of how to implement Observer in Java:
// Subject
class Subject {
private List<Observer> observers = new ArrayList<>();
private int state;
public void attach(Observer observer) {
observers.add(observer);
}
public void setState(int state) {
this.state = state;
notifyAllObservers();
}
public int getState() {
return state;
}
private void notifyAllObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
// Observer
abstract class Observer {
protected Subject subject;
public abstract void update();
}
// Concrete Observer
class BinaryObserver extends Observer {
public BinaryObserver(Subject subject) {
this.subject = subject;
subject.attach(this);
}
public void update() {
System.out.println("Binary: " + Integer.toBinaryString(subject.getState()));
}
}
We define a Subject
class that maintains a list of Observer
instances. The Subject
class provides methods to attach observers, set state, and notify observers. When the state is updated, the Subject
notifies all attached observers. We define an abstract Observer
class with a reference to the Subject
and an abstract update()
method. The BinaryObserver
(concrete observer) extends the Observer
class and implements the update()
method to display the subject's state in binary format.
2. Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently from clients that use it.
// Strategy
interface SortingStrategy {
void sort(int[] numbers);
}
// Concrete Strategy
class BubbleSortStrategy implements SortingStrategy {
public void sort(int[] numbers) {
// Implement bubble sort algorithm
System.out.println("Bubble sort");
}
}
// Concrete Strategy
class QuickSortStrategy implements SortingStrategy {
public void sort(int[] numbers) {
// Implement quick sort algorithm
System.out.println("Quick sort");
}
}
// Context
class Sorter {
private SortingStrategy strategy;
public void setStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
public void sort(int[] numbers) {
strategy.sort(numbers);
}
}
We define a SortingStrategy
interface (strategy) with a sort()
method. The BubbleSortStrategy
and QuickSortStrategy
classes (concrete strategies) implement the SortingStrategy
interface and provide their respective sorting algorithms. The Sorter
class (context) contains a reference to a SortingStrategy
instance and provides a setStrategy()
method to change the strategy at runtime. The sort()
method in the Sorter
class delegates the sorting task to the currently set strategy.
3. Template Method Pattern
The Template Method pattern defines the skeleton of an algorithm in an operation, deferring some steps to subclasses, allowing subclasses to redefine certain steps without changing the algorithm's structure.
// Abstract Class
abstract class Game {
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
// Template method
public final void play() {
initialize();
startPlay();
endPlay();
}
}
// Concrete Class
class Football extends Game {
void initialize() {
System.out.println("Football Game Initialized");
}
void startPlay() {
System.out.println("Football Game Started");
}
void endPlay() {
System.out.println("Football Game Ended");
}
}
// Concrete Class
class Basketball extends Game {
void initialize() {
System.out.println("Basketball Game Initialized");
}
void startPlay() {
System.out.println("Basketball Game Started");
}
void endPlay() {
System.out.println("Basketball Game Ended");
}
}
We define an abstract Game
class with three abstract methods: initialize()
, startPlay()
, and endPlay()
. The play()
method (template method) in the Game
class calls these methods in a specific order. Subclasses like Football
and Basketball
extend the Game
class and provide their own implementations for the abstract methods. The play()
method in the Game
class ensures that the steps are executed in the same order for all concrete classes, while allowing subclasses to provide their own implementations for each step.
Summary
In summary, implementing design patterns in Java is a powerful tool for solving recurring software design problems, and it helps developers to build maintainable, efficient, and reusable code. The three types of design patterns are creational, structural, and behavioral. Each pattern has a specific set of benefits and drawbacks, and selecting the appropriate design pattern for a given situation is important. Implementing design patterns in Java requires an understanding of the pattern itself and familiarity with Java syntax and programming concepts. By following best practices and guidelines for using design patterns, developers can avoid common mistakes and apply patterns effectively in their projects. Overall, incorporating design patterns into Java programming can help developers write better code and achieve their software development goals.
Further Reading