Understanding Design Patterns in .NET
Design patterns are 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 templates or descriptions for how to solve a problem that can be used in many different situations.
Creational Patterns
These patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.
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 (Simplified) public abstract class Creator { public abstract Product FactoryMethod(); public string SomeOperation() { Product product = this.FactoryMethod(); return "Creator: The same creator's code has just worked with " + product.Operation(); } } public class ConcreteCreatorA : Creator { public override Product FactoryMethod() { return new ConcreteProductA(); } } public abstract class Product { public abstract string Operation(); } public class ConcreteProductA : Product { public override string Operation() { return "{Result of ConcreteProductA}"; } }
Singleton
Ensures a class only has one instance and provides a global point of access to it.
public sealed class Singleton { private static readonly Singleton instance = new Singleton(); private Singleton() { } public static Singleton Instance { get { return instance; } } public void SomeServiceMethod() { // ... } }
Structural Patterns
These patterns are concerned with class and object composition. They use inheritance to compose interfaces for common functionality.
Adapter
Converts the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
// Target Interface public interface ITarget { string Request(); } // Adaptee (incompatible interface) public class Adaptee { public string SpecificRequest() { return "Called SpecificRequest()"; } } // Adapter public class Adapter : ITarget { private readonly Adaptee _adaptee; public Adapter(Adaptee adaptee) { _adaptee = adaptee; } public string Request() { return "Adapter: " + _adaptee.SpecificRequest(); } }
Decorator
Attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
// Component Interface public interface IDecoratorComponent { string Operation(); } // Concrete Component public class ConcreteDecoratorComponent : IDecoratorComponent { public string Operation() { return "ConcreteDecoratorComponent"; } } // Base Decorator Class public abstract Decorator : IDecoratorComponent { protected IDecoratorComponent _wrappedComponent; public Decorator(IDecoratorComponent component) { _wrappedComponent = component; } public virtual string Operation() { return _wrappedComponent.Operation(); } } // Concrete Decorator public class ConcreteDecoratorA : Decorator { public ConcreteDecoratorA(IDecoratorComponent component) : base(component) { } public override string Operation() { return "ConcreteDecoratorA(" + base.Operation() + ")"; } }
Behavioral Patterns
These patterns are concerned with algorithms and the assignment of responsibilities between objects.
Observer
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
// Subject public interface ISubject { void Attach(IObserver observer); void Detach(IObserver observer); void Notify(); } // Concrete Subject public class ConcreteSubject : ISubject { public int State { get; set; } 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); } } } // Observer Interface public interface IObserver { void Update(ISubject subject); } // Concrete Observer public class ConcreteObserverA : IObserver { public void Update(ISubject subject) { if (subject is ConcreteSubject concreteSubject) { Console.WriteLine($"Observer A received update. New state: {concreteSubject.State}"); } } }
Strategy
Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
// Strategy Interface public interface IStrategy { string Execute(string data); } // Concrete Strategies public class ConcreteStrategyA : IStrategy { public string Execute(string data) { return "ConcreteStrategyA processed: " + data.ToLower(); } } public class ConcreteStrategyB : IStrategy { public string Execute(string data) { return "ConcreteStrategyB processed: " + data.ToUpper(); } } // Context public class Context { private IStrategy _strategy; public Context() { } public void SetStrategy(IStrategy strategy) { _strategy = strategy; } public string ExecuteStrategy(string data) { if (_strategy == null) { throw new InvalidOperationException("Strategy not set."); } return _strategy.Execute(data); } }
Exploring and implementing design patterns can significantly improve the quality, maintainability, and scalability of your .NET applications.
Explore More Patterns Back to Concepts