What is the SOLID Principle?
SOLID is an acronym for five key principles of object-oriented design that help create clean, scalable, testable, and maintainable code. These principles are especially useful when building layered applications like ASP.NET Core Web APIs or MVC apps.
SOLID stands for:
- S - Single Responsibility Principle
- O - Open/Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
Let's understand each point with an example:
1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change.
In ASP.NET Core, separate business logic, data access, and controller logic.
❌ Bad Example (Violates SRP).
public class ProductService { public void Save(Product p) { /* save to DB */ } public void SendEmail(Product p) { /* email logic */ } }
public class ProductService { private readonly IProductRepository _repo; public ProductService(IProductRepository repo) => _repo = repo; public void Save(Product product) => _repo.Add(product); } public class EmailService { public void SendEmail(Product p) { /* only email logic */ } }
public interface ILoggerService { void Log(string message); } public class FileLogger : ILoggerService { public void Log(string message) { /* log to file */ } } public class DbLogger : ILoggerService { public void Log(string message) { /* log to DB */ } }
public class BrokenNotificationService : NotificationService { public override void Send(string message) { throw new NotImplementedException(); // ❌ Violates LSP } }
public abstract class NotificationService { public abstract void Send(string message); }
public class EmailNotificationService : NotificationService { public override void Send(string message) { Console.WriteLine($"Email sent: {message}"); } } public class SmsNotificationService : NotificationService { public override void Send(string message) { Console.WriteLine($"SMS sent: {message}"); } }
public class NotificationController { private readonly NotificationService _service; public NotificationController(NotificationService service) { _service = service; } public void NotifyUser() { _service.Send("Hello, User!"); } }
var controller1 = new NotificationController(new EmailNotificationService()); controller1.NotifyUser(); // Output: Email sent: Hello, User! var controller2 = new NotificationController(new SmsNotificationService()); controller2.NotifyUser(); // Output: SMS sent: Hello, User!
- Inheritance + LSP = Child classes must work in place of their base class.
- Avoid incomplete or broken implementations in subclasses.
- In ASP.NET Core, always ensure services or controller dependencies fully implement expected behavior.
public interface IProductService { void Add(); void Update(); void ExportToExcel(); // not every implementation needs this }
public interface ICrudService { void Add(); void Update(); } public interface IExportService { void ExportToExcel(); }
public interface IEmailService { void Send(string to, string message); } public class SmtpEmailService : IEmailService { public void Send(string to, string message) { // logic to send email } } public class NotificationController : ControllerBase { private readonly IEmailService _emailService; public NotificationController(IEmailService emailService) { _emailService = emailService; } public IActionResult Notify() { _emailService.Send("user@example.com", "Hello!"); return Ok(); } }
- Easier to test
- Simpler to maintain
- More scalable and flexible
Design Patterns
1. Singleton Pattern.
- Logging services
- Configuration settings
- Caching
- Shared services (only if thread-safe and stateless)
public class AppLogger { // Static instance private static readonly AppLogger _instance = new AppLogger(); // Private constructor private AppLogger() { } // Public static property to access the instance public static AppLogger Instance => _instance; // Sample method public void Log(string message) { Console.WriteLine($"[LOG] {DateTime.Now}: {message}"); } }
class Program { static void Main(string[] args) { var logger1 = AppLogger.Instance; var logger2 = AppLogger.Instance; logger1.Log("Singleton is working!"); logger2.Log("Same instance is used again."); Console.WriteLine(ReferenceEquals(logger1, logger2)); // Output: True } }
2. Factory Pattern.
new
everywhere, I call a Factory method, which decides what class to return. This keeps my code flexible and clean."- When object creation logic is complex or repetitive
- When you want to decouple object creation from your main logic
- When the class to instantiate is determined at runtime
public interface INotification { void Send(string message); }
public class EmailNotification : INotification { public void Send(string message) { Console.WriteLine("Email sent: " + message); } } public class SmsNotification : INotification { public void Send(string message) { Console.WriteLine("SMS sent: " + message); } } public class PushNotification : INotification { public void Send(string message) { Console.WriteLine("Push sent: " + message); } }
public class NotificationFactory { public static INotification Create(string type) { return type.ToLower() switch { "email" => new EmailNotification(), "sms" => new SmsNotification(), "push" => new PushNotification(), _ => throw new ArgumentException("Invalid notification type"), }; } }
class Program { static void Main() { Console.Write("Enter notification type (email/sms/push): "); string type = Console.ReadLine(); INotification notification = NotificationFactory.Create(type); notification.Send("Hello from Factory Pattern!"); } }
3. Repository Pattern.
- Centralized database logic
- Make the code more testable
- Enforce Separation of Concerns (SoC)
public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } }
public class AppDbContext : DbContext { public DbSet<Product> Products { get; set; } public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } }
public interface IProductRepository { IEnumerable<Product> GetAll(); Product GetById(int id); void Add(Product product); void Update(Product product); void Delete(int id); }
public class ProductRepository : IProductRepository { private readonly AppDbContext _context; public ProductRepository(AppDbContext context) { _context = context; } public IEnumerable<Product> GetAll() => _context.Products.ToList(); public Product GetById(int id) => _context.Products.Find(id); public void Add(Product product) { _context.Products.Add(product); _context.SaveChanges(); } public void Update(Product product) { _context.Products.Update(product); _context.SaveChanges(); } public void Delete(int id) { var product = _context.Products.Find(id); if (product != null) { _context.Products.Remove(product); _context.SaveChanges(); } } }
builder.Services.AddScoped<IProductRepository, ProductRepository>(); builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
[ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly IProductRepository _repo; public ProductsController(IProductRepository repo) { _repo = repo; } [HttpGet] public IActionResult Get() => Ok(_repo.GetAll()); [HttpPost] public IActionResult Post(Product product) { _repo.Add(product); return CreatedAtAction(nameof(Get), new { id = product.Id }, product); } }
No comments:
Post a Comment