Design Patterns in Software Development — all you need to know

LordNeic
21 min readMay 21, 2024

Design patterns are fundamental to creating robust, scalable, and maintainable software. As developers gain experience, they often encounter more complex scenarios that require advanced design patterns. This blog delves deeper into sophisticated design patterns, exploring their applications, benefits, and nuances, which can significantly enhance your software architecture.

Introduction

  1. The Essence of Advanced Design Patterns

Categories of Advanced Design Patterns

  1. Creational Patterns
  2. Structural Patterns
  3. Behavioral Patterns
  4. Concurrency Patterns

Creational Patterns

  1. Singleton Pattern
  2. Prototype Pattern
  3. Factory Method Pattern
  4. Abstract Factory Pattern
  5. Builder Pattern

Structural Patterns

  1. Adapter Pattern
  2. Bridge Pattern
  3. Composite Pattern
  4. Decorator Pattern
  5. Facade Pattern
  6. Flyweight Pattern
  7. Proxy Pattern

Behavioral Patterns

  1. Chain of Responsibility Pattern
  2. Command Pattern
  3. Interpreter Pattern
  4. Iterator Pattern
  5. Mediator Pattern
  6. Memento Pattern
  7. Observer Pattern
  8. State Pattern
  9. Strategy Pattern
  10. Template Method Pattern
  11. Visitor Pattern

Concurrency Patterns

  1. Monitor Object Pattern
  2. Thread-Specific Storage Pattern
  3. Active Object Pattern
  4. Half-Sync/Half-Async Pattern
  5. Scheduler Pattern

Creational Patterns

1. Singleton Pattern

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

class Singleton {
private static $instance = null;

private function __construct() {}

public static function getInstance() {
if (self::$instance == null) {
self::$instance = new Singleton();
}
return self::$instance;
}
}

Use Case: When exactly one object is needed to coordinate actions across the system, such as a configuration manager or a connection pool.

Benefits:

  • Controlled Access: Guarantees controlled access to the sole instance.
  • Reduced Namespace Pollution: Prevents unnecessary global variables.
  • Flexible Scalability: Can be extended to manage a pool of instances.

2. Prototype Pattern

Purpose: Creates new objects by copying an existing object, known as a prototype, rather than creating objects from scratch.

class Prototype {
constructor(proto) {
this.proto = proto;
}

clone() {
let clone = Object.create(this.proto);
clone.constructor = this.constructor;
return clone;
}
}

const proto = { field: "value" };
const prototype = new Prototype(proto);
const clone = prototype.clone();
console.log(clone.field); // "value"

Use Case: When object creation is costly or complex. Cloning a prototype can be more efficient than building a new instance.

Benefits:

  • Performance: Reduces the overhead of creating new objects from scratch, leading to performance improvements.
  • Flexibility: Simplifies the creation of new objects, especially when the object’s structure is complex or the initialization requires significant resources.
  • Customization: Allows for easy customization and creation of unique objects by modifying the prototype.

3. Factory Method Pattern

Purpose: Defines an interface for creating an object, but lets subclasses alter the type of objects that will be created.

abstract class Creator {
abstract public function factoryMethod();

public function anOperation() {
$product = $this->factoryMethod();
return "Creator: The same creator's code has just worked with " . $product->operation();
}
}

class ConcreteCreator1 extends Creator {
public function factoryMethod() {
return new ConcreteProduct1();
}
}

class ConcreteProduct1 {
public function operation() {
return "{Result of the ConcreteProduct1}";
}
}

$creator = new ConcreteCreator1();
echo $creator->anOperation();

Use Case: When a class cannot anticipate the type of objects it needs to create beforehand.

Benefits:

  • Decoupling: Reduces the dependency on specific classes.
  • Flexibility: Allows for more flexible and extensible code.
  • Object Management: Centralizes the creation logic.

4. Abstract Factory Pattern

Purpose: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

public interface IButton {
void Paint();
}

public class WinButton : IButton {
public void Paint() {
Console.WriteLine("Render a button in a Windows style.");
}
}

public class OSXButton : IButton {
public void Paint() {
Console.WriteLine("Render a button in a macOS style.");
}
}

public interface IGUIFactory {
IButton CreateButton();
}

public class WinFactory : IGUIFactory {
public IButton CreateButton() {
return new WinButton();
}
}

public class OSXFactory : IGUIFactory {
public IButton CreateButton() {
return new OSXButton();
}
}

public class Application {
private readonly IGUIFactory factory;

public Application(IGUIFactory factory) {
this.factory = factory;
}

public void CreateUI() {
var button = factory.CreateButton();
button.Paint();
}
}

class Program {
static void Main() {
var winFactory = new WinFactory();
var app1 = new Application(winFactory);
app1.CreateUI();

var osxFactory = new OSXFactory();
var app2 = new Application(osxFactory);
app2.CreateUI();
}
}

Use Case: When a system needs to be independent of how its objects are created, composed, and represented. This is often used in frameworks that need to support multiple themes or configurations.

Benefits:

  • Consistency: Ensures that a family of related objects is used together, promoting consistency across the application.
  • Encapsulation: Hides the implementation details of the product families, exposing only the interfaces.
  • Scalability: Makes it easy to introduce new product families without modifying existing code.

