Understanding Creational Design Patterns in Java
Creational design patterns focus on object creation mechanisms, abstracting the instantiation process, and making the system independent of how its objects are composed, represented, and varied. In Java, these patterns play a crucial role in promoting flexibility, reusability, and maintainability. In this article, we'll explore some prominent creational design patterns in Java, providing insights into their usage and benefits.
Singleton Pattern
The Singleton Pattern ensures that a class has only one instance and provides a global point of access to that instance. This pattern is beneficial when exactly one object is needed to coordinate actions across the system.
public class Singleton {
private static Singleton instance;
// Private constructor to prevent instantiation
private Singleton() {}
// Lazy initialization with double-check locking
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Real-world Scenario: Database Connection
In scenarios where creating multiple database connections is unnecessary or resource-intensive, a singleton pattern can be applied to ensure that there's only one instance of the database connection manager. This can help manage resources efficiently.
public class DatabaseConnectionManager {
private static final DatabaseConnectionManager instance = new DatabaseConnectionManager();
private DatabaseConnectionManager() {
// Initialization code
}
public static DatabaseConnectionManager getInstance() {
return instance;
}
// Other database-related methods...
}
Factory Method Pattern
The Factory Method Pattern defines an interface for creating an object but lets subclasses alter the type of objects that will be created. It provides an interface for creating instances in a superclass, but the exact type of objects is deferred to the subclasses.
interface Product {
void create();
}
class ConcreteProductA implements Product {
@Override
public void create() {
System.out.println("Product A created.");
}
}
class ConcreteProductB implements Product {
@Override
public void create() {
System.out.println("Product B created.");
}
}
abstract class Creator {
// Factory method
abstract Product factoryMethod();
// Other methods in the class
}
class ConcreteCreatorA extends Creator {
@Override
Product factoryMethod() {
return new ConcreteProductA();
}
}
class ConcreteCreatorB extends Creator {
@Override
Product factoryMethod() {
return new ConcreteProductB();
}
}
Real-world Scenario: Document Conversion
Consider a document conversion system where different document types (PDF, Word, Excel) need to be converted to a common format. The factory method pattern can be applied to create document converters for each type.
interface Document {
void convert();
}
class PDFDocument implements Document {
@Override
public void convert() {
System.out.println("Converting PDF to common format...");
}
}
class WordDocument implements Document {
@Override
public void convert() {
System.out.println("Converting Word to common format...");
}
}
class ExcelDocument implements Document {
@Override
public void convert() {
System.out.println("Converting Excel to common format...");
}
}
abstract class DocumentConverter {
abstract Document createDocument();
}
class PDFConverter extends DocumentConverter {
@Override
Document createDocument() {
return new PDFDocument();
}
}
class WordConverter extends DocumentConverter {
@Override
Document createDocument() {
return new WordDocument();
}
}
class ExcelConverter extends DocumentConverter {
@Override
Document createDocument() {
return new ExcelDocument();
}
}
Abstract Factory Pattern
The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is particularly useful when a system should be independent of how its objects are created, composed, and represented.
// Abstract product interfaces
interface CPU {
String getArchitecture();
}
interface RAM {
String getType();
}
// Concrete product classes
class IntelCPU implements CPU {
@Override
public String getArchitecture() {
return "Intel Architecture";
}
}
class SamsungRAM implements RAM {
@Override
public String getType() {
return "Samsung RAM";
}
}
// Abstract Factory interface
interface ComputerFactory {
CPU createCPU();
RAM createRAM();
}
// Concrete Factory
class HighPerformanceComputerFactory implements ComputerFactory {
@Override
public CPU createCPU() {
return new IntelCPU();
}
@Override
public RAM createRAM() {
return new SamsungRAM();
}
}
Real-world Scenario: GUI Library
In a graphical user interface (GUI) library, the abstract factory pattern can be used to create platform-specific UI components. For instance, a ButtonFactory
can produce buttons with native styles for Windows, macOS, and Linux.
// Abstract product interfaces
interface Button {
void render();
}
interface Window {
void setTitle(String title);
void renderWindow();
}
// Concrete product classes for Windows
class WindowsButton implements Button {
@Override
public void render() {
System.out.println("Rendering Windows-style button");
}
}
class WindowsWindow implements Window {
@Override
public void setTitle(String title) {
System.out.println("Setting title for Windows window: " + title);
}
@Override
public void renderWindow() {
System.out.println("Rendering Windows-style window");
}
}
// Concrete Factory for Windows
class WindowsUIFactory implements UIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Window createWindow() {
return new WindowsWindow();
}
}
// Client code
UIFactory factory = new WindowsUIFactory();
Button button = factory.createButton();
Window window = factory.createWindow();
Builder Pattern
The Builder Pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It is useful when an object has a large number of parameters or configurations.
class Product {
private String part1;
private String part2;
// Private constructor
private Product() {}
// Setters (optional, depending on immutability requirements)
@Override
public String toString() {
return "Product{" +
"part1='" + part1 + '\'' +
", part2='" + part2 + '\'' +
'}';
}
// Builder class
static class Builder {
private Product product = new Product();
Builder withPart1(String part1) {
product.part1 = part1;
return this;
}
Builder withPart2(String part2) {
product.part2 = part2;
return this;
}
Product build() {
return product;
}
}
}
public class Client {
public static void main(String[] args) {
Product product = new Product.Builder()
.withPart1("Part 1")
.withPart2("Part 2")
.build();
System.out.println(product);
}
}
Real-world Scenario: Report Generation
Consider a scenario where reports with varying structures and contents need to be generated. The builder pattern can be used to create a ReportBuilder
which allows the flexible construction of different types of reports.
class Report {
private String title;
private String header;
private String content;
// Private constructor
private Report() {}
// Getters...
@Override
public String toString() {
return "Report{" +
"title='" + title + '\'' +
", header='" + header + '\'' +
", content='" + content + '\'' +
'}';
}
// Builder class
static class Builder {
private final Report report = new Report();
Builder withTitle(String title) {
report.title = title;
return this;
}
Builder withHeader(String header) {
report.header = header;
return this;
}
Builder withContent(String content) {
report.content = content;
return this;
}
Report build() {
return report;
}
}
}
public class ReportGenerator {
public static void main(String[] args) {
Report report = new Report.Builder()
.withTitle("Monthly Sales Report")
.withHeader("Sales Summary")
.withContent("This report summarizes the monthly sales data.")
.build();
System.out.println(report);
}
}
Subscribe to my newsletter
Read articles from Better Dev XP directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by