Design patterns: Classification and examples in C#
As a software developer with over a decade of experience working in large companies, I've seen how design patterns have transformed the way we approach software construction. These patterns provide reusable, proven solutions to common software design problems, enhancing code efficiency and quality. In this article, we will explore the classification of design patterns, provide a brief description of each, and offer practical examples in C#.
Classification of Design Patterns
Design patterns are generally classified into three main categories:
1. Creational Patterns: These patterns focus on how objects are created, helping to make the system independent of how its objects are created, composed, and represented.
2. Structural Patterns: These patterns deal with the composition of classes or objects, forming larger and more complex structures.
3. Behavioral Patterns: These patterns focus on communication between objects and delegation of responsibilities.
Creational Patterns
1. Singleton: Ensures a class has only one instance and provides a global point of access to it.
Example in C#:
public class Singleton
{
private static Singleton _instance;
private Singleton() { }
public static Singleton Instance
{
get
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
2. Factory Method: Defines an interface for creating an object but allows subclasses to alter the type of objects that will be created.
Example in C#:
public abstract class Creator
{
public abstract IProduct FactoryMethod();
}
public class ConcreteCreator : Creator
{
public override IProduct FactoryMethod()
{
return new ConcreteProduct();
}
}
public interface IProduct { }
public class ConcreteProduct : IProduct { }
3. Abstract Factory: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Example in C#:
public interface IAbstractFactory
{
IProductA CreateProductA();
IProductB CreateProductB();
}
public class ConcreteFactory1 : IAbstractFactory
{
public IProductA CreateProductA() => new ProductA1();
public IProductB CreateProductB() => new ProductB1();
}
public interface IProductA { }
public interface IProductB { }
public class ProductA1 : IProductA { }
public class ProductB1 : IProductB { }
Structural Patterns
1. Adapter: Allows classes with incompatible interfaces to work together by converting the interface of a class into another interface the client expects.
Example in C#:
领英推荐
public interface ITarget
{
void Request();
}
public class Adaptee
{
public void SpecificRequest()
{
Console.WriteLine("Called SpecificRequest()");
}
}
public class Adapter : ITarget
{
private readonly Adaptee _adaptee;
public Adapter(Adaptee adaptee)
{
_adaptee = adaptee;
}
public void Request()
{
_adaptee.SpecificRequest();
}
}
2. Decorator: Adds responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Example in C#:
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)
{
_component = component;
}
public override void Operation()
{
_component?.Operation();
}
}
public class ConcreteDecorator : Decorator
{
public override void Operation()
{
base.Operation();
AddedBehavior();
}
void AddedBehavior()
{
Console.WriteLine("ConcreteDecorator.AddedBehavior()");
}
}
3. Composite: Composes objects into tree structures to represent part-whole hierarchies. It allows clients to treat individual objects and compositions of objects uniformly.
Example in C#:
public abstract class Component
{
public abstract void Operation();
}
public class Leaf : Component
{
public override void Operation()
{
Console.WriteLine("Leaf.Operation()");
}
}
public class Composite : Component
{
private readonly List<Component> _children = new List<Component>();
public void Add(Component component)
{
_children.Add(component);
}
public void Remove(Component component)
{
_children.Remove(component);
}
public override void Operation()
{
Console.WriteLine("Composite.Operation()");
foreach (Component component in _children)
{
component.Operation();
}
}
}
Behavioral Patterns
1. Observer: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Example in C#:
public interface IObserver
{
void Update();
}
public interface ISubject
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}
public class ConcreteSubject : ISubject
{
private readonly 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();
}
}
}
public class ConcreteObserver : IObserver
{
public void Update()
{
Console.WriteLine("Observer updated.");
}
}
2. Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Allows the algorithm to vary independently from clients that use it.
Example in C#:
public interface IStrategy
{
void AlgorithmInterface();
}
public class ConcreteStrategyA : IStrategy
{
public void AlgorithmInterface()
{
Console.WriteLine("ConcreteStrategyA.AlgorithmInterface()");
}
}
public class ConcreteStrategyB : IStrategy
{
public void AlgorithmInterface()
{
Console.WriteLine("ConcreteStrategyB.AlgorithmInterface()");
}
}
public class Context
{
private IStrategy _strategy;
public Context(IStrategy strategy)
{
_strategy = strategy;
}
public void SetStrategy(IStrategy strategy)
{
_strategy = strategy;
}
public void ContextInterface()
{
_strategy.AlgorithmInterface();
}
}
3. Command: Encapsulates a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and logging of the requests. It also supports undoable operations.
Example in C#:
public interface ICommand
{
void Execute();
}
public class ConcreteCommand : ICommand
{
private readonly Receiver _receiver;
public ConcreteCommand(Receiver receiver)
{
_receiver = receiver;
}
public void Execute()
{
_receiver.Action();
}
}
public class Receiver
{
public void Action()
{
Console.WriteLine("Receiver.Action()");
}
}
public class Invoker
{
private ICommand _command;
public void SetCommand(ICommand command)
{
_command = command;
}
public void ExecuteCommand()
{
_command.Execute();
}
}
Conclusion
Design patterns are fundamental tools in software development, providing effective solutions to common problems and facilitating the creation of robust and maintainable software. I hope this article has given you a clear and concise view of the different types of design patterns and their practical applications in C#. Implementing them properly can make a significant difference in the quality and efficiency of our software projects.
Thank you for reading!.