5. Builder Pattern

Purpose: Separates the construction of a complex object from its representation, allowing the same construction process to create various representations.

public class Product {
private List<string> parts = new List<string>();

public void Add(string part) {
parts.Add(part);
}

public void Show() {
Console.WriteLine("\nProduct Parts -------");
foreach (string part in parts)
Console.WriteLine(part);
}
}

public abstract class Builder {
public abstract void BuildPartA();
public abstract void BuildPartB();
public abstract Product GetResult();
}

public class ConcreteBuilder1 : Builder {
private Product product = new Product();

public override void BuildPartA() {
product.Add("PartA");
}

public override void BuildPartB() {
product.Add("PartB");
}

public override Product GetResult() {
return product;
}
}

public class Director {
public void Construct(Builder builder) {
builder.BuildPartA();
builder.BuildPartB();
}
}

class Program {
static void Main() {
var director = new Director();

var b1 = new ConcreteBuilder1();
director.Construct(b1);
var p1 = b1.GetResult();
p1.Show();
}
}

Use Case: When an object needs to be created in a step-by-step manner or when the creation process is complex.

Benefits:

  • Step-by-Step Construction: Simplifies the construction of complex objects.
  • Reusability: Promotes reuse of the construction process for different representations.
  • Isolation of Construction: Isolates the construction logic from the representation.

Structural Patterns

6. Adapter Pattern

Purpose: Allows objects with incompatible interfaces to collaborate.

public interface ITarget {
string GetRequest();
}

public class Adaptee {
public string GetSpecificRequest() {
return "Specific request.";
}
}

public class Adapter : ITarget {
private readonly Adaptee _adaptee;

public Adapter(Adaptee adaptee) {
this._adaptee = adaptee;
}

public string GetRequest() {
return $"This is '{this._adaptee.GetSpecificRequest()}'";
}
}

class Program {
static void Main(string[] args) {
Adaptee adaptee = new Adaptee();
ITarget target = new Adapter(adaptee);
Console.WriteLine(target.GetRequest());
}
}

Use Case: When an existing class is needed, but its interface does not match the one you need.

Benefits:

  • Compatibility: Enables interaction between incompatible interfaces.
  • Flexibility: Provides a flexible alternative to subclassing.
  • Reusability: Facilitates the reuse of existing classes.

7. Bridge Pattern

Purpose: Separates an object’s abstraction from its implementation so that the two can vary independently.

public interface IImplementor {
void OperationImp();
}

public class ConcreteImplementorA : IImplementor {
public void OperationImp() {
Console.WriteLine("ConcreteImplementorA Operation");
}
}

public class ConcreteImplementorB : IImplementor {
public void OperationImp() {
Console.WriteLine("ConcreteImplementorB Operation");
}
}

public abstract class Abstraction {
protected IImplementor implementor;

protected Abstraction(IImplementor implementor) {
this.implementor = implementor;
}

public abstract void Operation();
}

public class RefinedAbstraction : Abstraction {
public RefinedAbstraction(IImplementor implementor) : base(implementor) { }

public override void Operation() {
implementor.OperationImp();
}
}

class Program {
static void Main(string[] args) {
Abstraction ab = new RefinedAbstraction(new ConcreteImplementorA());
ab.Operation();

ab = new RefinedAbstraction(new ConcreteImplementorB());
ab.Operation();
}
}

Use Case: When both the abstractions and their implementations should be extensible by subclassing.

Benefits:

  • Independence: Allows for independent variation of abstraction and implementation.
  • Scalability: Enhances scalability by decoupling the implementation from the interface.
  • Simplified Code: Reduces the complexity of code by separating concerns.

8. Composite Pattern

Purpose: Composes objects into tree structures to represent part-whole hierarchies, allowing individual objects and compositions to be treated uniformly.

public abstract class Component {
protected string name;

public Component(string name) {
this.name = name;
}

public abstract void Add(Component c);
public abstract void Remove(Component c);
public abstract void Display(int depth);
}

public class Composite : Component {
private List<Component> children = new List<Component>();

public Composite(string name) : base(name) { }

public override void Add(Component component) {
children.Add(component);
}

public override void Remove(Component component) {
children.Remove(component);
}

public override void Display(int depth) {
Console.WriteLine(new String('-', depth) + name);

foreach (Component component in children) {
component.Display(depth + 2);
}
}
}

public class Leaf : Component {
public Leaf(string name) : base(name) { }

public override void Add(Component c) {
Console.WriteLine("Cannot add to a leaf");
}

public override void Remove(Component c) {
Console.WriteLine("Cannot remove from a leaf");
}

public override void Display(int depth) {
Console.WriteLine(new String('-', depth) + name);
}
}

class Program {
static void Main(string[] args) {
Composite root = new Composite("root");
root.Add(new Leaf("Leaf A"));
root.Add(new Leaf("Leaf B"));

Composite comp = new Composite("Composite X");
comp.Add(new Leaf("Leaf XA"));
comp.Add(new Leaf("Leaf XB"));

root.Add(comp);
root.Add(new Leaf("Leaf C"));

Leaf leaf = new Leaf("Leaf D");
root.Add(leaf);
root.Remove(leaf);

root.Display(1);
}
}

