Hello, C# developers! Today, we’re going to explore design patterns in C#. Design patterns are proven solutions to common software design problems. They provide a standard terminology and are a way to communicate design principles to other developers. In this post, we’ll cover some of the most commonly used design patterns in C# and how they can enhance the architecture of your applications.
What are Design Patterns?
Design patterns are general reusable solutions to common problems that occur within a given context in software design. They are categorized into three main types:
- Creational Patterns: Concerned with the way of creating objects. They provide various ways to create objects while hiding the creation logic, rather than instantiating objects directly.
- Structural Patterns: Deal with object composition and help ensure that if one part of a system changes, the entire system doesn’t need to change.
- Behavioral Patterns: Focus on communication between objects, what goes on between objects and how they operate together.
Common Creational Patterns
Let’s discuss some common creational patterns:
1. Singleton Pattern
The Singleton Pattern ensures a class has only one instance and provides a global point of access to it. It’s often used for database connections or configuration settings.
public class Singleton
{
private static Singleton instance;
private static readonly object padlock = new object();
public static Singleton Instance
{
get
{
lock (padlock)
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}
private Singleton() { } // Private constructor
}
In this example, the Singleton
class uses a private constructor to prevent instantiation from outside the class and provides a static method to access the single instance.
2. Factory Method Pattern
The Factory Method Pattern provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created.
public abstract class Animal
{
public abstract string Speak();
}
public class Dog : Animal
{
public override string Speak() => "Woof!";
}
public class Cat : Animal
{
public override string Speak() => "Meow!";
}
public class AnimalFactory
{
public static Animal CreateAnimal(string type)
{
if (type == "Dog") return new Dog();
if (type == "Cat") return new Cat();
throw new ArgumentException("Invalid animal type");
}
}
With this pattern, the AnimalFactory
creates instances of Dog
or Cat
based on the provided type.
Common Structural Patterns
Now, let’s take a look at some structural patterns:
1. Adapter Pattern
The Adapter Pattern allows the interface of an existing class to be used as another interface. It acts as a bridge between two incompatible types.
public interface ITarget
{
string Request();
}
public class Adaptee
{
public string SpecificRequest() => "Specific request";
}
public class Adapter : ITarget
{
private readonly Adaptee adaptee;
public Adapter(Adaptee adaptee)
{
this.adaptee = adaptee;
}
public string Request() => adaptee.SpecificRequest();
}
The Adapter
class enables a client to interact with the Adaptee
type without modifying its code.
2. Composite Pattern
The Composite Pattern allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions uniformly.
public interface IComponent
{
string Operation();
}
public class Leaf : IComponent
{
public string Operation() => "Leaf";
}
public class Composite : IComponent
{
private readonly List<IComponent> components = new List<IComponent>();
public void Add(IComponent component) => components.Add(component);
public string Operation()
{
return "Composite: " + string.Join(", ", components.Select(c => c.Operation()));
}
}
This structure allows you to create a tree of objects where each object can be treated the same way, simplifying client code.
Common Behavioral Patterns
Let’s discuss some behavioral patterns:
1. 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 and updated automatically.
public class Subject
{
private readonly List<IObserver> observers = new List<IObserver>();
public void Attach(IObserver observer) => observers.Add(observer);
public void Notify()
{
foreach (var observer in observers)
{
observer.Update();
}
}
}
public interface IObserver
{
void Update();
}
The Subject
class maintains a list of observers and notifies them when necessary.
2. Strategy Pattern
The Strategy Pattern enables selecting an algorithm at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable.
public interface ISortStrategy
{
void Sort(List<int> list);
}
public class BubbleSort : ISortStrategy
{
public void Sort(List<int> list) { /* Bubble sort implementation */ }
}
public class QuickSort : ISortStrategy
{
public void Sort(List<int> list) { /* Quick sort implementation */ }
}
public class SortContext
{
private ISortStrategy sortStrategy;
public void SetSortStrategy(ISortStrategy strategy) => sortStrategy = strategy;
public void Sort(List<int> list) => sortStrategy.Sort(list);
}
In this example, SortContext
allows you to set the specific sorting algorithm without modifying the way the list is sorted.
Conclusion
Design patterns are essential tools that can help you create scalable and maintainable applications in C#. Understanding the various design patterns available and knowing when to implement them can significantly enhance your code quality and development process. Dive deeper into each pattern to understand its strengths and nuances.
To learn more about ITER Academy, visit our website. Visit Here