No history yet

Object-Oriented Programming

The Four Pillars of OOP

Object-oriented programming isn't just a way of writing code; it's a way of thinking about problems. By modeling our software after real-world objects, we can create systems that are more intuitive, flexible, and easier to maintain. This approach stands on four main pillars: abstraction, encapsulation, inheritance, and polymorphism. Let's look at how C# implements each one.

The four principles of object-oriented programming are encapsulation, abstraction, inheritance, and polymorphism.

Abstraction: Focus on the Essentials

Abstraction means hiding complex reality while exposing only the essential parts. Think about driving a car. You don't need to know how the engine's combustion cycle works to press the accelerator and go. You just interact with a simple interface: the pedals and steering wheel.

In C#, abstraction is achieved primarily through abstract classes and interfaces. They define a blueprint of what an object can do without specifying how it does it.

An abstract class is a special kind of class that cannot be instantiated on its own. It's meant to be inherited by other classes. It can define abstract methods, which are methods without a body that must be implemented by any non-abstract child class.

// Cannot create an instance of Vehicle
abstract class Vehicle
{
    // Concrete method with implementation
    public void TurnOn()
    {
        Console.WriteLine("Vehicle is on.");
    }

    // Abstract method - no implementation here!
    public abstract void Move();
}

class Car : Vehicle
{
    // Provide the required implementation
    public override void Move()
    {
        Console.WriteLine("The car drives forward.");
    }
}

Interfaces take abstraction a step further. An interface is a contract that defines a set of method signatures. Any class that implements the interface must provide an implementation for all its methods. A class can inherit from only one base class, but it can implement multiple interfaces.

interface IControl
{
    void Steer(string direction);
    void Brake();
}

class Bicycle : IControl
{
    public void Steer(string direction)
    {
        Console.WriteLine($"Turning handlebars {direction}.");
    }

    public void Brake()
    {
        Console.WriteLine("Squeezing hand brakes.");
    }
}

Encapsulation: Protecting Your Data

Encapsulation is the bundling of data and the methods that operate on that data into a single unit, or class. A key part of this is data hiding: restricting direct access to an object's internal state. Instead of letting other parts of the code reach in and change an object's data at will, we provide public methods to do so in a controlled way.

This is done in C# using access modifiers (private, public, etc.) and properties. By marking fields as private, we prevent external access. We then expose them through public properties that can contain logic, like validation.

public class BankAccount
{
    // This field is hidden from the outside world.
    private decimal _balance;

    // A public property to control access to the balance.
    public decimal Balance
    { 
        get { return _balance; } 
        private set // The setter is private!
        {
            if (value < 0)
            {
                throw new ArgumentException("Balance cannot be negative.");
            }
            _balance = value;
        }
    }

    public BankAccount(decimal initialBalance)
    {
        Balance = initialBalance; // Uses the property's setter logic
    }

    public void Deposit(decimal amount)
    {
        if (amount > 0)
        {
            Balance += amount;
        }
    }
}

Encapsulation protects your object from having its data put into an invalid or inconsistent state. It makes your code more predictable and easier to debug.

Inheritance: Building on What Exists

Inheritance allows a new class to absorb the properties and methods of an existing class. The new class is called the derived (or child) class, and the existing one is the base (or parent) class. This promotes code reuse and creates a logical hierarchy.

A Car is a type of Vehicle. An Employee is a type of Person. This "is-a" relationship is a classic sign that inheritance might be a good fit.

Lesson image

In C#, you specify inheritance with a colon. The derived class gets all the public and protected members of the base class. It can use them as its own, and it can also add new members or modify the behavior of inherited ones.

// Base class
public class Animal
{
    public int Age { get; set; }

    public void Eat()
    {
        Console.WriteLine("This animal is eating.");
    }
}

// Derived class
public class Dog : Animal
{
    public string Breed { get; set; }

    // A method specific to Dog
    public void Bark()
    {
        Console.WriteLine("Woof!");
    }
}

// Usage:
Dog myDog = new Dog();
myDog.Age = 5;      // Inherited from Animal
myDog.Eat();      // Inherited from Animal
myDog.Breed = "Golden Retriever"; // Specific to Dog
myDog.Bark();     // Specific to Dog

Polymorphism: One Interface, Many Forms

Polymorphism, which means "many forms," allows you to treat objects of different classes in the same way. It's the ability for a single variable, method, or object to take on many different forms.

The most common way to achieve polymorphism in C# is through method overriding. This allows a derived class to provide its own specific implementation of a method that is already defined in its base class. The base class method must be marked with the virtual keyword, and the derived class method uses the override keyword.

public class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing a generic shape.");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle.");
    }
}

public class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square.");
    }
}

// --- Polymorphic behavior in action ---

List<Shape> shapes = new List<Shape>();
shapes.Add(new Shape());
shapes.Add(new Circle());
shapes.Add(new Square());

foreach (Shape s in shapes)
{
    s.Draw(); // The correct Draw() method is called!
}

Because Circle and Square are both types of Shape, we can put them in the same list. When we call Draw() on each element, the program knows which version to run at runtime. This makes the code incredibly flexible.

These four pillars work together to help you build complex systems from simple, reliable components. Understanding them is key to writing effective, object-oriented C# code.