Use Case: When dealing with tree structures, like file systems or graphical user interfaces.

Benefits:

  • Uniformity: Treats individual objects and compositions uniformly.
  • Simplification: Simplifies the client code by allowing it to interact with tree structures transparently.
  • Extensibility: Makes it easy to add new kinds of components.

9. Decorator Pattern

Purpose: Adds additional responsibilities to an object dynamically.

public abstract class Component {
public abstract void Operation();
}

public class ConcreteComponent : Component {
public override void Operation() {
Console.WriteLine("ConcreteComponent.Operation()");
}
}

public abstract class Decorator : Component {
protected Component component;

public void SetComponent(Component component) {
this.component = component;
}

public override void Operation() {
if (component != null) {
component.Operation();
}
}
}

public class ConcreteDecoratorA : Decorator {
public override void Operation() {
base.Operation();
Console.WriteLine("ConcreteDecoratorA.Operation()");
}
}

public class ConcreteDecoratorB : Decorator {
public override void Operation() {
base.Operation();
AddedBehavior();
Console.WriteLine("ConcreteDecoratorB.Operation()");
}

void AddedBehavior() { }
}

class Program {
static void Main(string[] args) {
ConcreteComponent c = new ConcreteComponent();
ConcreteDecoratorA d1 = new ConcreteDecoratorA();
ConcreteDecoratorB d2 = new ConcreteDecoratorB();

d1.SetComponent(c);
d2.SetComponent(d1);

d2.Operation();
}
}

Use Case: When you need to add functionalities to objects at runtime without altering their structure.

Benefits:

  • Flexibility: Adds functionality without altering the object’s structure.
  • Reusability: Promotes the reuse of core functionality with different enhancements.
  • Extensibility: Facilitates the extension of object behavior.

10. Facade Pattern

Purpose: Provides a simplified interface to a complex subsystem.

public class Subsystem1 {
public void Operation1() {
Console.WriteLine("Subsystem1: Ready!");
}

public void OperationN() {
Console.WriteLine("Subsystem1: Go!");
}
}

public class Subsystem2 {
public void Operation1() {
Console.WriteLine("Subsystem2: Get ready!");
}

public void OperationZ() {
Console.WriteLine("Subsystem2: Fire!");
}
}

public class Facade {
protected Subsystem1 _subsystem1;
protected Subsystem2 _subsystem2;

public Facade(Subsystem1 subsystem1, Subsystem2 subsystem2) {
this._subsystem1 = subsystem1;
this._subsystem2 = subsystem2;
}

public void Operation() {
Console.WriteLine("Facade initializes subsystems:");
this._subsystem1.Operation1();
this._subsystem2.Operation1();
Console.WriteLine("Facade orders subsystems to perform the action:");
this._subsystem1.OperationN();
this._subsystem2.OperationZ();
}
}

class Program {
static void Main(string[] args) {
Subsystem1 subsystem1 = new Subsystem1();
Subsystem2 subsystem2 = new Subsystem2();
Facade facade = new Facade(subsystem1, subsystem2);
facade.Operation();
}
}

Use Case: When you need to simplify interaction with a complex system, making it easier to use.

Benefits:

  • Simplicity: Simplifies the interaction with complex subsystems.
  • Decoupling: Decouples the client code from the subsystem.
  • Maintainability: Improves maintainability by providing a clear entry point.

11. Flyweight Pattern

Purpose: Reduces the cost of creating and managing a large number of similar objects by sharing common state among them.

public class Flyweight {
private string _intrinsicState;

public Flyweight(string intrinsicState) {
this._intrinsicState = intrinsicState;
}

public void Operation(string extrinsicState) {
Console.WriteLine($"Intrinsic State: {_intrinsicState}, Extrinsic State: {extrinsicState}");
}
}

public class FlyweightFactory {
private Dictionary<string, Flyweight> _flyweights = new Dictionary<string, Flyweight>();

public Flyweight GetFlyweight(string key) {
if (!_flyweights.ContainsKey(key)) {
_flyweights[key] = new Flyweight(key);
}
return _flyweights[key];
}
}

class Program {
static void Main(string[] args) {
FlyweightFactory factory = new FlyweightFactory();

Flyweight flyweight1 = factory.GetFlyweight("A");
flyweight1.Operation("First Call");

Flyweight flyweight2 = factory.GetFlyweight("B");
flyweight2.Operation("Second Call");

Flyweight flyweight3 = factory.GetFlyweight("A");
flyweight3.Operation("Third Call");
}
}

Use Case: When an application uses a large number of objects that can be simplified by sharing common data, reducing memory usage.

Benefits:

  • Memory Efficiency: Significantly reduces memory usage by sharing as much data as possible.
  • Performance: Improves performance by reducing the number of objects created and managed.
  • Complexity Reduction: Simplifies the management of many similar objects.

12. Proxy Pattern

Purpose: Provides a surrogate or placeholder for another object to control access to it.

public interface ISubject {
void Request();
}

public class RealSubject : ISubject {
public void Request() {
Console.WriteLine("RealSubject: Handling Request.");
}
}

public class Proxy : ISubject {
private RealSubject _realSubject;

public void Request() {
if (_realSubject == null) {
_realSubject = new RealSubject();
}
_realSubject.Request();
}
}

