Single Responsibility Principle (SRP) and Factory Pattern
The Single Responsibility Principle states that a class should have one reason to change, promoting separation of concerns. The Factory pattern aligns with SRP by isolating object creation, allowing classes to focus on their primary responsibilities.
Example: Factory Pattern Supporting SRP
public interface IProduct {
void Display();
}
public class Electronics : IProduct {
public void Display() => Console.WriteLine("Displaying Electronics");
}
public class Furniture : IProduct {
public void Display() => Console.WriteLine("Displaying Furniture");
}
public class ProductFactory {
public static IProduct CreateProduct(string type) {
return type switch {
"Electronics" => new Electronics(),
"Furniture" => new Furniture(),
_ => throw new ArgumentException("Invalid product type")
};
}
}
In this example, `ProductFactory` handles product creation, keeping each product class focused on its own responsibilities. This separation enhances code organization and simplifies maintenance.
Open/Closed Principle (OCP) and Strategy Pattern
The Open/Closed Principle suggests that classes should be open for extension but closed for modification. The Strategy pattern aligns with OCP by allowing behavior to be extended without modifying the original class.
Example: Strategy Pattern Supporting OCP
public interface IDiscountStrategy {
decimal ApplyDiscount(decimal amount);
}
public class NoDiscount : IDiscountStrategy {
public decimal ApplyDiscount(decimal amount) => amount;
}
public class SeasonalDiscount : IDiscountStrategy {
public decimal ApplyDiscount(decimal amount) => amount * 0.9m;
}
public class ShoppingCart {
private IDiscountStrategy _discountStrategy;
public ShoppingCart(IDiscountStrategy discountStrategy) {
_discountStrategy = discountStrategy;
}
public decimal CalculateTotal(decimal amount) => _discountStrategy.ApplyDiscount(amount);
}
// Usage
var cart = new ShoppingCart(new SeasonalDiscount());
Console.WriteLine(cart.CalculateTotal(100)); // Output: 90
The Strategy pattern enables adding new discount types without modifying the `ShoppingCart` class, adhering to OCP and allowing easy extensions.
Liskov Substitution Principle (LSP) and Decorator Pattern
Liskov Substitution Principle states that subclasses should be substitutable for their base classes without altering functionality. The Decorator pattern aligns with LSP by enhancing functionality without changing the underlying interface, making it possible to use decorated objects interchangeably.
Example: Decorator Pattern Supporting LSP
public interface INotifier {
void Send(string message);
}
public class EmailNotifier : INotifier {
public void Send(string message) => Console.WriteLine($"Email: {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: {message}");
}
}
// Usage
INotifier notifier = new SMSDecorator(new EmailNotifier());
notifier.Send("Hello World");
// Output:
// Email: Hello World
// SMS: Hello World
The `SMSDecorator` can wrap any `INotifier`, extending functionality without altering the original behavior, in line with LSP.
Interface Segregation Principle (ISP) and Observer Pattern
The Interface Segregation Principle suggests that classes should not be forced to implement interfaces they do not use. The Observer pattern supports ISP by defining specific observer interfaces for different types of updates, allowing clients to subscribe only to relevant updates.
Example: Observer Pattern Supporting ISP
public interface IObserver {
void Update(string message);
}
public class ConcreteObserver : IObserver {
public void Update(string message) => Console.WriteLine($"Observer received: {message}");
}
public class Subject {
private List<IObserver> observers = new();
public void Subscribe(IObserver observer) => observers.Add(observer);
public void NotifyObservers(string message) {
foreach (var observer in observers) observer.Update(message);
}
}
// Usage
var subject = new Subject();
var observer = new ConcreteObserver();
subject.Subscribe(observer);
subject.NotifyObservers("New Update!"); // Observer receives the message
By separating `IObserver` into smaller, relevant interfaces, the Observer pattern enables targeted updates, adhering to ISP by keeping interfaces specific and concise.
Dependency Inversion Principle (DIP) and Singleton Pattern
The Dependency Inversion Principle suggests that high-level modules should not depend on low-level modules but on abstractions. The Singleton pattern can support DIP when applied to services shared across the application, as long as access is mediated through an interface or dependency injection.
Example: Singleton Pattern Supporting DIP
public interface ILogger {
void Log(string message);
}
public class Logger : ILogger {
private static readonly Lazy<Logger> instance = new(() => new Logger());
public static Logger Instance => instance.Value;
private Logger() {}
public void Log(string message) => Console.WriteLine($"Log: {message}");
}
// Usage
ILogger logger = Logger.Instance;
logger.Log("Application started"); // Output: Log: Application started
By accessing `Logger` through the `ILogger` interface, the code adheres to DIP, enabling flexibility to switch logging implementations if needed.
Best Practices for SOLID Principles and Design Patterns
- Apply Principles Selectively: Use SOLID principles to solve specific issues without overengineering, ensuring that each principle adds clear value.
- Combine Patterns Wisely: Patterns like Factory and Singleton can coexist to manage creation while maintaining global access where necessary.
- Focus on Flexibility: Aim for a flexible design that supports future changes with minimal modifications, aligning with OCP and DIP.
Conclusion
Combining SOLID principles with design patterns results in flexible, maintainable code that adapts to evolving requirements. By applying patterns like Factory for SRP, Strategy for OCP, and Decorator for LSP, developers can create robust applications that adhere to industry standards, improving readability, scalability, and long-term maintainability. Leveraging SOLID principles with design patterns ensures a well-structured codebase that supports continuous growth and refinement.