Description
The singleton design pattern aims to prevent the creation of multiple objects by a class. To do so, if the object has already been created, its class will return its instance. It is one of the simplest design patterns, but it is also the most powerful. It can be implemented in 2 different ways:
- Lazy
- Eager
Scenario
🖨️ You are an employee in a company, when the employee launches a print, we check if a printer already exists. In real life, you agree that we don't create a printer for every print, that's why we're going to use the singleton to do that.
Pros and Cons
Pros | Cons |
---|---|
✔️Controlled access | ❌Procedural programming: in case this pattern is used excessively, we lose all object-oriented notion. |
✔️Reduced namespace | ❌Difficult maintenance, as malfunctions are very difficult to trace. In case of modifications/deletions, it is difficult to trace which object is using it. |
✔️Simple method refinement | ❌Reduced performance: because it represents a bottleneck due to its singularity. |
✔️Control of number of instances |
When to use it?
💡 You have recurring tasks: drivers, cache mechanism, ... 💡 Writing data to a file: logging, print jobs to a printer buffer, ...
UML Diagram
This design pattern can be used in two ways:
- Lazy
- Eager
Implementation
Let's start by creating the basis of our design pattern: the Printer class.
Lazy Printer
package com.design.singleton;
public class Printer {
private volatile static Printer printer;
private int numberOfPage = 0;
private Printer() {}
public static synchronized Printer getInstance() {
if(printer == null) {
printer = new Printer();
}
return printer;
}
public void print(String printable, int numberOfPage) {
this.numberOfPage += numberOfPage;
System.out.println("This printer has print : " + numberOfPage);
System.out.println("To be print:" + printable);
System.out.println("Number of page:" + numberOfPage);
}
}
Eager Printer
package com.design.singleton;
public class Printer {
private volatile static Printer printer = new Printer();
private int numberOfPage = 0;
private Printer() {}
public static synchronized Printer getInstance() {
return printer;
}
public void print(String printable, int numberOfPage) {
this.numberOfPage += numberOfPage;
System.out.println("This printer has print : " + numberOfPage);
System.out.println("To be print:" + printable);
System.out.println("Number of page:" + numberOfPage);
}
}
Now that our singleton is implemented, let's define our employee class:
Employee
package com.design.singleton;
public class Employee {
private String name;
public Employee(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Now, let's test all of this in our main
class:
Main
package com.design.singleton;
public class Main {
public static void main(String[] args) {
Employee employee = new Employee("John doe");
Printer printer = Printer.getInstance();
System.out.println("Employe : " + employee.getName());
printer.print("Print this text", 10);
Printer printer2 = Printer.getInstance();
System.out.println("Employe : " + employee.getName());
printer.print("Print this text again", 20);
printer.print("Print this text again again", 20);
// Check reference
System.out.println("Object printer: " + printer);
System.out.println("Object printer: " + printer2);
}
}
Result:
Employe : John doe
This printer has print: 10
To be print: Print this text
Number of page: 10
Employe : John doe
This printer has print: 20
To be print: Print this text again
Number of page: 20
This printer has print: 20
To be print: Print this text again again
Number of page: 20
Object printer: com.design.singleton.Printer@1a407d53
Object printer: com.design.singleton.Printer@1a407d53
Process finished with exit code 0
Here, we have finished our implementation. What we can notice:
- It doesn't matter if we create a new instance, the object reference remains the same.
- We can easily put conditions to load multiple instances, for example, to say that our printer can only be implemented X times.