class Program {
static void Main(string[] args) {
Proxy proxy = new Proxy();
proxy.Request();
}
}

Use Case: When you need to control access to an object, add additional functionality, or delay the creation and initialization of expensive objects.

Benefits:

  • Control Access: Regulates access to the real object, adding a layer of security or functionality.
  • Lazy Initialization: Defers the creation and initialization of resource-intensive objects until necessary.
  • Logging and Monitoring: Provides a centralized point to add logging, monitoring, or other cross-cutting concerns.

Behavioral Patterns

13. Chain of Responsibility Pattern

Purpose: Passes a request along a chain of handlers, allowing each handler to process the request or pass it along to the next handler in the chain.

abstract class Handler {
protected Handler successor;

public void SetSuccessor(Handler successor) {
this.successor = successor;
}

public abstract void HandleRequest(int request);
}

class ConcreteHandler1 : Handler {
public override void HandleRequest(int request) {
if (request >= 0 && request < 10) {
Console.WriteLine($"{this.GetType().Name} handled request {request}");
} else if (successor != null) {
successor.HandleRequest(request);
}
}
}

class ConcreteHandler2 : Handler {
public override void HandleRequest(int request) {
if (request >= 10 && request < 20) {
Console.WriteLine($"{this.GetType().Name} handled request {request}");
} else if (successor != null) {
successor.HandleRequest(request);
}
}
}

class Program {
static void Main(string[] args) {
Handler h1 = new ConcreteHandler1();
Handler h2 = new ConcreteHandler2();
h1.SetSuccessor(h2);

int[] requests = { 5, 14, 22, 18, 3, 27, 20 };

foreach (int request in requests) {
h1.HandleRequest(request);
}
}
}

Use Case: When multiple objects can handle a request, and the handler is determined at runtime.

Benefits:

  • Decoupling: Decouples the sender and receiver of the request.
  • Flexibility: Adds or changes the handlers dynamically.
  • Responsibility Sharing: Distributes the handling of a request among multiple objects.

14. Command Pattern

Purpose: Encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of the requests.

public interface ICommand {
void Execute();
}

public class ConcreteCommand : ICommand {
private Receiver _receiver;

public ConcreteCommand(Receiver receiver) {
this._receiver = receiver;
}

public void Execute() {
_receiver.Action();
}
}

public class Receiver {
public void Action() {
Console.WriteLine("Receiver: Executing Action");
}
}

public class Invoker {
private ICommand _command;

public void SetCommand(ICommand command) {
this._command = command;
}

public void ExecuteCommand() {
_command.Execute();
}
}

class Program {
static void Main(string[] args) {
Receiver receiver = new Receiver();
ICommand command = new ConcreteCommand(receiver);

Invoker invoker = new Invoker();
invoker.SetCommand(command);
invoker.ExecuteCommand();
}
}

Use Case: When you need to issue requests to objects without knowing the operations being requested or the receiver of the request.

Benefits:

  • Decoupling: Decouples the sender and receiver of the request.
  • Flexibility: Supports undoable operations.
  • Extensibility: Facilitates the addition of new commands without modifying existing code.

15. Interpreter Pattern

Purpose: Defines a representation for a grammar and an interpreter that uses the representation to interpret sentences in the language.

public abstract class AbstractExpression {
public abstract void Interpret(Context context);
}

public class TerminalExpression : AbstractExpression {
public override void Interpret(Context context) {
Console.WriteLine("Terminal expression");
}
}

public class NonTerminalExpression : AbstractExpression {
private AbstractExpression _expression1;
private AbstractExpression _expression2;

public NonTerminalExpression(AbstractExpression expression1, AbstractExpression expression2) {
this._expression1 = expression1;
this._expression2 = expression2;
}

public override void Interpret(Context context) {
Console.WriteLine("Non-terminal expression");
_expression1.Interpret(context);
_expression2.Interpret(context);
}
}

public class Context {
private string _input;
private string _output;

public Context(string input) {
this._input = input;
}

public string Input { get => _input; set => _input = value; }
public string Output { get => _output; set => _output = value; }
}

class Program {
static void Main(string[] args) {
Context context = new Context("input");

List<AbstractExpression> list = new List<AbstractExpression>();
list.Add(new TerminalExpression());
list.Add(new NonTerminalExpression(new TerminalExpression(), new TerminalExpression()));

foreach (AbstractExpression exp in list) {
exp.Interpret(context);
}
}
}

Use Case: When there is a need to interpret languages or expressions, such as in developing domain-specific languages, scripting languages, or rules engines.

Benefits:

  • Flexibility: Allows for easy extension of the language or expressions being interpreted.
  • Modularity: Separates the grammar definition from the interpretation logic, promoting modularity.
  • Reusability: Enables reuse of the interpreter for different expressions or languages.

16. Iterator Pattern

Purpose: Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

public interface IIterator {
bool HasNext();
object Next();
}

public interface IAggregate {
IIterator CreateIterator();
}

public class ConcreteAggregate : IAggregate {
private List<object> _items = new List<object>();

public IIterator CreateIterator() {
return new ConcreteIterator(this);
}

public int Count { get => _items.Count; }

public object this[int index] {
get { return _items[index]; }
set { _items.Insert(index, value); }
}
}

