
Object-Oriented Programming in C#: A Practical Guide
Learn classes, objects, encapsulation, abstraction, inheritance, and polymorphism in C# with practical examples, flashcards, and interview quizzes.
Object-oriented programming (OOP) organizes software around objects: values that combine state with the operations that work on that state. In C#, classes and structs define types, and objects are instances of those types.
OOP is more than creating a class for every noun. A good object model protects its invariants, exposes meaningful behavior, and gives callers a clear contract.
Best practice:Green marks a useful practice; red marks a common design pitfall; orange flags a point to consider.Classes, objects, and members
A class defines a reference type: its members, behavior, and rules. An object is a runtime instance of that class. Properties represent accessible state, while methods express behavior.
public sealed class BankAccount
{
public string Owner { get; }
public decimal Balance { get; private set; }
public BankAccount(string owner, decimal openingBalance)
{
if (openingBalance < 0)
{
throw new ArgumentOutOfRangeException(nameof(openingBalance));
}
Owner = owner;
Balance = openingBalance;
}
public void Deposit(decimal amount)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(amount));
}
Balance += amount;
}
}
var account = new BankAccount("Avery", 100m);
account.Deposit(25m);The class describes how accounts work; account refers to one particular account object. The constructor establishes valid starting state, and callers use Deposit rather than setting the balance directly.
The four pillars of OOP
| Pillar | What it does | C# tools you may use |
|---|---|---|
| Encapsulation | Protects state and invariants behind a clear API. | Access modifiers, properties, methods |
| Abstraction | Exposes what callers need while hiding implementation details. | Interfaces, abstract classes |
| Inheritance | Reuses or specializes a compatible base-type contract. | Base classes, virtual, override |
| Polymorphism | Lets callers use a common contract with different implementations. | Interfaces, virtual dispatch, overloads |
1. Encapsulation
Encapsulation combines state and behavior and controls how callers can interact with them. It is not merely making fields private: the public API should prevent invalid state and offer operations that make sense for the type.
In BankAccount, the balance has a private setter. Deposits must be positive, and all changes go through a method that enforces that rule.
Quick check: Why does BankAccount expose Deposit() instead of a public Balance setter?
2. Abstraction
Abstraction lets callers depend on the essential capability while implementation details remain behind a contract. For example, an order service may need to send a notification without knowing whether delivery uses email or a test fake.
public interface INotifier
{
Task SendAsync(string recipient, string message);
}
public sealed class OrderService
{
private readonly INotifier _notifier;
public OrderService(INotifier notifier)
{
_notifier = notifier;
}
public Task NotifyOrderShippedAsync(string email) =>
_notifier.SendAsync(email, "Your order has shipped.");
}OrderService knows the operation it needs. A separate class can provide the delivery details.
3. Inheritance
Inheritance lets a derived class inherit and specialize a base class. Use it when the derived type truly is a substitutable form of the base type, and callers can rely on the base contract for either.
public abstract class Shape
{
public abstract double GetArea();
}
public sealed class Circle : Shape
{
public double Radius { get; }
public Circle(double radius)
{
if (radius < 0)
{
throw new ArgumentOutOfRangeException(nameof(radius));
}
Radius = radius;
}
public override double GetArea() => Math.PI * Radius * Radius;
}
public sealed class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height)
{
if (width < 0 || height < 0)
{
throw new ArgumentOutOfRangeException(nameof(width), "Dimensions cannot be negative.");
}
Width = width;
Height = height;
}
public override double GetArea() => Width * Height;
}A method that accepts Shape can work with either shape because both provide the promised GetArea() behavior.
Quick check: When is inheritance a good fit?
4. Polymorphism
Polymorphism means using a common contract while different types provide different behavior. With runtime polymorphism, an overridden method is selected according to the object's runtime type.
public static double TotalArea(IEnumerable<Shape> shapes)
{
return shapes.Sum(shape => shape.GetArea());
}
var shapes = new Shape[]
{
new Circle(2),
new Rectangle(3, 4)
};
double total = TotalArea(shapes);TotalArea does not need to check whether each item is a circle or rectangle. Each object's implementation supplies its own area.
Overriding provides a subtype-specific implementation for an inherited virtual or abstract member. Overloading uses the same method name with different parameter lists; overload selection is based on the arguments' compile-time types.
Quick check: Which statement describes method overriding?
Interfaces and abstract classes
Both interfaces and abstract classes can express contracts, but they serve different modeling needs.
| Choose an interface when... | Choose an abstract class when... |
|---|---|
| A type needs to promise a capability, possibly alongside other base classes. | Related derived types share a base identity or common implementation. |
| The contract should not prescribe shared instance state. | Shared state, constructors, or protected helpers are useful. |
| Unrelated types need to support the same operation. | You want a base type to provide some behavior and require overrides for the rest. |
A class can implement multiple interfaces but can inherit from only one class. Interfaces can include default implementations in modern C#, but an interface is still primarily a contract for capabilities.
Quick check: Which type can a C# class directly inherit from multiple times?
Composition: another way to reuse behavior
Inheritance is not the only way to share behavior. Composition builds an object from collaborators that it holds and delegates work to. This is useful when capabilities vary independently or a type should not claim to be a subtype of another.
public interface IClock
{
DateTimeOffset GetUtcNow();
}
public sealed class ExpirationService
{
private readonly IClock _clock;
public ExpirationService(IClock clock)
{
_clock = clock;
}
public bool IsExpired(DateTimeOffset expiresAt) =>
_clock.GetUtcNow() >= expiresAt;
}Supplying a fake clock in a test lets you check expiration behavior without waiting for real time to pass.
Quick revision flashcards
Try to answer each prompt before flipping the card. Mark topics you know; progress is stored separately for this OOP deck in this browser.
OOP interview-ready summary
Object-oriented programming models software with objects that combine state and behavior. Encapsulation protects an object's rules, abstraction exposes a useful contract, inheritance specializes a compatible base type, and polymorphism lets code use a common contract with different implementations. Use composition as an alternative when a type needs collaborators rather than a base-class relationship.