Design patterns are proven solutions to common software design problems. They help developers create scalable, maintainable, and efficient software systems. Among the many design patterns, five stand out as essential for any software engineer. This article explores these critical design patterns with real-world examples and best practices.
1. Singleton Pattern
Purpose: Ensures that a class has only one instance and provides a global point of access to it.
Example using Enum (Java):
public enum SingletonEnum {
INSTANCE;
public void someMethod() {
System.out.println("Singleton using Enum");
}
}
Code language: PHP (php)
Example using Enum (C#):
public sealed class SingletonEnum {
private static readonly SingletonEnum instance = new SingletonEnum();
private SingletonEnum() {}
public static SingletonEnum Instance {
get { return instance; }
}
public void SomeMethod() {
Console.WriteLine("Singleton using Enum");
}
}
Code language: PHP (php)
Use Cases:
- Managing shared resources (e.g., database connections, logging mechanisms, configuration settings)
- Implementing caches or thread pools
Example in Java ( using null check and synchronized block):
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Code language: PHP (php)
Example in C#:
public sealed class Singleton {
private static readonly object lockObj = new object();
private static Singleton instance = null;
private Singleton() {}
public static Singleton Instance {
get {
lock (lockObj) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
}
Code language: PHP (php)
Best Practices:
- Use double-checked locking for thread safety.
- Consider using an enum-based singleton for a simpler implementation.
2. Factory Method Pattern
Purpose: Defines an interface for creating objects but allows subclasses to alter the type of objects that will be created.
Example in Java:
abstract class Product {
abstract void printPrice();
}
class ConcreteProductA extends Product {
@Override
void printPrice() {
System.out.println("Price of Product A");
}
}
class ConcreteProductB extends Product {
@Override
void printPrice() {
System.out.println("Price of Product B");
}
}
class Factory {
static Product createProduct(String type) {
if (type.equals("A")) {
return new ConcreteProductA();
} else if (type.equals("B")) {
return new ConcreteProductB();
}
throw new IllegalArgumentException("Unknown product type");
}
}
Code language: JavaScript (javascript)
Example in C#:
abstract class Product {
public abstract void Operation();
}
class ConcreteProductA : Product {
public override void Operation() {
Console.WriteLine("Product A");
}
}
class ConcreteProductB : Product {
public override void Operation() {
Console.WriteLine("Product B");
}
}
class Factory {
public static Product CreateProduct(string type) {
return type switch {
"A" => new ConcreteProductA(),
"B" => new ConcreteProductB(),
_ => throw new ArgumentException("Unknown product type")
};
}
}
3. Observer Pattern
Purpose: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified automatically.
Example in Java:
import java.util.ArrayList;
import java.util.List;
interface Observer {
void update(String message);
}
class ConcreteObserver implements Observer {
private String name;
public ConcreteObserver(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " received update: " + message);
}
}
class Subject {
private List<Observer> observers = new ArrayList<>();
void subscribe(Observer observer) {
observers.add(observer);
}
void unsubscribe(Observer observer) {
observers.remove(observer);
}
void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
Code language: JavaScript (javascript)
Example in C#:
using System;
using System.Collections.Generic;
interface IObserver {
void Update(string message);
}
class ConcreteObserver : IObserver {
private string name;
public ConcreteObserver(string name) {
this.name = name;
}
public void Update(string message) {
Console.WriteLine($"{name} received update: {message}");
}
}
class Subject {
private List<IObserver> observers = new List<IObserver>();
public void Subscribe(IObserver observer) {
observers.Add(observer);
}
public void Unsubscribe(IObserver observer) {
observers.Remove(observer);
}
public void NotifyObservers(string message) {
foreach (var observer in observers) {
observer.Update(message);
}
}
}
Code language: HTML, XML (xml)
4. Strategy Pattern
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows a class’s behavior to be selected at runtime.
Instead of implementing multiple algorithms within a class, the Strategy Pattern delegates responsibility to separate classes, each representing a different strategy.
Key Components of Strategy Pattern
1. Strategy Interface – Defines a common interface for all supported strategies.
2. Concrete Strategies – Implements different variations of the algorithm.
3. Context Class – Uses a Strategy object to invoke a specific behavior.
When to Use the Strategy Pattern?
Use the Strategy Pattern when:
• You have multiple algorithms that can be used interchangeably.
• You want to avoid if-else or switch-case statements to determine which algorithm to use.
• You need to follow the Open-Closed Principle (allow extensions without modifying existing code).
• Behavior needs to be dynamically chosen at runtime.
Example in Java:
interface PaymentStrategy {
void pay(int amount);
}
// Concrete Strategy 1: CreditCard Payment
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card.");
}
}
// Concrete Strategy 2: PayPal Payment
class PayPalPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal.");
}
}
// Concrete Strategy 3: Bitcoin Payment
class BitcoinPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using Bitcoin.");
}
}
class PaymentContext {
private PaymentStrategy strategy;
// Method to set the strategy at runtime
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
// Executes the payment using the selected strategy
public void executePayment(int amount) {
if (strategy == null) {
System.out.println("No payment strategy selected!");
return;
}
strategy.pay(amount);
}
}
Code language: PHP (php)
Now, let’s create a test class to demonstrate how we can dynamically switch between payment strategies.
public class StrategyPatternExample {
public static void main(String[] args) {
PaymentContext context = new PaymentContext();
// Using Credit Card Payment Strategy
context.setStrategy(new CreditCardPayment());
context.executePayment(100);
// Switching to PayPal Payment Strategy
context.setStrategy(new PayPalPayment());
context.executePayment(200);
// Switching to Bitcoin Payment Strategy
context.setStrategy(new BitcoinPayment());
context.executePayment(300);
}
}
Code language: JavaScript (javascript)
Example in C#:
interface PaymentStrategy {
void Pay(int amount);
}
class CreditCardPayment : PaymentStrategy {
public void Pay(int amount) {
Console.WriteLine($"Paid {amount} using Credit Card.");
}
}
//Add rest of the code based on java example
5. Decorator Pattern
Allows behavior to be added to an individual object dynamically without modifying its structure.
It is an alternative to subclassing, providing a flexible way to extend functionality by wrapping objects inside other objects.
Key Characteristics of the Decorator Pattern:
• Used to dynamically add new behavior to objects.
• Avoids creating a large number of subclasses.
• Uses composition over inheritance by wrapping objects.
• Adheres to the Open-Closed Principle (open for extension, closed for modification).
Key Components of the Decorator Pattern
1. Component (Interface or Abstract Class) – The base interface for objects that can have responsibilities added to them dynamically.
2. Concrete Component – A class that implements the Component interface (base functionality).
3. Decorator (Abstract Class) – Wraps the component and implements the same interface.
4. Concrete Decorators – Extends the Decorator class and adds extra functionality.
Real-World Example: Coffee Shop
Imagine a coffee shop where customers can order a basic coffee and customize it with additional features like milk, sugar, or caramel. Instead of creating separate classes for every coffee combination, we use the Decorator Pattern to add ingredients dynamically.
Example in Java:
// Component Interface
interface Coffee {
String getDescription();
double getCost();
}
class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple Coffee";
}
@Override
public double getCost() {
return 5.00;
}
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public String getDescription() {
return decoratedCoffee.getDescription();
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
}
// Concrete Decorator 1: Adds Milk
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + ", Milk";
}
@Override
public double getCost() {
return super.getCost() + 1.50;
}
}
// Concrete Decorator 2: Adds Sugar
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + ", Sugar";
}
@Override
public double getCost() {
return super.getCost() + 0.50;
}
}
//Using the pattern
public class DecoratorPatternExample {
public static void main(String[] args) {
Coffee simpleCoffee = new SimpleCoffee();
System.out.println(simpleCoffee.getDescription() + " -> Cost: $" + simpleCoffee.getCost());
// Add Milk
Coffee milkCoffee = new MilkDecorator(simpleCoffee);
System.out.println(milkCoffee.getDescription() + " -> Cost: $" + milkCoffee.getCost());
// Add Sugar
Coffee sugarMilkCoffee = new SugarDecorator(milkCoffee);
System.out.println(sugarMilkCoffee.getDescription() + " -> Cost: $" + sugarMilkCoffee.getCost());
}
}
Code language: PHP (php)
Example in C#:
interface Component {
void Execute();
}
class ConcreteComponent : Component {
public void Execute() {
Console.WriteLine("Executing base component");
}
}
//Add rest of the code based on java example
Conclusion
Understanding and applying design patterns effectively can significantly improve software architecture. The Singleton, Factory Method, Observer, Strategy, and Decorator patterns are among the most valuable for building maintainable and scalable applications. By mastering these patterns, developers can design more robust and flexible systems that are easier to extend and maintain.
Comments
One response to “Top 5 Essential Design Patterns in Software Development with Java & C# Examples”
Nice one.