Implement Design Patterns in Java [In-Depth Tutorial]


JAVA

Author: Bashir Alam
Reviewer: Deepak Prasad

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

Desing Pattern in Java

 

Views: 66
Bashir Alam

Bashir Alam

He is a Computer Science graduate from the University of Central Asia, currently employed as a full-time Machine Learning Engineer at uExel. His expertise lies in Python, Java, Machine Learning, OCR, text extraction, data preprocessing, and predictive models. You can connect with him on his LinkedIn profile.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment