Understanding Common Design Patterns in Software Development
πŸ“’

Understanding Common Design Patterns in Software Development

Tags
Published
September 15, 2024
Design patterns are proven solutions to common problems in software design. They provide a template for writing code that is efficient, reusable, and maintainable. This blog post explores some of the most widely used design patterns, complete with code examples to illustrate their practical applications.

Table of Contents

  1. Creational Patterns
  1. Structural Patterns
  1. Behavioral Patterns
  1. Conclusion

Creational Patterns

Creational patterns focus on object creation mechanisms, optimizing code for flexibility and reuse.

Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.

When to Use

  • When exactly one object is needed to coordinate actions across the system.
  • Managing shared resources like configuration settings or connection pools.

Code Example (Python)

class SingletonMeta(type): _instance = None def __call__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__call__(*args, **kwargs) return cls._instance class ConfigurationManager(metaclass=SingletonMeta): def __init__(self): self.settings = {} # Usage config1 = ConfigurationManager() config2 = ConfigurationManager() print(config1 is config2) # Output: True

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.

When to Use

  • When a class cannot anticipate the type of objects it needs to create beforehand.
  • To delegate the responsibility of object instantiation to subclasses.

Code Example (Java)

abstract class Transport { public abstract void deliver(); } class Truck extends Transport { public void deliver() { System.out.println("Deliver by land in a box."); } } class Ship extends Transport { public void deliver() { System.out.println("Deliver by sea in a container."); } } abstract class Logistics { // factory public void planDelivery() { Transport transport = createTransport(); transport.deliver(); } public abstract Transport createTransport(); // factory method } class RoadLogistics extends Logistics { public Transport createTransport() { return new Truck(); } } class SeaLogistics extends Logistics { public Transport createTransport() { return new Ship(); } } // Usage public class Main { public static void main(String[] args) { Logistics logistics = new RoadLogistics(); logistics.planDelivery(); // Output: Deliver by land in a box. } }
Β 

Structural Patterns

Structural patterns deal with object composition, ensuring that if one part changes, the entire structure doesn't need to.

Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together by converting the interface of one class into an interface expected by the clients.

When to Use

  • To integrate classes that couldn't otherwise work together due to incompatible interfaces.
  • To reuse existing classes without modifying their source code.

Code Example (C#)

// Existing interface public interface ITarget { void Request(); } // Adaptee with an incompatible interface public class Adaptee { public void SpecificRequest() { Console.WriteLine("Called SpecificRequest()"); } } // Adapter making Adaptee compatible with ITarget public class Adapter : ITarget { private readonly Adaptee _adaptee; public Adapter(Adaptee adaptee) { _adaptee = adaptee; } public void Request() { _adaptee.SpecificRequest(); } } // Usage class Program { static void Main() { Adaptee adaptee = new Adaptee(); ITarget target = new Adapter(adaptee); target.Request(); // Output: Called SpecificRequest() } }

Decorator Pattern

The Decorator pattern adds new behaviors to objects dynamically by placing them inside wrapper objects.

When to Use

  • To add responsibilities to individual objects dynamically without affecting other objects.
  • When subclassing would result in an explosion of subclasses to cover every combination.

Code Example (JavaScript)

// Component class Coffee { getCost() { return 5; } getDescription() { return "Coffee"; } } // Decorator class MilkDecorator { constructor(coffee) { this.coffee = coffee; } getCost() { return this.coffee.getCost() + 1; } getDescription() { return this.coffee.getDescription() + ", Milk"; } } // Usage let myCoffee = new Coffee(); myCoffee = new MilkDecorator(myCoffee); console.log(myCoffee.getCost()); // Output: 6 console.log(myCoffee.getDescription()); // Output: Coffee, Milk

Behavioral Patterns

Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects.

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.

When to Use

  • When changes to one object require changing others, and the number of objects is unknown.
  • To decouple the subject and observers, allowing them to vary independently.

Code Example (Python)

class Subject: def __init__(self): self._observers = [] self._state = None def attach(self, observer): self._observers.append(observer) @property def state(self): return self._state @state.setter def state(self, value): self._state = value self.notify() def notify(self): for observer in self._observers: observer.update(self) class ConcreteObserver: def update(self, subject): print(f"Observer: Reacted to state change to {subject.state}") # Usage subject = Subject() observer_a = ConcreteObserver() observer_b = ConcreteObserver() subject.attach(observer_a) subject.attach(observer_b) subject.state = "New State" # Output: # Observer: Reacted to state change to New State # Observer: Reacted to state change to New State

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

When to Use

  • When multiple algorithms are needed, and you want to switch between them at runtime.
  • To avoid conditional statements for selecting desired behavior.

Code Example (Java)

interface PaymentStrategy { void pay(int amount); } class CreditCardStrategy implements PaymentStrategy { public void pay(int amount) { System.out.println("Paid $" + amount + " using Credit Card."); } } class PayPalStrategy implements PaymentStrategy { public void pay(int amount) { System.out.println("Paid $" + amount + " using PayPal."); } } class ShoppingCart { private PaymentStrategy paymentStrategy; public void setPaymentStrategy(PaymentStrategy paymentStrategy) { this.paymentStrategy = paymentStrategy; } public void checkout(int amount) { paymentStrategy.pay(amount); } } // Usage public class Main { public static void main(String[] args) { ShoppingCart cart = new ShoppingCart(); cart.setPaymentStrategy(new CreditCardStrategy()); cart.checkout(100); // Output: Paid $100 using Credit Card. cart.setPaymentStrategy(new PayPalStrategy()); cart.checkout(200); // Output: Paid $200 using PayPal. } }

Conclusion

Design patterns are essential tools in a developer's toolkit, providing solutions to common design issues. By understanding and applying these patterns, you can create flexible, reusable, and maintainable code. The patterns discussed here are just the tip of the iceberg; exploring them further will undoubtedly enhance your software development skills.

References
  • Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
  • Official documentation and tutorials on design patterns in various programming languages.