C#
OOP in C#: The Examples That Finally Made It Click
Dog, Animal, and Bark() never stuck. Encapsulation, abstraction, inheritance, and polymorphism finally clicked once I saw them in a bank account, a notification service, and a payment processor instead.
Every OOP tutorial I started with used the same textbook examples: a Dog that inherits from Animal and calls Bark(), a Car that inherits from Vehicle and calls Drive(). None of that stuck, because I don't build dog kennels or car dealerships. I build payment flows and API integrations. The four pillars only actually clicked once I saw them in code that looked like what I actually write.
Encapsulation: BankAccount
The version that didn't click was a public field anyone could set directly:
public class BankAccount
{
public decimal Balance;
}
Nothing stops any code in the solution from setting Balance to a negative number. What made encapsulation click wasn't "hide the field," it was realizing the object should be the only thing allowed to change its own state, and only through rules it enforces itself:
public class BankAccount
{
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Deposit amount must be positive.");
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount > Balance)
throw new InvalidOperationException("Insufficient funds.");
Balance -= amount;
}
}
Abstraction: a notification service
Abstraction clicked once I stopped thinking of it as "make an interface" and started thinking of it as hiding the mess a caller shouldn't have to care about:
public interface INotificationService
{
Task SendSmsAsync(string phoneNumber, string message);
}
public class TwilioNotificationService : INotificationService
{
private readonly HttpClient _httpClient;
public async Task SendSmsAsync(string phoneNumber, string message)
{
// HTTP payload formatting, auth header signing, error handling
}
}
Whatever calls SendSmsAsync doesn't need to know about HTTP status codes or signing headers. That gap between the simple contract and the messy implementation behind it is what abstraction actually buys you.
Inheritance: a payment processor template
Deep inheritance chains never made sense to me until I saw inheritance used for a shared workflow instead of a shared taxonomy:
public abstract class PaymentProcessor
{
public async Task ProcessTransactionAsync(decimal amount)
{
ValidateAmount(amount);
await ExecutePaymentAsync(amount);
LogReceipt();
}
private void ValidateAmount(decimal amount) { /* shared logic */ }
private void LogReceipt() { /* shared logic */ }
protected abstract Task ExecutePaymentAsync(decimal amount);
}
public class StripePaymentProcessor : PaymentProcessor
{
protected override async Task ExecutePaymentAsync(decimal amount)
{
// Stripe API integration logic
}
}
The base class owns the process. Each subclass only overrides the one step that actually differs. That's a much smaller commitment than the Dog -> Mammal -> Animal chains I kept seeing in tutorials.
Polymorphism: swapping payment providers
The example that made polymorphism click was replacing a growing if/else chain with a single abstraction:
public class CheckoutService
{
private readonly PaymentProcessor _paymentProcessor;
public CheckoutService(PaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}
public async Task CompleteOrderAsync(Order order)
{
await _paymentProcessor.ProcessTransactionAsync(order.TotalAmount);
}
}
CheckoutService doesn't know or care which concrete PaymentProcessor subclass it got, StripePaymentProcessor or anything added later. Adding a new payment method means writing a new subclass, not editing a switch statement buried somewhere else in the codebase.
What actually changed
What made these four stick is that they're all things I'd actually write. The concept finally had somewhere real to attach, instead of sitting on top of a Dog class I was never going to use again.