Design Patterns in Software Architecture
Design Patterns are general, reusable solutions to commonly occurring problems within a given context in software design. They are not finished designs that can be directly translated into code, but rather descriptions or templates for how to solve a problem that can be used in many different situations.
This section explores fundamental design patterns, categorized by their purpose: Creational, Structural, and Behavioral. Understanding these patterns can lead to more maintainable, flexible, and robust software systems.
Creational Patterns
Creational patterns deal with object-creation mechanisms, trying to create objects in a manner suitable to the situation. They are concerned with how classes are instantiated and objects are created.
Factory Method
Defines an interface for creating an object, but lets subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
Example (Conceptual C#)
Imagine creating different types of documents:
public abstract class Creator {
public abstract IProduct FactoryMethod();
public void SomeOperation() {
var product = FactoryMethod();
// Use the product
Console.WriteLine($"Created: {product.Operation()}");
}
}
public class ConcreteCreatorA : Creator {
public override IProduct FactoryMethod() {
return new ConcreteProductA();
}
}
public interface IProduct {
string Operation();
}
public class ConcreteProductA : IProduct {
public string Operation() {
return "Result of ConcreteProductA";
}
}
Singleton Pattern
Ensures a class only has one instance and provides a global point of access to it.
Example (Conceptual Java)
public class Singleton {
private static Singleton instance;
private Singleton() {
// Private constructor to prevent instantiation
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void showMessage() {
System.out.println("Hello from Singleton!");
}
}
Structural Patterns
Structural patterns are concerned with class and object composition. They use inheritance and composition to find simple ways to realize relationships among entities.
Adapter Pattern
Converts the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
Example (Conceptual Python)
class Target:
def request(self):
return "Target: The default target's behavior."
class Adaptee:
def specific_request(self):
return "Adaptee: The adapted request's behavior."
class Adapter(Target):
def __init__(self, adaptee):
self._adaptee = adaptee
def request(self):
return f"Adapter: (TRANSLATING) {self._adaptee.specific_request()}"
# Client code
client_code = Adapter(Adaptee())
print(client_code.request())
Decorator Pattern
Attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Behavioral Patterns
Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. They describe how objects interact with each other and how they are organized.
Observer Pattern
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Example (Conceptual JavaScript)
class Subject {
constructor() {
this.observers = [];
}
attach(observer) {
this.observers.push(observer);
}
detach(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify() {
this.observers.forEach(observer => observer.update(this));
}
}
class ConcreteSubject extends Subject {
constructor() {
super();
this.state = 0;
}
get getState() {
return this.state;
}
set setState(value) {
this.state = value;
this.notify();
}
}
class Observer {
update(subject) {
throw new Error("Observer.update() must be implemented.");
}
}
class ConcreteObserverA extends Observer {
update(subject) {
if (subject.getState % 2 === 0) {
console.log(`ConcreteObserverA: Reacted to the event. State is ${subject.getState}`);
}
}
}
const subject = new ConcreteSubject();
const observerA = new ConcreteObserverA();
subject.attach(observerA);
subject.setState = 10;
subject.setState = 3;
subject.detach(observerA);
subject.setState = 20;
Strategy Pattern
Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.