Design Pattern — Types, Cases and Examples

Sapan Kumar Mohanty
13 min readMay 10, 2024

--

Design Pattern — Structure, Behavior, Interface

In software development, “design” refers to the process of defining the architecture, components, interfaces, and other characteristics of a system or component. It is a blueprint or plan for what you’re building and involves considering various aspects such as:

  • Structure: How components within the system are organized and interact with each other.
  • Behavior: How the system behaves in response to various inputs or events.
  • Interface: The way different components of the system interact with each other and with external systems.
  • Interaction: How the system will meet the requirements of the users and other stakeholders.

Design is crucial because it directly affects the quality, maintainability, scalability, and performance of the software. It is often the stage where developers decide how to tackle certain problems, choose technologies and frameworks, and set standards and guidelines for the project.

Design Pattern

A “design pattern” is a reusable solution to a commonly occurring problem within a given context in software design. Design patterns are formalized best practices that the programmer can use to solve common problems when designing an application or system.

Key Characteristics of Design Patterns:

  • Reusable: Design patterns provide solutions that are not tied to a specific problem, making them reusable in different scenarios.
  • Proven: These patterns have been evolved and proven over time by the developer community, ensuring they are reliable.
  • Expressive: They provide a standard terminology and are specific to particular scenarios, making them easy to understand and communicate.

Categories of Design Patterns

Categories of Design Patterns

Design patterns are typically divided into three categories:

  1. Creational Patterns: These focus on the design of object instantiation. They are used when a decision must be made about which class to instantiate or in what manner. Examples include Singleton, Factory Method, Builder, and Prototype.
  2. Structural Patterns: These deal with the composition of classes or objects. Structural patterns help ensure that if one part of a system changes, the entire system doesn’t need to do the same. They help in making the system easier to understand and manage. Examples include Adapter, Composite, Proxy, and Decorator.
  3. Behavioral Patterns: These focus on communication between objects. They help make complex flows easier to manage and more scalable. Examples include Observer, Strategy, Command, and Iterator.

Design patterns help to speed up the development process by providing tested, proven development paradigms. Effective software design requires considering issues that may not become visible until later in the implementation or maintaining the system. Reusing design patterns helps to prevent subtle issues that can cause major problems and improves code readability for coders and architects familiar with the patterns.

Type Of Patterns

1. Singleton Pattern

Use Case: Ensures a class has only one instance, and provides a global point of access to it.

Details: Often used in configurations, thread pool management, and caching. It can be implemented using various thread-safe techniques and can help in resource management by avoiding multiple object creation.

Type: Creational

Commonly used for: Ensuring a class has only one instance and providing a global point of access to it. Often used for managing resources like database connections, logging, or configuration settings.

Example: A logging class that centralises the logging mechanism throughout an application. All parts of the application use this single instance to log various messages to a file, examples in PHP & JAVA.

<?php
class Logger {
private static $instance = null;

// Private constructor to prevent instantiation
private function __construct() {
// Initialize logging mechanism (e.g., open log file)
}

// Prevent cloning of the instance
private function __clone() { }

// Prevent unserializing of the instance
private function __wakeup() { }

// Public method to provide access to the single instance
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new Logger();
}
return self::$instance;
}

// Method to log messages
public function log($message) {
// Log the message (e.g., write to a file)
echo $message . PHP_EOL;
}
}

// Usage
$logger = Logger::getInstance();
$logger->log("This is a log message.");

$anotherLogger = Logger::getInstance();
$anotherLogger->log("This is another log message.");

// Both $logger and $anotherLogger refer to the same instance
?>
public class Logger {
private static Logger instance;

private Logger() {
// Private constructor to prevent instantiation
}

public static Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}

public void log(String message) {
System.out.println(message);
}
}

2. Factory Pattern

Use Case: Used to create objects without specifying the exact class of object that will be created.

Details: This is a part of the creational pattern group that works on the principle of encapsulating the object creation process. It can help manage and maintain code when objects share a common interface.

Type: Creational

Commonly used for: Creating objects without specifying the exact class of object that will be created. It’s used in scenarios where a class cannot anticipate the class of objects it must create.

Example: A UI control factory that creates different types of controls based on input. It can create buttons, text boxes, or sliders without exposing creation logic to the client.

<?php

interface Control {
public function render();
}

class Button implements Control {
public function render() {
echo "Render Button\n";
}
}

class TextBox implements Control {
public function render() {
echo "Render TextBox\n";
}
}

class ControlFactory {
public static function createControl($type) {
if (strtolower($type) == "button") {
return new Button();
} elseif (strtolower($type) == "textbox") {
return new TextBox();
}
return null; // Optionally, throw an exception or handle errors as needed
}
}

// Example usage:
$button = ControlFactory::createControl("button");
$button->render();

