Want to write clean, scalable, and maintainable C# code? Let’s break down the OOP fundamentals and the SOLID design principles — with real-world examples, explanations, and code snippets you can copy-paste into your next project.
📚 Table of Contents
- 🎯 OOP fundamentals
- 🔒 Encapsulation
- 🎭 Abstraction
- 🧬 Inheritance
- 🔄 Polymorphism
- 🏗️ SOLID principles
- S — Single Responsibility Principle
- O — Open/Closed Principle
- L — Liskov Substitution Principle
- I — Interface Segregation Principle
- D — Dependency Inversion Principle
- 🛒 Mini e-commerce scenario (tying everything together)
- 💡 Next steps (make it runnable!)
🎯 1. OOP Fundamentals
🔒 Encapsulation — protect your object’s state
👉 Idea: Don’t let anyone mess with your object’s internals. Provide safe, controlled methods.
Use-case: An Order should manage its own items and discounts.
public class Order
{
private readonly List<OrderItem> _items = new();
private decimal _discountPercent;
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public void AddItem(OrderItem item)
{
if (item == null) throw new ArgumentNullException(nameof(item));
_items.Add(item);
}
public void SetDiscount(decimal percent)
{
if (percent < 0 || percent > 100) throw new ArgumentOutOfRangeException();
_discountPercent = percent;
}
public decimal CalculateTotal()
{
var subtotal = _items.Sum(i => i.Price * i.Quantity);
return subtotal * (1 - _discountPercent / 100m);
}
}
✨ Why it’s encapsulation: Items are private, exposed as read-only, and discounts are validated before being applied.
🎭 Abstraction — hide the messy details
👉 Idea: Expose what something does, not how it does it.
Use-case: Different payment providers (Stripe, PayPal).
public interface IPaymentProcessor
{
bool Charge(decimal amount, string currency);
}
public class StripeProcessor : IPaymentProcessor
{
public bool Charge(decimal amount, string currency)
{
Console.WriteLine($"Stripe charged {amount} {currency}");
return true;
}
}
public class PaymentService
{
private readonly IPaymentProcessor _processor;
public PaymentService(IPaymentProcessor processor) => _processor = processor;
public bool Pay(decimal amount, string currency) => _processor.Charge(amount, currency);
}
✨ Why it’s abstraction: PaymentService doesn’t care which processor it uses — just that it can Charge.
🧬 Inheritance — reuse what’s common
👉 Idea: Share code with a base class when you have an “is-a” relationship.
public abstract class User
{
public string Name { get; set; }
public string Email { get; set; }
public virtual string GetDisplayName() => $"{Name} ({Email})";
}
public class Admin : User
{
public int AdminLevel { get; set; }
}
public class Customer : User
{
public DateTime Joined { get; set; }
}
✨ Why it’s inheritance: Both Admin and Customer are Users with shared fields.
🔄 Polymorphism — one interface, many behaviors
👉 Idea: Write code that works with multiple implementations.
Use-case: Notifications via Email or SMS.
public interface INotifier { void Notify(string message); }
public class EmailNotifier : INotifier { public void Notify(string message) => Console.WriteLine($"Email: {message}"); }
public class SmsNotifier : INotifier { public void Notify(string message) => Console.WriteLine($"SMS: {message}"); }
public class OrderProcessor
{
private readonly INotifier _notifier;
public OrderProcessor(INotifier notifier) => _notifier = notifier;
public void CompleteOrder() => _notifier.Notify("Order completed");
}
✨ Why it’s polymorphism: OrderProcessor just calls Notify. The actual behavior depends on which notifier is passed in.
🏗️ 2. SOLID Principles
✅ S — Single Responsibility Principle (SRP)
Rule: One class = one reason to change.
public class ProductRepository { /* DB only */ }
public class ProductService { /* Business logic only */ }
👉 Repo handles persistence. Service handles business rules.
✅ O — Open/Closed Principle (OCP)
Rule: Add new features without modifying existing code.
public interface IShippingCalculator { decimal Calculate(Order o); }
public class StandardShipping : IShippingCalculator { public decimal Calculate(Order o) => 5m; }
public class ExpressShipping : IShippingCalculator { public decimal Calculate(Order o) => 20m; }
👉 Want overnight shipping? Just add a new class, no changes elsewhere.
✅ L — Liskov Substitution Principle (LSP)
Rule: Subtypes must be safe replacements for base types.
🚫 Bad: Square inherits Rectangle but breaks assumptions.
👉 Better: Treat them as separate IShape implementations.
✅ I — Interface Segregation Principle (ISP)
Rule: Smaller, focused interfaces are better.
public interface IPrinter { void Print(Document d); }
public interface IScanner { Document Scan(); }
👉 A SimplePrinter doesn’t need to fake scanning capabilities.
✅ D — Dependency Inversion Principle (DIP)
Rule: Depend on abstractions, not concrete classes.
public interface ILogger { void Log(string message); }
public class ConsoleLogger : ILogger { public void Log(string message) => Console.WriteLine(message); }
public class UserController
{
private readonly ILogger _logger;
public UserController(ILogger logger) => _logger = logger;
public void Register(string user) => _logger.Log($"Registered {user}");
}
👉 Switch ConsoleLogger with a FileLogger or DatabaseLogger without touching UserController.
🛒 3. Mini E-commerce Scenario
- Encapsulation:
Orderkeeps items safe. - Abstraction/DIP:
IPaymentProcessorfor PayPal, Stripe, etc. - OCP: Add new shipping calculators without touching checkout.
- SRP: Separate repositories and services.
- ISP: Split
INotifierintoIEmailNotifierandISmsNotifier.
💡 Each class has a single responsibility, is easy to extend, and is testable in isolation.
Comments
Post a Comment