public class ConcreteIterator : IIterator {
private ConcreteAggregate _aggregate;
private int _current = 0;

public ConcreteIterator(ConcreteAggregate aggregate) {
this._aggregate = aggregate;
}

public bool HasNext() {
return _current < _aggregate.Count;
}

public object Next() {
return _aggregate[_current++];
}
}

class Program {
static void Main(string[] args) {
ConcreteAggregate aggregate = new ConcreteAggregate();
aggregate[0] = "Item A";
aggregate[1] = "Item B";
aggregate[2] = "Item C";

IIterator iterator = aggregate.CreateIterator();

while (iterator.HasNext()) {
object item = iterator.Next();
Console.WriteLine(item);
}
}
}

Use Case: When you need to traverse a collection of objects without exposing the collection’s internal structure.

Benefits:

  • Encapsulation: Preserves the encapsulation of the collection’s implementation.
  • Uniformity: Provides a standard way to traverse different collections.
  • Simplification: Simplifies the collection traversal logic.

17. Mediator Pattern

Purpose: Defines an object that encapsulates how a set of objects interact, promoting loose coupling by keeping objects from referring to each other explicitly.

public class Mediator {
private Colleague1 _colleague1;
private Colleague2 _colleague2;

public Colleague1 Colleague1 {
set { _colleague1 = value; }
}

public Colleague2 Colleague2 {
set { _colleague2 = value; }
}

public void Send(string message, Colleague colleague) {
if (colleague == _colleague1) {
_colleague2.Notify(message);
} else {
_colleague1.Notify(message);
}
}
}

public abstract class Colleague {
protected Mediator mediator;

public Colleague(Mediator mediator) {
this.mediator = mediator;
}
}

public class Colleague1 : Colleague {
public Colleague1(Mediator mediator) : base(mediator) { }

public void Send(string message) {
mediator.Send(message, this);
}

public void Notify(string message) {
Console.WriteLine("Colleague1 gets message: " + message);
}
}

public class Colleague2 : Colleague {
public Colleague2(Mediator mediator) : base(mediator) { }

public void Send(string message) {
mediator.Send(message, this);
}

public void Notify(string message) {
Console.WriteLine("Colleague2 gets message: " + message);
}
}

class Program {
static void Main(string[] args) {
Mediator mediator = new Mediator();

Colleague1 c1 = new Colleague1(mediator);
Colleague2 c2 = new Colleague2(mediator);

mediator.Colleague1 = c1;
mediator.Colleague2 = c2;

c1.Send("How are you?");
c2.Send("Fine, thanks!");
}
}

Use Case: When a system has complex interactions between multiple objects, the mediator pattern simplifies these interactions by centralizing the communication logic.

Benefits:

  • Decoupling: Reduces dependencies between communicating objects, promoting loose coupling.
  • Centralized Control: Simplifies the maintenance and modification of communication logic by centralizing it.
  • Scalability: Makes it easier to add new communication paths without modifying existing objects.

18. Memento Pattern

Purpose: Captures and externalizes an object’s internal state so that the object can be restored to this state later without violating encapsulation.

public class Memento {
private string _state;

public Memento(string state) {
this._state = state;
}

public string State {
get { return _state; }
}
}

public class Originator {
private string _state;

public string State {
get { return _state; }
set {
_state = value;
Console.WriteLine("State = " + _state);
}
}

public Memento CreateMemento() {
return new Memento(_state);
}

public void SetMemento(Memento memento) {
Console.WriteLine("Restoring state...");
State = memento.State;
}
}

public class Caretaker {
private Memento _memento;

public Memento Memento {
set { _memento = value; }
get { return _memento; }
}
}

class Program {
static void Main(string[] args) {
Originator o = new Originator();
o.State = "On";

Caretaker c = new Caretaker();
c.Memento = o.CreateMemento();

o.State = "Off";

o.SetMemento(c.Memento);
}
}

Use Case: When you need to implement undo and redo functionality in an application.

Benefits:

  • Encapsulation: Preserves the encapsulation of an object’s state while allowing it to be restored.
  • History Management: Facilitates the management of historical states, enabling undo/redo operations.
  • State Isolation: Keeps the state restoration logic separate from the object’s primary functionality.

19. Observer Pattern

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

public interface IObserver {
void Update(ISubject subject);
}

public interface ISubject {
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}

public class ConcreteSubject : ISubject {
public int State { get; set; } = -0;

private List<IObserver> _observers = new List<IObserver>();

public void Attach(IObserver observer) {
_observers.Add(observer);
}

public void Detach(IObserver observer) {
_observers.Remove(observer);
}

public void Notify() {
foreach (var observer in _observers) {
observer.Update(this);
}
}
}

public class ConcreteObserverA : IObserver {
public void Update(ISubject subject) {
if ((subject as ConcreteSubject).State < 3) {
Console.WriteLine("ConcreteObserverA: Reacted to the event.");
}
}
}

public class ConcreteObserverB : IObserver {
public void Update(ISubject subject) {
if ((subject as ConcreteSubject).State == 0 || (subject as ConcreteSubject).State >= 2) {
Console.WriteLine("ConcreteObserverB: Reacted to the event.");
}
}
}

