3 Java Design Patterns You Must Know In 2023

Chelariu Andrei
5 min readFeb 10, 2023

--

Simplify your understanding of design patterns in Java

Photo by Desola Lanre-Ologun on Unsplash

Design patterns in Java are reusable solutions to commonly occurring software design problems. They provide a blueprint for solving these problems, which makes it easier for developers to write reliable and maintainable code.

There are several types of design patterns, including creational patterns, structural patterns, and behavioral patterns.

The most useful design patterns in Java you should know are Singleton the simplest one, Composite, and Strategy.

Singleton Pattern

This pattern ensures that a class has only one instance and provides a global point of access to it. It is a creational pattern with large realizability that is widely used in the Java community and can help you write better more maintainable code.

Let’s discuss one example to have a better understanding,

/**
* Created by Chelariu Andrei on 10/2/2023.
*/
public class Singleton {

//Instantiate a new Singleton() object
private static Singleton uniqueInstance = new Singleton();
private Singleton() {
}

//In this method we used Singleton Lazy,
//a new object is created only when needed
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}

We apply a design pattern to solve a problem. In our case, we want to implement a functionality in the management system for luggage.

Basically, this method creates a new object of type responsible in the class responsible. After saving the person in charge, a message will be displayed if it has been saved or not in the database.

Solution: In the Singleton class, should be instantiated an operation that lets the clients access its unique instance. It’s responsible for creating and maintaining its unique instance.

Composite Pattern

This is a structural design pattern in Java that allows you to treat individual objects and compositions of objects uniformly. The composite pattern is used to structure the data in a tree-like manner, where the individual objects are the leaves of the tree, and the compositions of objects are the branches and the trunk.

In the composite pattern, a component interface defines the common methods for both the leaf nodes and composite nodes. The leaf nodes represent the objects that cannot be broken down any further, while the composite nodes represent objects that are made up of multiple components. The composite objects can have methods that aggregate their children and operate on them as a whole.

The composite pattern is useful when you want to represent a part-whole hierarchy of objects, where you want to be able to treat individual objects and compositions of objects uniformly.

Let’s discuss another example to have a better understanding,

Our baggage application management system has many roles for its use. So, let’s just say that we have an interface like this:

public interface Human {

String getNume();
String getPrenume();
void print();
void addResponsabil(Human responsabil);
void addManager(Human manager);
void addAdmin(Human admin);
}

We will override all these methods in Amin, Manager, and Responsabil classes to define the role and hierarchical grade.

public class Admin implements Human{

private String nume;

private String prenume;

//Getter/Setter
//Constructor

@Override
public String getNume() {
return nume;
}

@Override
public String getPrenume() {
return prenume;
}

@Override
public void print() {
System.out.println("==============================");
System.out.println("Nume : " + getNume());
System.out.println("Prenume : " + getPrenume());
System.out.println("Responsabili :");
for (Human responsabil: responsabili) {
System.out.println(" - Name : " + responsabil.getNume());
}
System.out.println("==============================");
}

private List<Human> responsabili = new ArrayList<>();
@Override
public void addResponsabil(Human responsabil) {
responsabili.add(responsabil);
}
}

Manager class:

public class Manager implements Human{

private String nume;

private String prenume;

//Getter/Setter
//Constructor

@Override
public String getNume() {
return nume;
}

@Override
public String getPrenume() {
return prenume;
}

@Override
public void print() {
System.out.println("==============================");
System.out.println("Nume : " + getNume());
System.out.println("Prenume : " + getPrenume());
System.out.println("Manageri :");
for (Human manager: admins) {
System.out.println(" - Name : " + manager.getNume());
}
System.out.println("Responsabili :");
for (Human responsabil: responsabili) {
System.out.println(" - Name : " + responsabil.getNume());
}
System.out.println("==============================");
}

private List<Human> responsabili = new ArrayList<>();
@Override
public void addResponsabil(Human responsabil) {
responsabili.add(responsabil);
}

private List<Human> admins = new ArrayList<>();
@Override
public void addManager(Human manager) {
admins.add(manager);

}

Responsabil class:

public class Responsabil implements Human{

private String nume;

private String prenume;

//Getter/Setter
//Constructor

@Override
public String getNume() {
return nume;
}

@Override
public String getPrenume() {
return prenume;
}

@Override
public void print() {
System.out.println("==============================");
System.out.println("Nume : " + getNume());
System.out.println("Prenume : " + getPrenume());
System.out.println("Manageri :");
for (Human manager: managers) {
System.out.println(" - Name : " + manager.getNume());
}
System.out.println("==============================");
}

private List<Human> managers = new ArrayList<>();
@Override
public void addManager(Human manager) {
managers.add(manager);
}

Let’s declare a main class to instantiate our objects

Main class:

public class Main {

public static void main(String[] args) {

Responsabil codrin = new Responsabil("Codrin", "Le Codrin");
Manager marin = new Manager("Marin", "Le Marin");
Manager costel = new Manager("Costel", "Le Costel");

codrin.addManager(marin);
codrin.addManager(costel);
marin.addResponsabil(codrin);
costel.addManager(marin);
costel.addResponsabil(codrin);

Admin filip = new Admin("Filip", "Filip");

filip.addResponsabil(marin);
marin.addManager(costel);

codrin.print();
marin.print();
costel.print();
}

Therefore, by using the Composite pattern, we will obtain a hierarchical representation of the roles and their subordinates.

Strategy Pattern

This is a behavioral design pattern in Java that enables you to select an algorithm at runtime. It provides a way to define a family of algorithms, encapsulate each one as an object, and make them interchangeable.

The key benefit of using the strategy pattern is that it decouples the algorithm implementation from the client code. This makes it easier to add new algorithms or change existing ones without affecting the client code. It also enables you to use the same algorithm in different contexts, without having to duplicate the code.

Let’s discuss another example to have a better understanding,

The strategy pattern is used when we have multiple algorithms for a given task and the client decides the actual implementation to be used at runtime. In our application, we already have integrated payment using cards and decided to integrate payment with bitcoin.

How can we integrate another functionality without affect the structure and the existing method for payment?

Solution: One interface is defined for each family of algorithms. The choice of payment method can be made at run-time.

Below I have attached the repository with the earlier examples as well as the complete example with the strategy Design Pattern. Do not hesitate to clone the repository and try these examples by yourself.

GitHub repository here.

--

--