Design patterns are reusable solutions to common problems that occur in software design. They represent best practices for solving specific design problems and provide general templates for creating robust and maintainable software. These patterns are not frameworks or libraries; instead, they are templates that can be adapted to address various situations in software development.
There are three main types of design patterns:
Creational design patterns
Structural design patterns
Behavioral design patterns
They are subdivided into many different patterns. Let's get started with Creational design patterns.
1) Creational Design Pattern
Singleton
The Singleton Design Pattern is a creational pattern that ensures a class has only one instance and provides a global point of access to that instance. It is often used when exactly one object is needed to coordinate actions across the system. The Singleton pattern is commonly employed to control access to resources such as database connections or logging services.
Here's a simple example of how you might implement the Singleton pattern in Java:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Builder
The Builder Design Pattern is a creational pattern that separates the construction of a complex object from its representation. It allows the same construction process to create different representations of an object. This pattern is especially useful when an object needs to be created with a large number of optional parameters, and not all combinations of those parameters make sense.
Example:
// Product class representing the order
class Order {
private String burger;
private String fries;
private String drink;
public void setBurger(String burger) {
this.burger = burger;
}
public void setFries(String fries) {
this.fries = fries;
}
public void setDrink(String drink) {
this.drink = drink;
}
@Override
public String toString() {
return "Order: Burger - " + burger + ", Fries - " + fries + ", Drink - " + drink;
}
}
// Builder interface defining the steps to build an order
interface RestaurantCrew {
void buildBurger();
void buildFries();
void buildDrink();
Order getOrder();
}
// Concrete builder implementing the steps to build an order
class ConcreteRestaurantCrew implements RestaurantCrew {
private Order order;
public ConcreteRestaurantCrew() {
this.order = new Order();
}
@Override
public void buildBurger() {
order.setBurger("Classic Burger");
}
@Override
public void buildFries() {
order.setFries("Large Fries");
}
@Override
public void buildDrink() {
order.setDrink("Cola");
}
@Override
public Order getOrder() {
return order;
}
}
// Director class (Cashier) that instructs the builder to build an order
class Cashier {
private RestaurantCrew restaurantCrew;
public Cashier(RestaurantCrew restaurantCrew) {
this.restaurantCrew = restaurantCrew;
}
public void takeOrder() {
restaurantCrew.buildBurger();
restaurantCrew.buildFries();
restaurantCrew.buildDrink();
}
public Order serveOrder() {
return restaurantCrew.getOrder();
}
}
// Client (Customer) code
public class Customer {
public static void main(String[] args) {
// Customer wants to order a meal
RestaurantCrew restaurantCrew = new ConcreteRestaurantCrew();
Cashier cashier = new Cashier(restaurantCrew);
// Cashier takes the order
cashier.takeOrder();
// Cashier serves the order to the customer
Order order = cashier.serveOrder();
// Display the order
System.out.println(order);
}
}
2) Structural design patterns
Adapter
When two incompatible interfaces cannot be connected directly, an adapter pattern serves as a bridge. This pattern's primary objective is to change an existing interface into one that the client expects.
This pattern's structure is comparable to that of the Decorator. Nonetheless, the extension is typically taken into consideration when using the Decorator. Usually, when the initial code to connect incompatible interfaces is developed, the Adapter is implemented.
Example:
// Target interface representing a basic socket
interface BasicSocket {
void plugIn();
}
// Adaptee class representing a basic socket implementation
class ConcreteBasicSocket implements BasicSocket {
@Override
public void plugIn() {
System.out.println("Plugged into a basic socket.");
}
}
// Adapter class adapting a basic socket to a ratchet socket
class RatchetSocketAdapter implements BasicSocket {
private ConcreteBasicSocket basicSocket;
public RatchetSocketAdapter(ConcreteBasicSocket basicSocket) {
this.basicSocket = basicSocket;
}
@Override
public void plugIn() {
basicSocket.plugIn();
System.out.println("Converted to a ratchet socket.");
}
}
// Client code using a ratchet socket
public class Client {
public static void main(String[] args) {
// Using a basic socket
ConcreteBasicSocket basicSocket = new ConcreteBasicSocket();
basicSocket.plugIn();
// Adapting a basic socket to a ratchet socket using an adapter
RatchetSocketAdapter ratchetAdapter = new RatchetSocketAdapter(basicSocket);
ratchetAdapter.plugIn();
}
}
Decorator
A structural pattern called the Decorator Design Pattern makes it possible to add functionality to a single object—either statically or dynamically—without influencing the behavior of other objects in the same class. It is helpful for adding features to classes in a reusable and adaptable manner.
Example:
// Component interface representing the base weapon
interface BaseWeapon {
void fire();
}
// Concrete component representing a basic weapon
class ConcreteWeapon implements BaseWeapon {
@Override
public void fire() {
System.out.println("Firing a basic weapon");
}
}
// Decorator interface representing weapon accessories
interface WeaponAccessory extends BaseWeapon {
}
// Concrete decorator representing a scope accessory
class Scope implements WeaponAccessory {
private BaseWeapon weapon;
public Scope(BaseWeapon weapon) {
this.weapon = weapon;
}
@Override
public void fire() {
weapon.fire();
System.out.println("with a Scope");
}
}
// Concrete decorator representing a silencer accessory
class Silencer implements WeaponAccessory {
private BaseWeapon weapon;
public Silencer(BaseWeapon weapon) {
this.weapon = weapon;
}
@Override
public void fire() {
weapon.fire();
System.out.println("with a Silencer");
}
}
// Client code using the decorated weapon
public class Client {
public static void main(String[] args) {
// Creating a basic weapon
BaseWeapon basicWeapon = new ConcreteWeapon();
basicWeapon.fire();
// Decorating the weapon with a scope
WeaponAccessory scopedWeapon = new Scope(basicWeapon);
scopedWeapon.fire();
// Decorating the weapon with a silencer
WeaponAccessory silencedWeapon = new Silencer(basicWeapon);
silencedWeapon.fire();
// Decorating the weapon with both a scope and a silencer
WeaponAccessory scopedAndSilencedWeapon = new Silencer(new Scope(basicWeapon));
scopedAndSilencedWeapon.fire();
}
}
3) Behavioral Patterns
Strategy
The Strategy Design Pattern is a behavioral pattern that defines a family of algorithms, encapsulates each algorithm, and makes them interchangeable. It allows a client to choose an algorithm from a family of algorithms at runtime, independently of the clients that use it. The pattern defines a common interface for all supported algorithms, making them interchangeable.
Example
// Strategy interface representing different transportation strategies
interface TravelStrategy {
void travel();
}
// Concrete strategy representing traveling to the airport by car
class Car implements TravelStrategy {
@Override
public void travel() {
System.out.println("Traveling to the airport by car");
}
}
// Concrete strategy representing traveling to the airport by city bus
class CityBus implements TravelStrategy {
@Override
public void travel() {
System.out.println("Traveling to the airport by city bus");
}
}
// Concrete strategy representing traveling to the airport by taxi
class Taxi implements TravelStrategy {
@Override
public void travel() {
System.out.println("Traveling to the airport by taxi");
}
}
// Context class that uses a travel strategy
class TransportationToAirport {
private TravelStrategy travelStrategy;
public void setTravelStrategy(TravelStrategy travelStrategy) {
this.travelStrategy = travelStrategy;
}
public void goToAirport() {
System.out.print("Starting journey: ");
travelStrategy.travel();
}
}
// Client code using different transportation strategies
public class Client {
public static void main(String[] args) {
// Creating transportation context
TransportationToAirport transportationContext = new TransportationToAirport();
// Traveling to the airport by car
transportationContext.setTravelStrategy(new Car());
transportationContext.goToAirport();
// Traveling to the airport by city bus
transportationContext.setTravelStrategy(new CityBus());
transportationContext.goToAirport();
// Traveling to the airport by taxi
transportationContext.setTravelStrategy(new Taxi());
transportationContext.goToAirport();
}
}
Template
An algorithm's skeleton is defined in the superclass using the Template Method Design Pattern, a behavioral pattern that permits subclasses to override certain stages without altering the algorithm's overall structure. It's employed when you have a standard algorithm that requires a few minor adjustments to specific steps.
Example
// Abstract class representing a Worker
abstract class Worker {
// Template method defining the daily routine of a worker
public final void workDay() {
arriveAtWork();
performDuties();
departFromWork();
}
// Concrete methods shared by all workers
private void arriveAtWork() {
System.out.println("Arriving at work");
}
private void departFromWork() {
System.out.println("Leaving work");
}
// Abstract method to be implemented by subclasses
protected abstract void performDuties();
}
// Concrete class representing a Firefighter
class Firefighter extends Worker {
@Override
protected void performDuties() {
System.out.println("Fighting fires");
}
}
// Concrete class representing a Lumberjack
class Lumberjack extends Worker {
@Override
protected void performDuties() {
System.out.println("Cutting down trees");
}
}
// Concrete class representing a Postman
class Postman extends Worker {
@Override
protected void performDuties() {
System.out.println("Delivering mail");
}
}
// Concrete class representing a Manager
class Manager extends Worker {
@Override
protected void performDuties() {
System.out.println("Managing the team");
}
}
// Client code using the template method
public class Client {
public static void main(String[] args) {
// Creating instances of different workers
Worker firefighter = new Firefighter();
Worker lumberjack = new Lumberjack();
Worker postman = new Postman();
Worker manager = new Manager();
// Performing the daily routine for each worker
System.out.println("Firefighter's Day:");
firefighter.workDay();
System.out.println("\nLumberjack's Day:");
lumberjack.workDay();
System.out.println("\nPostman's Day:");
postman.workDay();
System.out.println("\nManager's Day:");
manager.workDay();
}
}
Conclusion
Design patterns are incredibly useful resources for resolving typical software development issues. Developers can produce code that is more scalable, efficient, and maintainable by comprehending and utilizing these patterns. Every pattern gives a tested fix for a particular problem.
Commentaires