class Program {
static void Main(string[] args) {
var subject = new ConcreteSubject();

var observerA = new ConcreteObserverA();
subject.Attach(observerA);

var observerB = new ConcreteObserverB();
subject.Attach(observerB);

subject.State = 2;
subject.Notify();

subject.State = 3;
subject.Notify();

subject.Detach(observerA);

subject.State = 1;
subject.Notify();
}
}

Use Case: When multiple objects need to be informed about state changes in another object, such as in a publish-subscribe system.

Benefits:

  • Decoupling: Decouples the subject and its observers, promoting loose coupling.
  • Scalability: Supports dynamic relationships between subjects and observers.
  • Flexibility: Allows observers to be added or removed at runtime.

20. State Pattern

Purpose: Allows an object to alter its behavior when its internal state changes, making it appear as if the object has changed its class.

public interface IState {
void Handle(Context context);
}

public class ConcreteStateA : IState {
public void Handle(Context context) {
context.State = new ConcreteStateB();
}
}

public class ConcreteStateB : IState {
public void Handle(Context context) {
context.State = new ConcreteStateA();
}
}

public class Context {
private IState _state;

public Context(IState state) {
this._state = state;
}

public IState State {
get { return _state; }
set {
_state = value;
Console.WriteLine("State: " + _state.GetType().Name);
}
}

public void Request() {
_state.Handle(this);
}
}

class Program {
static void Main(string[] args) {
Context context = new Context(new ConcreteStateA());

context.Request();
context.Request();
context.Request();
context.Request();
}
}

Use Case: When an object’s behavior depends on its state, and it must change behavior at runtime depending on its state.

Benefits:

  • State Management: Encapsulates state-specific behavior and state transitions.
  • Maintainability: Simplifies the addition of new states without modifying existing code.
  • Clarity: Enhances code clarity by separating state-specific logic.

21. Strategy Pattern

Purpose: Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

public interface IStrategy {
object DoAlgorithm(object data);
}

public class ConcreteStrategyA : IStrategy {
public object DoAlgorithm(object data) {
var list = data as List<string>;
list.Sort();
return list;
}
}

public class ConcreteStrategyB : IStrategy {
public object DoAlgorithm(object data) {
var list = data as List<string>;
list.Sort();
list.Reverse();
return list;
}
}

public class Context {
private IStrategy _strategy;

public Context() { }

public Context(IStrategy strategy) {
this._strategy = strategy;
}

public void SetStrategy(IStrategy strategy) {
this._strategy = strategy;
}

public void DoSomeBusinessLogic() {
var result = _strategy.DoAlgorithm(new List<string> { "a", "b", "c", "d", "e" });

string resultStr = string.Empty;
foreach (var element in result as List<string>) {
resultStr += element + ",";
}

Console.WriteLine(resultStr);
}
}

class Program {
static void Main(string[] args) {
var context = new Context();

Console.WriteLine("Client: Strategy is set to normal sorting.");
context.SetStrategy(new ConcreteStrategyA());
context.DoSomeBusinessLogic();

Console.WriteLine();

Console.WriteLine("Client: Strategy is set to reverse sorting.");
context.SetStrategy(new ConcreteStrategyB());
context.DoSomeBusinessLogic();
}
}

Use Case: When you need to use different variants of an algorithm within an application.

Benefits:

  • Flexibility: Enables the selection of algorithms at runtime.
  • Encapsulation: Encapsulates algorithm implementation, promoting clean code separation.
  • Reusability: Facilitates the reuse of algorithms across different contexts.

22. Template Method Pattern

Purpose: Defines the skeleton of an algorithm in a method, deferring some steps to subclasses.

public abstract class AbstractClass {
public void TemplateMethod() {
BaseOperation1();
RequiredOperations1();
BaseOperation2();
Hook1();
RequiredOperation2();
BaseOperation3();
Hook2();
}

protected void BaseOperation1() {
Console.WriteLine("AbstractClass says: I am doing the bulk of the work");
}

protected void BaseOperation2() {
Console.WriteLine("AbstractClass says: But I let subclasses override some operations");
}

protected void BaseOperation3() {
Console.WriteLine("AbstractClass says: But I am doing the bulk of the work anyway");
}

protected abstract void RequiredOperations1();
protected abstract void RequiredOperation2();

protected virtual void Hook1() { }
protected virtual void Hook2() { }
}

public class ConcreteClass1 : AbstractClass {
protected override void RequiredOperations1() {
Console.WriteLine("ConcreteClass1 says: Implemented Operation1");
}

protected override void RequiredOperation2() {
Console.WriteLine("ConcreteClass1 says: Implemented Operation2");
}
}

public class ConcreteClass2 : AbstractClass {
protected override void RequiredOperations1() {
Console.WriteLine("ConcreteClass2 says: Implemented Operation1");
}

protected override void RequiredOperation2() {
Console.WriteLine("ConcreteClass2 says: Implemented Operation2");
}

protected override void Hook1() {
Console.WriteLine("ConcreteClass2 says: Overridden Hook1");
}
}

