Use design patterns and architectural principles to write clean, scalable, and maintainable software.
Welcome to our chapter on the SOLID Principles! These five fundamental design principles will help you create cleaner, more maintainable code in your C# projects. Let's dive into each principle and see how they can transform your approach to software architecture.
The Single Responsibility Principle states that a class should have only one reason to change. In other words, each class should have a single responsibility or job.
The Open/Closed Principle states that classes should be open for extension but closed for modification. This means you can add new functionality without changing the existing code.
The Liskov Substitution Principle states that objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program.
The Interface Segregation Principle states that no client should be forced to depend on methods it does not use. Instead of one large interface, create smaller, specialized interfaces.
The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions.
Let's see how these principles can be applied in real-world scenarios. We'll use the example of a file processing system.
// Good implementation - Single responsibility
class FileProcessor {
public void Process(string filePath) {
var content = ReadFile(filePath);
var result = AnalyzeContent(content);
SaveResult(result);
}
}
// Bad implementation - Multiple responsibilities
class FileProcessor {
public void ProcessAndLog(string filePath) {
var content = ReadFile(filePath);
var result = AnalyzeContent(content);
SaveResult(result);
LogProcessing(filePath, result);
}
}
// Good implementation - Open for extension
class FileProcessor {
public void Process(string filePath) {
var content = ReadFile(filePath);
var result = AnalyzeContent(content);
SaveResult(result);
}
}
interface IAnalyzer {
object Analyze(string content);
}
// Extend functionality by creating new implementations
class CSVAnalyzer : IAnalyzer {
public object Analyze(string csv) {
// Analysis logic for CSV files
}
}
// Good implementation - Liskov substitution
class Shape {
public virtual void Draw() { }
}
class Circle : Shape {
public override void Draw() {
// Draw circle logic
}
}
// Good implementation - Interface segregation
interface IReadble {
string Read();
}
interface IWrittable {
void Write(string content);
}
// Bad implementation - Fat interface
interface IFileSystem {
string Read();
void Write(string content);
void Delete();
}
// Good implementation - Dependency inversion
interface ILogger {
void Log(string message);
}
class FileProcessor {
private readonly ILogger _logger;
public FileProcessor(ILogger logger) {
_logger = logger;
}
public void Process(string filePath) {
// Processing logic
_logger.Log("File processed successfully");
}
}
Welcome to our chapter on Common Design Patterns in C#! In this section, we'll explore some of the most widely used patterns that help developers create maintainable, scalable, and efficient software. Whether you're building web applications, desktop apps, or enterprise systems, these patterns will be your go-to tools for solving common architectural challenges.
The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it. This is useful for managing resources like database connections or logging systems.
public class DatabaseConnection
{
private static DatabaseConnection _instance;
private DatabaseConnection() {}
public static DatabaseConnection Instance
{
get
{
if (_instance == null)
_instance = new DatabaseConnection();
return _instance;
}
}
}
The Factory Pattern provides an interface for creating objects without specifying their exact implementation. This promotes loose coupling and allows for easier maintenance of the codebase.
public abstract class LoggerFactory
{
public abstract ILogger CreateLogger();
}
public class ConsoleLoggerFactory : LoggerFactory
{
public override ILogger CreateLogger()
{
return new ConsoleLogger();
}
}
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This allows for dynamic selection of the algorithm used at runtime.
public interface ISortingStrategy
{
void Sort(int[] array);
}
public class BubbleSortStrategy : ISortingStrategy
{
public void Sort(int[] array) { /* Implementation */ }
}
The Observer Pattern establishes a one-to-many relationship between objects so that when one object changes state, all its dependents are notified and updated automatically. This is commonly used in event-driven systems.
public interface IObservable
{
void Subscribe(IObserver observer);
void Unsubscribe(IObserver observer);
void Notify();
}
public class StockTicker : IObservable
{
private List<IObserver> _observers = new List<IObserver>();
public void UpdatePrice(decimal price) { /* Implementation */ }
}
The Repository Pattern serves as a layer between the application and the database, abstracting data access operations. This promotes separation of concerns and makes the code more testable.
public interface IRepository<T>
{
IEnumerable<T> GetAll();
T GetById(int id);
void Add(T entity);
void Update(T entity);
void Delete(T entity);
}
public class EfRepository<T> : IRepository<T> where T : class
{
private readonly DbContext _context;
public EfRepository(DbContext context)
{
_context = context;
}
}
Clean Architecture is a software design approach that focuses on creating modular, testable, and maintainable codebases. It emphasizes separating concerns and decoupling modules to improve overall system flexibility.
Dependency Injection (DI) is a technique where components receive their dependencies from external sources rather than creating them internally. This improves testability and maintainability.
// Without DI
public class UserService {
private readonly Database _database = new Database();
public void SaveUser(User user) {
_database.Save(user);
}
}
// With DI
public class UserService {
private readonly IDatabase _database;
public UserService(IDatabase database) {
_database = database;
}
public void SaveUser(User user) {
_database.Save(user);
}
}
The .NET Core DI Container provides built-in support for dependency injection through the IServiceProvider interface.
public class Program {
public static void Main(string[] args) {
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddSingleton<IDatabase, Database>();
var app = builder.Build();
app.Run();
}
}
new()
.Question 1 of 15