This is a start for a series of blogs about Design Patterns. In this blog, we will discuss the first type of design pattern, Creational Patterns. Here the types which fall under creational patterns will be discussed with some real-world examples. I will be using Java as my language of selection.
What are Design Patterns?
Design patterns play a crucial role in software development, providing proven solutions to common problems and promoting best practices. They are like pre-made blueprints that you can customize to solve a recurring design problem in your code.
Exploring Creational Design Patterns in Java
In object-oriented programming, creational design patterns play a significant role since they make it possible for object instantiation to be separated from their utilization thereby increasing the flexibility and scalability of object creation. This blog post will focus on five main types of creational design patterns: Factory Method, Abstract Factory, Builder, Prototype and Singleton. To show how each one works, we will use real-world examples in Java.
1. Factory Method
The Factory Method pattern defines an interface for creating an object but allows subclasses to alter the type of objects that will be created. This pattern supports loose coupling in Java to stop the need to bind application-specific classes into the code.
Real-life Scenario: Consider a logistics company that transports goods using various vehicles such as trucks and ships. The type of vehicle depends on the transport mode required.
// Product Interface
interface Transport {
void deliver();
}
// Concrete Products
class Truck implements Transport {
@Override
public void deliver() {
System.out.println("Deliver by land in a truck.");
}
}
class Ship implements Transport {
@Override
public void deliver() {
System.out.println("Deliver by sea in a ship.");
}
}
// Creator
abstract class Logistics {
public abstract Transport createTransport();
public void planDelivery() {
Transport transport = createTransport();
transport.deliver();
}
}
// Concrete Creators
class RoadLogistics extends Logistics {
@Override
public Transport createTransport() {
return new Truck();
}
}
class SeaLogistics extends Logistics {
@Override
public Transport createTransport() {
return new Ship();
}
}
// let's call the main class
public class Main {
public static void main(String[] args) {
Logistics logistics = new RoadLogistics();
logistics.planDelivery();
logistics = new SeaLogistics();
logistics.planDelivery();
}
}
2. Abstract Factory
The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is useful when the system needs to be independent of how its objects are created.
Real-life Scenario: Imagine a furniture store that sells different types of furniture sets, such as Victorian and Modern. Each set includes products like chairs and sofas.
// Abstract Products
interface Chair {
void sitOn();
}
interface Sofa {
void lieOn();
}
// Concrete Products
class VictorianChair implements Chair {
@Override
public void sitOn() {
System.out.println("Sitting on a Victorian chair.");
}
}
class ModernChair implements Chair {
@Override
public void sitOn() {
System.out.println("Sitting on a Modern chair.");
}
}
class VictorianSofa implements Sofa {
@Override
public void lieOn() {
System.out.println("Lying on a Victorian sofa.");
}
}
class ModernSofa implements Sofa {
@Override
public void lieOn() {
System.out.println("Lying on a Modern sofa.");
}
}
// Abstract Factory
interface FurnitureFactory {
Chair createChair();
Sofa createSofa();
}
// Concrete Factories
class VictorianFurnitureFactory implements FurnitureFactory {
@Override
public Chair createChair() {
return new VictorianChair();
}
@Override
public Sofa createSofa() {
return new VictorianSofa();
}
}
class ModernFurnitureFactory implements FurnitureFactory {
@Override
public Chair createChair() {
return new ModernChair();
}
@Override
public Sofa createSofa() {
return new ModernSofa();
}
}
// Client code
public class Main {
private static void createFurniture(FurnitureFactory factory) {
Chair chair = factory.createChair();
Sofa sofa = factory.createSofa();
chair.sitOn();
sofa.lieOn();
}
public static void main(String[] args) {
FurnitureFactory victorianFactory = new VictorianFurnitureFactory();
createFurniture(victorianFactory);
FurnitureFactory modernFactory = new ModernFurnitureFactory();
createFurniture(modernFactory);
}
}
3. Builder
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It is particularly useful for creating objects with many optional attributes.
Real-life Scenario: Consider an online pizza ordering system where customers can customize their pizzas with various toppings, sizes, and crust types.
// Product
class Pizza {
private String dough = "";
private String sauce = "";
private String topping = "";
public void setDough(String dough) { this.dough = dough; }
public void setSauce(String sauce) { this.sauce = sauce; }
public void setTopping(String topping) { this.topping = topping; }
@Override
public String toString() {
return "Pizza [dough=" + dough + ", sauce=" + sauce + ", topping=" + topping + "]";
}
}
// Builder Interface
interface PizzaBuilder {
void buildDough();
void buildSauce();
void buildTopping();
Pizza getPizza();
}
// Concrete Builders
class HawaiianPizzaBuilder implements PizzaBuilder {
private Pizza pizza;
public HawaiianPizzaBuilder() {
this.pizza = new Pizza();
}
@Override
public void buildDough() { pizza.setDough("cross"); }
@Override
public void buildSauce() { pizza.setSauce("mild"); }
@Override
public void buildTopping() { pizza.setTopping("ham+pineapple"); }
@Override
public Pizza getPizza() { return this.pizza; }
}
class SpicyPizzaBuilder implements PizzaBuilder {
private Pizza pizza;
public SpicyPizzaBuilder() {
this.pizza = new Pizza();
}
@Override
public void buildDough() { pizza.setDough("pan baked"); }
@Override
public void buildSauce() { pizza.setSauce("hot"); }
@Override
public void buildTopping() { pizza.setTopping("pepperoni+salami"); }
@Override
public Pizza getPizza() { return this.pizza; }
}
// Director
class Waiter {
private PizzaBuilder pizzaBuilder;
public void setPizzaBuilder(PizzaBuilder pb) { pizzaBuilder = pb; }
public Pizza getPizza() { return pizzaBuilder.getPizza(); }
public void constructPizza() {
pizzaBuilder.buildDough();
pizzaBuilder.buildSauce();
pizzaBuilder.buildTopping();
}
}
// Client code
public class Main {
public static void main(String[] args) {
Waiter waiter = new Waiter();
PizzaBuilder hawaiianPizzaBuilder = new HawaiianPizzaBuilder();
PizzaBuilder spicyPizzaBuilder = new SpicyPizzaBuilder();
waiter.setPizzaBuilder(hawaiianPizzaBuilder);
waiter.constructPizza();
Pizza pizza1 = waiter.getPizza();
System.out.println("Pizza built: " + pizza1);
waiter.setPizzaBuilder(spicyPizzaBuilder);
waiter.constructPizza();
Pizza pizza2 = waiter.getPizza();
System.out.println("Pizza built: " + pizza2);
}
}
4. Prototype
The Prototype pattern is used to create a new object by copying an existing object, known as the prototype. This pattern is useful when the cost of creating a new object is expensive.
Real-life Scenario: Think of a graphical editor where you can create, duplicate, and edit shapes.
import java.util.HashMap;
import java.util.Map;
// Prototype
abstract class Shape implements Cloneable {
private String id;
protected String type;
abstract void draw();
public String getType() { return type; }
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}
// Concrete Prototypes
class Rectangle extends Shape {
public Rectangle() { type = "Rectangle"; }
@Override
public void draw() { System.out.println("Drawing a Rectangle."); }
}
class Circle extends Shape {
public Circle() { type = "Circle"; }
@Override
public void draw() { System.out.println("Drawing a Circle."); }
}
// Prototype Registry
class ShapeCache {
private static Map<String, Shape> shapeMap = new HashMap<>();
public static Shape getShape(String shapeId) {
Shape cachedShape = shapeMap.get(shapeId);
return (Shape) cachedShape.clone();
}
public static void loadCache() {
Rectangle rectangle = new Rectangle();
rectangle.setId("1");
shapeMap.put(rectangle.getId(), rectangle);
Circle circle = new Circle();
circle.setId("2");
shapeMap.put(circle.getId(), circle);
}
}
// Client code
public class Main {
public static void main(String[] args) {
ShapeCache.loadCache();
Shape clonedShape1 = ShapeCache.getShape("1");
System.out.println("Shape: " + clonedShape1.getType());
Shape clonedShape2 = ShapeCache.getShape("2");
System.out.println("Shape: " + clonedShape2.getType());
}
}
5. Singleton
The Singleton pattern ensures a class has only one instance and provides a global point of access to it. This pattern is commonly used for logging, caching, and thread pools.
Real-life Scenario: Imagine a printer spooler where only one instance should manage all print jobs.
class PrinterSpooler {
private static PrinterSpooler instance;
private PrinterSpooler() {
// private constructor to prevent instantiation
}
public static PrinterSpooler getInstance() {
if (instance == null) {
instance = new PrinterSpooler();
}
return instance;
}
public void print(String document) {
System.out.println("Printing document: " + document);
}
}
// Client code
public class Main {
public static void main(String[] args) {
PrinterSpooler spooler1 = PrinterSpooler.getInstance();
PrinterSpooler spooler2 = PrinterSpooler.getInstance();
spooler1.print("Document 1");
spooler2.print("Document 2");
System.out.println("Are both spoolers the same instance? " + (spooler1 == spooler2));
}
}
References
https://www.javatpoint.com/design-patterns-in-java
https://www.digitalocean.com/community/tutorials/java-design-patterns-example-tutorial
Top comments (0)