Design Patterns in .NET Core
Design patterns are critical for writing scalable and maintainable applications in .NET Core. They allow developers to address common software design challenges and improve code reuse, flexibility, and readability. Below is a breakdown of key patterns used in .NET Core along with tricky interview questions.
Creational Design Patterns
1. Singleton Pattern
Definition: The Singleton pattern ensures a class has only one instance and provides a global point of access to it. This is commonly used for scenarios like logging or configuration management in .NET Core.
public sealed class Logger {
private static Logger _instance;
private static readonly object _lock = new object();
private Logger() { }
public static Logger Instance {
get {
lock (_lock) {
if (_instance == null) {
_instance = new Logger();
}
return _instance;
}
}
}
}
Answer: No, a Singleton class should not be inherited to ensure only one instance is created. Mark the class
sealed
to prevent inheritance.
2. Factory Method Pattern
Definition: The Factory Method pattern provides an interface for creating objects but allows subclasses to alter the type of objects that will be created. It is ideal for situations where the exact class of objects is unknown until runtime.
public abstract class PaymentFactory {
public abstract IPayment CreatePayment();
}
public class CreditCardFactory : PaymentFactory {
public override IPayment CreatePayment() {
return new CreditCardPayment();
}
}
public class PayPalFactory : PaymentFactory {
public override IPayment CreatePayment() {
return new PayPalPayment();
}
}
Answer: It decouples object creation from the implementation by abstracting the instantiation process, allowing flexibility in creating objects.
3. Dependency Injection (DI)
Definition: DI is a technique where the dependencies of a class are injected by an external entity. .NET Core has built-in support for DI, making it easy to manage service lifetimes.
public class MyService {
private readonly IMyDependency _dependency;
public MyService(IMyDependency dependency) {
_dependency = dependency;
}
}
Answer: DI allows mocking dependencies during testing, which makes it easier to isolate and test individual components.
Structural Design Patterns
1. Adapter Pattern
Definition: The Adapter pattern allows incompatible interfaces to work together by converting the interface of a class into another interface that clients expect.
public interface ILogger {
void Log(string message);
}
public class ThirdPartyLogger {
public void WriteLog(string log) {
Console.WriteLine(log);
}
}
public class LoggerAdapter : ILogger {
private readonly ThirdPartyLogger _thirdPartyLogger;
public LoggerAdapter(ThirdPartyLogger thirdPartyLogger) {
_thirdPartyLogger = thirdPartyLogger;
}
public void Log(string message) {
_thirdPartyLogger.WriteLog(message);
}
}
Answer: By allowing existing classes to work with new clients without modifying the original code, promoting code reuse.
2. Decorator Pattern
Definition: The Decorator pattern allows behavior to be added to an object dynamically without modifying its structure. This is useful when behavior needs to change based on the context.
public interface INotifier {
void Send(string message);
}
public class EmailNotifier : INotifier {
public void Send(string message) {
Console.WriteLine($"Email sent: {message}");
}
}
public class SMSDecorator : INotifier {
private readonly INotifier _notifier;
public SMSDecorator(INotifier notifier) {
_notifier = notifier;
}
public void Send(string message) {
_notifier.Send(message);
Console.WriteLine($"SMS sent: {message}");
}
}
Answer: The Decorator pattern is preferable when you need to add behavior dynamically, while inheritance is more static and rigid.
Behavioral Design Patterns
1. Observer Pattern
Definition: The Observer pattern defines a one-to-many relationship where multiple objects (observers) are notified whenever the state of another object (subject) changes.
public class Subject {
public event Action Notify;
public void ChangeState() {
Notify?.Invoke();
}
}
public class Observer {
public void Update() {
Console.WriteLine("Observer has been notified.");
}
}
Answer: The Observer pattern has direct relationships between subjects and observers, while Pub/Sub uses a message broker to decouple these relationships.
2. Command Pattern
Definition: The Command pattern encapsulates a request as an object, allowing parameterization of clients, request queueing, and undo operations.
public interface ICommand {
void Execute();
}
public class Light {
public void TurnOn() { Console.WriteLine("Light is On"); }
public void TurnOff() { Console.WriteLine("Light is Off"); }
}
public class TurnOnCommand : ICommand {
private readonly Light _light;
public TurnOnCommand(Light light) {
_light = light;
}
public void Execute() {
_light.TurnOn();
}
}
Answer: Each command should have an
Unexecute()
method that reverses the Execute()
operation. Use a stack to manage commands and undo them in reverse order.