Skip to main content

🚀 Mastering OOP & SOLID in C#


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

  1. 🎯 OOP fundamentals
    • 🔒 Encapsulation
    • 🎭 Abstraction
    • 🧬 Inheritance
    • 🔄 Polymorphism
  2. 🏗️ SOLID principles
    • S — Single Responsibility Principle
    • O — Open/Closed Principle
    • L — Liskov Substitution Principle
    • I — Interface Segregation Principle
    • D — Dependency Inversion Principle
  3. 🛒 Mini e-commerce scenario (tying everything together)
  4. 💡 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: Order keeps items safe.
  • Abstraction/DIP: IPaymentProcessor for PayPal, Stripe, etc.
  • OCP: Add new shipping calculators without touching checkout.
  • SRP: Separate repositories and services.
  • ISP: Split INotifier into IEmailNotifier and ISmsNotifier.

💡 Each class has a single responsibility, is easy to extend, and is testable in isolation.

Comments

Popular posts from this blog

☁️ Azure Key vault Short Notes

🟢 What is Azure Key Vault? A cloud service for securely storing and accessing secrets, keys, and certificates . Removes the need to keep secrets (like connection strings, passwords, API keys) inside code or config files. Provides centralized secret management, encryption, and access control . 👉 Think of it like a secure password manager but for your applications. 🟢 Key Features Secrets → store text values (e.g., DB connection string, API key). Keys → store cryptographic keys (RSA, EC) for encryption, signing. Certificates → store/manage SSL/TLS certificates. Access Control → Access Policies (older model). Azure RBAC (modern, preferred). Integration → works with App Service, Functions, AKS, VMs, SQL DB, etc. Logging → audit who accessed secrets via Azure Monitor / Diagnostic Logs. 🟢 Why Use Key Vault? Security → secrets are encrypted with HSM (Hardware Security Modules). Compliance → meet industry standards (PCI-DSS, ISO, GDPR). Automation → aut...