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

🏗️ Deep Dive: Understanding Every Concept in Microsoft Entra API Onboarding for .NET Developers

When working with Microsoft Entra (formerly Azure Active Directory), you’ll hear terms like App Registration, Tenant, Client ID, Audience, Scopes, Roles, Tokens, OBO flow , and more. If you’re new, it can feel overwhelming. This guide breaks down every key term and concept , with definitions, examples, and how they connect when you onboard and consume a new API. 🔹 1. Tenant Definition : A tenant in Entra ID is your organization’s dedicated, isolated instance of Microsoft Entra. Think of it like : Your company’s identity directory. Example : contoso.onmicrosoft.com is a tenant for Contoso Ltd. 🔹 2. App Registration Definition : The process of registering an application in Entra to give it an identity and permission to use Microsoft identity platform. Why needed : Without registration, Entra doesn’t know about your app. What it creates : Application (Client) ID – unique identifier for your app Directory (Tenant) ID – your organization’s ID Types of apps : Web ...

🗑️ Garbage Collection & Resource Management in .NET (C#) — Beginner Friendly Guide

When you start working with .NET and C#, one of the biggest advantages is that you don’t need to manually manage memory like in C or C++. The Garbage Collector (GC) does most of the work for you. But here’s the catch — not everything is managed automatically. Some resources like files, database connections, sockets, and native memory still need special handling. This blog will help you understand: ✔ How the GC works ✔ What are managed vs unmanaged resources ✔ The difference between Dispose , Finalize , and using ✔ The Dispose pattern with examples ✔ Best practices every C# developer should know 1) How Garbage Collection Works in .NET Managed resources → Normal .NET objects (string, List, etc.). GC frees them automatically. Unmanaged resources → External resources like file handles, database connections, sockets, native memory. GC cannot clean them up — you must do it. 👉 GC uses a Generational Model for performance: Gen 0 : Short-lived objects (local variables, t...

☁️ 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...