class Program {
static void Main(string[] args) {
Console.WriteLine("Same client code can work with different subclasses:");

AbstractClass concreteClass1 = new ConcreteClass1();
concreteClass1.TemplateMethod();

Console.WriteLine();

AbstractClass concreteClass2 = new ConcreteClass2();
concreteClass2.TemplateMethod();
}
}

Use Case: When you need to define the structure of an algorithm but allow subclasses to override certain steps without changing the algorithm’s structure.

Benefits:

  • Code Reuse: Promotes code reuse by defining the invariant parts of the algorithm.
  • Consistency: Ensures consistent behavior by defining a fixed algorithm structure.
  • Customization: Allows for customization of specific steps without altering the overall algorithm.

23. Visitor Pattern

Purpose: Represents an operation to be performed on elements of an object structure. It allows defining a new operation without changing the classes of the elements on which it operates.

public interface IComponent {
void Accept(IVisitor visitor);
}

public class ConcreteComponentA : IComponent {
public void Accept(IVisitor visitor) {
visitor.VisitConcreteComponentA(this);
}

public string ExclusiveMethodOfConcreteComponentA() {
return "A";
}
}

public class ConcreteComponentB : IComponent {
public void Accept(IVisitor visitor) {
visitor.VisitConcreteComponentB(this);
}

public string SpecialMethodOfConcreteComponentB() {
return "B";
}
}

public interface IVisitor {
void VisitConcreteComponentA(ConcreteComponentA element);
void VisitConcreteComponentB(ConcreteComponentB element);
}

public class ConcreteVisitor1 : IVisitor {
public void VisitConcreteComponentA(ConcreteComponentA element) {
Console.WriteLine(element.ExclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor1");
}

public void VisitConcreteComponentB(ConcreteComponentB element) {
Console.WriteLine(element.SpecialMethodOfConcreteComponentB() + " + ConcreteVisitor1");
}
}

public class ConcreteVisitor2 : IVisitor {
public void VisitConcreteComponentA(ConcreteComponentA element) {
Console.WriteLine(element.ExclusiveMethodOfConcreteComponentA() + " + ConcreteVisitor2");
}

public void VisitConcreteComponentB(ConcreteComponentB element) {
Console.WriteLine(element.SpecialMethodOfConcreteComponentB() + " + ConcreteVisitor2");
}
}

class Program {
static void Main(string[] args) {
List<IComponent> components = new List<IComponent> {
new ConcreteComponentA(),
new ConcreteComponentB()
};

Console.WriteLine("The client code works with all visitors via the base Visitor interface:");
var visitor1 = new ConcreteVisitor1();
foreach (var component in components) {
component.Accept(visitor1);
}

Console.WriteLine();

Console.WriteLine("It allows the same client code to work with different types of visitors:");
var visitor2 = new ConcreteVisitor2();
foreach (var component in components) {
component.Accept(visitor2);
}
}
}

Use Case: When you need to perform operations across a complex object structure without modifying the classes on which the operations are performed.

Benefits:

  • Separation of Concerns: Separates the algorithms from the object structure, promoting clean code separation.
  • Extensibility: Makes it easy to add new operations without modifying the existing object structure.
  • Maintainability: Simplifies the addition of new behaviors to complex object structures.

Concurrency Patterns

24. Monitor Object Pattern

Purpose: Combines mutual exclusion and condition synchronization to coordinate access to an object among concurrent threads.

public class MonitorObject {
private readonly object _lock = new object();
private int _value = 0;

public void Increment() {
lock (_lock) {
_value++;
Console.WriteLine("Value: " + _value);
}
}

public int Value {
get {
lock (_lock) {
return _value;
}
}
}
}

class Program {
static void Main(string[] args) {
MonitorObject monitorObject = new MonitorObject();

Task task1 = Task.Run(() => {
for (int i = 0; i < 10; i++) {
monitorObject.Increment();
Thread.Sleep(100);
}
});

Task task2 = Task.Run(() => {
for (int i = 0; i < 10; i++) {
monitorObject.Increment();
Thread.Sleep(100);
}
});

Task.WaitAll(task1, task2);
}
}

Use Case: When multiple threads need to safely access and modify shared data.

Benefits:

  • Thread Safety: Ensures safe concurrent access to shared resources.
  • Simplicity: Encapsulates synchronization details within the monitor object, simplifying thread management.
  • Reliability: Reduces the likelihood of concurrency-related bugs, enhancing system reliability.

25. Thread-Specific Storage Pattern

Purpose: Allows multiple threads to have their own independent copies of data, avoiding conflicts.

public class ThreadLocalExample {
private static ThreadLocal<int> _threadLocalValue = new ThreadLocal<int>(() => 0);

public void Increment() {
_threadLocalValue.Value++;
Console.WriteLine("Thread ID: " + Thread.CurrentThread.ManagedThreadId + ", Value: " + _threadLocalValue.Value);
}

public int Value {
get {
return _threadLocalValue.Value;
}
}
}

class Program {
static void Main(string[] args) {
ThreadLocalExample example = new ThreadLocalExample();

Task task1 = Task.Run(() => {
for (int i = 0; i < 10; i++) {
example.Increment();
Thread.Sleep(100);
}
});

Task task2 = Task.Run(() => {
for (int i = 0; i < 10; i++) {
example.Increment();
Thread.Sleep(100);
}
});

Task.WaitAll(task1, task2);
}
}

Use Case: When each thread needs its own instance of a class to maintain thread safety without the overhead of synchronized access.

Benefits:

