Object-Oriented Programming in C#: A Practical Guide
← All interview guides

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.

By DotNetInterviews
Enjoyed this article?

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

PillarWhat it doesC# tools you may use
EncapsulationProtects state and invariants behind a clear API.Access modifiers, properties, methods
AbstractionExposes what callers need while hiding implementation details.Interfaces, abstract classes
InheritanceReuses or specializes a compatible base-type contract.Base classes, virtual, override
PolymorphismLets 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.

Best practice:Protect invariants by validating changes at the object's public boundary.Pitfall:A public setter for every property can let callers put an object into a state its methods cannot safely handle.

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.

Caution:C# classes have single class inheritance: a class can have one direct base class, but it can implement multiple interfaces.Pitfall:Avoid inheriting just to reuse a few lines. A fragile or forced base-class relationship can couple unrelated types.

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.

Loading comments...