$textBox = ControlFactory::createControl("textbox");
$textBox->render();

?>
public interface Control {
void render();
}

public class Button implements Control {
public void render() {
System.out.println("Render Button");
}
}

public class TextBox implements Control {
public void render() {
System.out.println("Render TextBox");
}
}

public class ControlFactory {
public static Control createControl(String type) {
if (type.equalsIgnoreCase("button")) {
return new Button();
} else if (type.equalsIgnoreCase("textbox")) {
return new TextBox();
}
return null;
}
}

3. Observer Pattern

Use Case: A one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Details: Useful in GUI applications, event management systems, and real-time data feeds. Helps in establishing a low-coupling between the subject and observers.

Type: Behavioral

Commonly used for: Defining a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Frequently used in event handling systems.

Example: An application where the user interface needs to be updated whenever the data model changes.

<?php

interface Observer {
public function update();
}

class DataModel {
private $observers = [];

public function addObserver(Observer $observer) {
$this->observers[] = $observer;
}

public function changeData() {
// Simulate data change
$this->notifyObservers();
}

private function notifyObservers() {
foreach ($this->observers as $observer) {
$observer->update();
}
}
}

class UserInterface implements Observer {
public function update() {
echo "UI Update: Data has changed!\n";
}
}

// Example usage:
$dataModel = new DataModel();
$ui = new UserInterface();

$dataModel->addObserver($ui);
$dataModel->changeData(); // Will output: UI Update: Data has changed!
?>
public interface Observer {
void update();
}

public class DataModel {
private List<Observer> observers = new ArrayList<>();

public void addObserver(Observer observer) {
observers.add(observer);
}

public void changeData() {
// Data changes
notifyObservers();
}

private void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}

4. Strategy Pattern

Use Case: Consider an application that provides different payment methods. We want to allow users to select their preferred payment method at runtime. The Strategy Pattern is suitable here as it enables selecting an algorithm (payment method) at runtime.

Type: Behavioral

Commonly used for: Defining a family of algorithms, encapsulating each one, and making them interchangeable. This pattern allows the algorithm to vary independently from clients that use it. It’s often used in scenarios where multiple algorithms are available for a specific task.

Example:

Step 1: Define the Strategy Interface
First, we create an interface that defines a method `pay` which all concrete payment strategies will implement.

<?php
interface PaymentStrategy {
public function pay($amount);
}
?>

Step 2: Implement Concrete Strategies
Next, we implement different payment strategies: Credit Card, PayPal, and Bitcoin.

<?php
class CreditCardPayment implements PaymentStrategy {
private $name;
private $cardNumber;
private $cvv;
private $dateOfExpiry;

public function __construct($name, $cardNumber, $cvv, $dateOfExpiry) {
$this->name = $name;
$this->cardNumber = $cardNumber;
$this->cvv = $cvv;
$this->dateOfExpiry = $dateOfExpiry;
}

public function pay($amount) {
echo "Paid $amount using Credit Card.\n";
}
}

class PayPalPayment implements PaymentStrategy {
private $email;
private $password;

public function __construct($email, $password) {
$this->email = $email;
$this->password = $password;
}

public function pay($amount) {
echo "Paid $amount using PayPal.\n";
}
}

class BitcoinPayment implements PaymentStrategy {
private $walletAddress;

public function __construct($walletAddress) {
$this->walletAddress = $walletAddress;
}

public function pay($amount) {
echo "Paid $amount using Bitcoin.\n";
}
}
?>

Step 3: Create the Context Class
The context class uses a `PaymentStrategy` to perform the payment. This class can change its payment strategy at runtime.

<?php
class ShoppingCart {
private $paymentStrategy;
public function setPaymentStrategy(PaymentStrategy $paymentStrategy) {
$this->paymentStrategy = $paymentStrategy;
}
public function checkout($amount) {
$this->paymentStrategy->pay($amount);
}
}
?>

Step 4: Using the Strategy Pattern
Now, let’s use the strategy pattern in action.

<?php
// Include the files containing the strategy and context classes
include 'PaymentStrategy.php';
include 'CreditCardPayment.php';
include 'PayPalPayment.php';
include 'BitcoinPayment.php';
include 'ShoppingCart.php';
// Example usage
$shoppingCart = new ShoppingCart();
// Pay using Credit Card
$creditCardPayment = new CreditCardPayment("John Doe", "1234567890123456", "123", "12/23");
$shoppingCart->setPaymentStrategy($creditCardPayment);
$shoppingCart->checkout(100);
// Pay using PayPal
$payPalPayment = new PayPalPayment("john.doe@example.com", "securepassword");
$shoppingCart->setPaymentStrategy($payPalPayment);
$shoppingCart->checkout(200);
// Pay using Bitcoin
$bitcoinPayment = new BitcoinPayment("1HcMhzXxQ8zG4m6XZYr4U5RkR2D4w1L7Pe");
$shoppingCart->setPaymentStrategy($bitcoinPayment);
$shoppingCart->checkout(300);
?>