  • Isolation: Ensures data isolation between threads, preventing conflicts and data corruption.
  • Performance: Improves performance by eliminating the need for synchronization.
  • Simplicity: Simplifies thread management by providing thread-specific data storage.

26. Active Object Pattern

Purpose: Decouples method execution from method invocation to enhance concurrency and simplify synchronized access to objects.

public class ActiveObject {
private readonly BlockingCollection<Action> _queue = new BlockingCollection<Action>();

public ActiveObject() {
Task.Run(() => ProcessQueue());
}

public void Enqueue(Action action) {
_queue.Add(action);
}

private void ProcessQueue() {
foreach (var action in _queue.GetConsumingEnumerable()) {
action();
}
}

public void DoWork() {
Enqueue(() => {
Console.WriteLine("Working on thread " + Thread.CurrentThread.ManagedThreadId);
});
}
}

class Program {
static void Main(string[] args) {
ActiveObject activeObject = new ActiveObject();

for (int i = 0; i < 10; i++) {
activeObject.DoWork();
}

Thread.Sleep(1000); // Wait for all tasks to complete
}
}

Use Case: When you need to separate the execution of operations from their invocation to improve concurrency.

Benefits:

  • Decoupling: Decouples method execution from invocation, enhancing concurrency.
  • Synchronization: Simplifies synchronized access to shared resources.
  • Scalability: Improves the scalability of concurrent applications.

27. Half-Sync/Half-Async Pattern

Purpose: Divides a system into synchronous and asynchronous layers to simplify concurrent programming.

public class HalfSyncHalfAsync {
private readonly BlockingCollection<Action> _syncQueue = new BlockingCollection<Action>();

public HalfSyncHalfAsync() {
Task.Run(() => ProcessQueue());
}

public void EnqueueSync(Action action) {
_syncQueue.Add(action);
}

private void ProcessQueue() {
foreach (var action in _syncQueue.GetConsumingEnumerable()) {
action();
}
}

public async Task EnqueueAsync(Func<Task> asyncAction) {
await asyncAction();
}

public void DoSyncWork() {
EnqueueSync(() => {
Console.WriteLine("Sync work on thread " + Thread.CurrentThread.ManagedThreadId);
});
}

public async Task DoAsyncWork() {
await EnqueueAsync(async () => {
await Task.Delay(100);
Console.WriteLine("Async work on thread " + Thread.CurrentThread.ManagedThreadId);
});
}
}

class Program {
static async Task Main(string[] args) {
HalfSyncHalfAsync system = new HalfSyncHalfAsync();

for (int i = 0; i < 5; i++) {
system.DoSyncWork();
await system.DoAsyncWork();
}

Thread.Sleep(1000); // Wait for all tasks to complete
}
}

Use Case: When you need to balance synchronous and asynchronous processing to improve efficiency and simplicity.

Benefits:

  • Modularity: Separates synchronous and asynchronous concerns, promoting modularity.
  • Simplification: Simplifies concurrent programming by providing clear separation of concerns.
  • Performance: Balances efficiency and simplicity in concurrent applications.

28. Scheduler Pattern

Purpose: Provides a mechanism to control the order and timing of thread execution.

public class TaskSchedulerExample {
private readonly BlockingCollection<Action> _taskQueue = new BlockingCollection<Action>();

public TaskSchedulerExample() {
Task.Run(() => ProcessQueue());
}

public void ScheduleTask(Action action) {
_taskQueue.Add(action);
}

private void ProcessQueue() {
foreach (var task in _taskQueue.GetConsumingEnumerable()) {
task();
}
}

public void DoScheduledWork() {
ScheduleTask(() => {
Console.WriteLine("Scheduled work on thread " + Thread.CurrentThread.ManagedThreadId);
});
}
}

class Program {
static void Main(string[] args) {
TaskSchedulerExample scheduler = new TaskSchedulerExample();

for (int i = 0; i < 10; i++) {
scheduler.DoScheduledWork();
}

Thread.Sleep(1000); // Wait for all tasks to complete
}
}

Use Case: When you need to manage the execution order and timing of multiple threads.

Benefits:

  • Control: Provides fine-grained control over thread execution.
  • Efficiency: Optimizes the use of system resources by managing thread scheduling.
  • Flexibility: Allows for flexible scheduling policies.

The Essence of Advanced Design Patterns

Advanced design patterns address more complex scenarios and challenges in software development. They provide refined solutions that go beyond basic problems, offering intricate mechanisms to tackle issues related to concurrency, state management, and structural complexities. Understanding these patterns equips developers with the tools to build more sophisticated and high-performing systems.

Categories of Advanced Design Patterns

  1. Creational Patterns: Advanced object creation techniques that provide additional flexibility and scalability.
  2. Structural Patterns: More intricate ways of composing objects to form larger structures.
  3. Behavioral Patterns: Complex interaction patterns between objects to ensure flexible and efficient communication.

--

--

LordNeic

Software Architect • Laravel • Web & Game Programmer • Full-stack • book writer • Board game designer • Public Speaker • Motivation, Business and Sales Coach