Explanation:

1. Strategy Interface: `PaymentStrategy` defines the `pay` method that each payment strategy must implement.
2. Concrete Strategies: `CreditCardPayment`, `PayPalPayment`, and `BitcoinPayment` implement the `PaymentStrategy` interface.
3. Context Class: `ShoppingCart` uses a `PaymentStrategy` to perform the payment and allows changing the payment strategy at runtime.
4. Client Code: The client (main script) creates instances of `ShoppingCart` and different payment strategies, sets the strategy on the shopping cart, and performs the checkout process.

This example demonstrates how the Strategy Pattern allows for easy extension and modification of algorithms (payment methods) without changing the context class.

5. Adapter Pattern

Use Case: Converts the interface of a class into another interface clients expect.

Details: Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces. It is especially useful when trying to integrate new features or components into existing systems.

Type: Structural

Commonly used for: Allowing incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces. Often used when you want to use an existing class but its interface does not match the one you need.

Example: An application needs to integrate with a third-party library where the interface is incompatible with the existing application code.

<?php

// Define the new library interface
interface NewLibraryInterface {
public function newRequest();
}

// Define the old library class with an incompatible interface
class OldLibraryClass {
public function oldRequest() {
echo "Old request method\n";
}
}

// Define the adapter class that implements the new interface and adapts the old class
class LibraryAdapter implements NewLibraryInterface {
private $oldLibrary;

public function __construct(OldLibraryClass $oldLibrary) {
$this->oldLibrary = $oldLibrary;
}

public function newRequest() {
$this->oldLibrary->oldRequest();
}
}

// Client code that uses the new interface
function clientCode(NewLibraryInterface $library) {
$library->newRequest();
}

// Usage
$oldLibrary = new OldLibraryClass();
$adapter = new LibraryAdapter($oldLibrary);

// Now the client can use the new interface to interact with the old library
clientCode($adapter);

?>
public interface NewLibraryInterface {
void newRequest();
}

public class OldLibraryClass {
public void oldRequest() {
System.out.println("Old request method");
}
}

public class LibraryAdapter implements NewLibraryInterface {
private OldLibraryClass oldLibrary;

public LibraryAdapter(OldLibraryClass oldLibrary) {
this.oldLibrary = oldLibrary;
}

public void newRequest() {
oldLibrary.oldRequest();
}
}

6. Load Balancer Pattern

Use Case: Distributes incoming network traffic across multiple backend servers to increase throughput, enhance the responsiveness, and ensure the availability of applications.

Details: A fundamental aspect in designing highly available and scalable systems. Common strategies include round-robin, least connections, and IP-hash.

Type: Structural

Commonly used for: Distributing incoming network traffic across multiple servers to ensure no single server bears too much demand. It improves application availability and reliability.

Example: Implementing a simple round-robin load balancer in PHP & Java to distribute incoming requests evenly across a list of servers.

<?php

class LoadBalancer {
private $servers;
private $currentServer;

public function __construct(array $servers) {
$this->servers = $servers;
$this->currentServer = 0;
}

public function getServer() {
$server = $this->servers[$this->currentServer];
$this->currentServer = ($this->currentServer + 1) % count($this->servers);
return $server;
}
}

// Example usage
$servers = ["Server1", "Server2", "Server3"];
$loadBalancer = new LoadBalancer($servers);

// Simulating incoming requests
for ($i = 0; $i < 10; $i++) {
echo $loadBalancer->getServer() . "\n";
}
?>
import java.util.List;

public class LoadBalancer {

private List<String> servers;
private int currentServer;

public LoadBalancer(List<String> servers) {
this.servers = servers;
this.currentServer = 0;
}

public String getServer() {
String server = servers.get(currentServer);
currentServer = (currentServer + 1) % servers.size();
return server;
}

public static void main(String[] args) {
List<String> servers = List.of("Server1", "Server2", "Server3");
LoadBalancer lb = new LoadBalancer(servers);

for (int i = 0; i < 10; i++) {
System.out.println(lb.getServer());
}
}
}

This simple load balancer cycles through a list of servers and distributes requests evenly, ensuring no single server is overwhelmed.

7. Circuit Breaker Pattern

Use Case: Handles the failure and latency of an unresponsive service elegantly.

Details: When a system fails to respond, the circuit breaker temporarily halts operations to the service. It’s crucial in microservices architectures to prevent a network or service failure from cascading to other services.

Type: Behavioral

Commonly used for: Preventing a network or service failure from constantly triggering requests, which would waste resources. It’s used in microservices architectures to handle failures gracefully.

Example: Implementing a basic circuit breaker in PHP to handle failures when making API calls.

<?php

class CircuitBreaker {
private $failureThreshold;
private $recoveryTimeout;
private $failures;
private $lastAttempt;
private $state;

public function __construct($failureThreshold, $recoveryTimeout) {
$this->failureThreshold = $failureThreshold;
$this->recoveryTimeout = $recoveryTimeout;
$this->failures = 0;
$this->lastAttempt = null;
$this->state = "CLOSED";
}

public function call($url) {
if ($this->state === "OPEN" && time() < $this->lastAttempt + $this->recoveryTimeout) {
throw new Exception("Circuit is open");
}

try {
$response = file_get_contents($url);
if ($response === FALSE) {
throw new Exception("Request failed");
}
$this->state = "CLOSED";
$this->failures = 0;
return $response;
} catch (Exception $e) {
$this->failures++;
$this->lastAttempt = time();
if ($this->failures >= $this->failureThreshold) {
$this->state = "OPEN";
}
throw $e;
}
}
}

// Example Usage
$circuitBreaker = new CircuitBreaker(3, 10);
$url = "http://example.com/api";
try {
$result = $circuitBreaker->call($url);
echo $result;
} catch (Exception $e) {
echo $e->getMessage();
}
?>

This circuit breaker opens after a certain number of consecutive failures and prevents further attempts until a timeout period has passed, giving the failing service time to recover.

8. Caching Pattern

Use Case: Temporarily stores copies of data in accessible storage locations to reduce access time.

Details: Helps in reducing database load and improving application performance. Implementation can be as simple as key-value pairs or as complex as incorporating distributed caching systems like Redis.

Type: Structural

Commonly used for: Storing frequently accessed data in a temporary storage area to improve performance and reduce load on the primary data source. Often used in web applications to cache frequently accessed resources.

Example: Implementing caching using Python’s functools.lru_cache for a function that computes Fibonacci numbers, which can be expensive.

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)

# Example Usage
print(fibonacci(10)) # Outputs 55

This example uses an LRU (Least Recently Used) cache to store the results of expensive function calls, reducing the computation time for repeated requests.

9. Repository Pattern

Use Case: Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.

Details: This pattern is commonly used in applications that require access to complex data or require an abstraction layer that decouples the service layer from the data layer, enhancing modularity and scalability.

Type: Structural

Commonly used for: Encapsulating the logic needed to access data sources. It mediates between the domain and data mapping layers, providing a collection-like interface for accessing domain objects. Used to centralize data logic or business logic.

Example: A simple repository class in Python that abstracts the access to a list of users.

class UserRepository:
def __init__(self):
self.users = {} # This would be a database in a real application

def add_user(self, user_id, user_info):
self.users[user_id] = user_info

def get_user(self, user_id):
return self.users.get(user_id)

def update_user(self, user_id, user_info):
self.users[user_id] = user_info

def delete_user(self, user_id):
if user_id in self.users:
del self.users[user_id]

# Example Usage
repo = UserRepository()
repo.add_user(1, {"name": "John", "age": 30})
print(repo.get_user(1))

This repository pattern example provides a simplified interface to the data access layer, allowing easy modifications and isolating the business logic from data access concerns.

10. API Gateway Pattern

Use Case: Provides a single entry point for all the microservices running within the system.

Details: It handles requests by routing them to the appropriate microservice, and by providing other cross-cutting features such as authentication, SSL termination, and cache management.

Type: Structural

Commonly used for: Providing a single entry point for a set of microservices. It handles requests by routing them to the appropriate microservice, and can also provide functionalities such as request aggregation, security, and load balancing.

Example: A basic implementation of an API Gateway using Python’s Flask framework, which routes requests to different microservices.

from flask import Flask, jsonify, request

app = Flask(__name__)

@app.route('/api/users/<user_id>', methods=['GET'])
def get_user(user_id):
# Route to user service
response = requests.get(f"http://user-service/{user_id}")
return jsonify(response.json())

@app.route('/api/orders/<order_id>', methods=['GET'])
def get_order(order_id):
# Route to order service
response = requests.get(f"http://order-service/{order_id}")
return jsonify(response.json())

if __name__ == '__main__':
app.run(port=5000)

This example of an API gateway routes requests for user and order data to different services, handling the complexity of interactions between the client and the microservices.

If you’re a developer eager to dive deeper into the code, this GitHub repository is perfect for you.

Understanding these patterns and their real-world applications can significantly enhance your ability to design robust systems. Additionally, practicing these patterns with real or hypothetical projects can provide a deeper insight and better preparation for your interviews.

For future article, You can subscribe.

👏👏👏 If you enjoyed reading this post, please give it a clap 👏 and follow me on Medium! 👏👏👏

Cheers!

--

--