Easy Learn C#

Object-Oriented Programming Concepts in C#

Introduction to Object-Oriented Programming (OOP)

Object-Oriented Programming is a programming paradigm based on the concept of "objects" that contain data and code. C# is a fully object-oriented programming language designed around the OOP principles.

OOP helps developers to:

  • Organize and structure code in a logical way
  • Reuse code through inheritance
  • Build modular, maintainable applications
  • Model real-world entities as software objects
  • Create cleaner, more readable code

The Four Pillars of OOP

Object-Oriented Programming is built on four fundamental principles, often referred to as the "four pillars of OOP":

1. Encapsulation

Encapsulation is the bundling of data (attributes) and methods (behaviors) that operate on the data into a single unit (class), and restricting access to some of the object's components.


// Example of encapsulation in C#
public class BankAccount
{
    // Private fields - hidden from outside access
    private double balance;
    private string accountNumber;
    
    // Public property - controlled access to balance
    public double Balance
    {
        get { return balance; }
        private set { balance = value; } // Can only be set within the class
    }
    
    // Public methods to interact with the private data
    public void Deposit(double amount)
    {
        if (amount > 0)
        {
            balance += amount;
        }
    }
    
    public bool Withdraw(double amount)
    {
        if (amount > 0 && balance >= amount)
        {
            balance -= amount;
            return true;
        }
        return false;
    }
}
                            

Benefits of encapsulation:

  • Data hiding - Private fields can't be accessed directly from outside the class
  • Controlled access - Public methods provide controlled ways to modify internal state
  • Validation - Can validate data before allowing changes (e.g., deposit amount must be positive)
  • Flexibility - Internal implementation can change without affecting code that uses the class

2. Inheritance

Inheritance allows a class (derived class) to inherit attributes and methods from another class (base class). This promotes code reuse and establishes a relationship between classes.


// Base class
public class Vehicle
{
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    
    public void StartEngine()
    {
        Console.WriteLine("Engine started!");
    }
    
    public virtual void DisplayInfo()
    {
        Console.WriteLine($"{Year} {Make} {Model}");
    }
}

// Derived class inheriting from Vehicle
public class Car : Vehicle
{
    public int NumberOfDoors { get; set; }
    public bool IsConvertible { get; set; }
    
    // Override base class method
    public override void DisplayInfo()
    {
        // Call base class implementation
        base.DisplayInfo();
        
        // Add Car-specific information
        Console.WriteLine($"Doors: {NumberOfDoors}, Convertible: {IsConvertible}");
    }
    
    // Car-specific method
    public void OpenTrunk()
    {
        Console.WriteLine("Trunk opened!");
    }
}
                            

Key concepts in inheritance:

  • Base and derived classes - "Car" is derived from "Vehicle"
  • Code reuse - Car inherits all Vehicle properties and methods
  • Method overriding - Derived class can provide a new implementation of a base class method
  • Extension - Derived class can add new functionality (OpenTrunk method)
  • The "is-a" relationship - A Car "is a" Vehicle

3. Polymorphism

Polymorphism means "many forms" and allows objects to be treated as instances of their parent class rather than their actual class. This enables you to process objects differently depending on their data type or class.


// Using the Vehicle and Car classes from above
public class Garage
{
    public void ServiceVehicle(Vehicle vehicle)
    {
        Console.WriteLine($"Servicing a {vehicle.Make} {vehicle.Model}");
        
        // vehicle.DisplayInfo() will call the appropriate version depending on the actual type
        vehicle.DisplayInfo();
        
        // Check what type of vehicle we're dealing with
        if (vehicle is Car car)
        {
            // Can access Car-specific properties and methods
            Console.WriteLine($"This car has {car.NumberOfDoors} doors");
            car.OpenTrunk();
        }
    }
}

// Usage example
static void Main()
{
    Garage garage = new Garage();
    
    Vehicle genericVehicle = new Vehicle 
    { 
        Make = "Generic", 
        Model = "Vehicle", 
        Year = 2020 
    };
    
    Car myCar = new Car 
    { 
        Make = "Toyota", 
        Model = "Corolla", 
        Year = 2022, 
        NumberOfDoors = 4, 
        IsConvertible = false 
    };
    
    // Both can be passed to ServiceVehicle method
    garage.ServiceVehicle(genericVehicle);  // Will use Vehicle.DisplayInfo()
    garage.ServiceVehicle(myCar);           // Will use Car.DisplayInfo()
}
                            

Types of polymorphism in C#:

  • Runtime polymorphism (Method Overriding) - Using virtual/override methods
  • Compile-time polymorphism (Method Overloading) - Multiple methods with the same name but different parameters
  • Interface implementation - Multiple classes implementing the same interface
  • Abstract classes and methods - Base classes that define behavior that derived classes must implement

4. Abstraction

Abstraction means hiding complex implementation details and showing only the necessary features of an object. It helps manage complexity by hiding unnecessary details from the user.


// Abstract class
public abstract class Shape
{
    // Abstract method - must be implemented by any non-abstract derived class
    public abstract double CalculateArea();
    
    // Concrete method - available to all derived classes
    public void Display()
    {
        Console.WriteLine($"This shape has an area of {CalculateArea()}");
    }
}

// Concrete derived classes implementing the abstract method
public class Circle : Shape
{
    public double Radius { get; set; }
    
    public Circle(double radius)
    {
        Radius = radius;
    }
    
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }
    
    public override double CalculateArea()
    {
        return Width * Height;
    }
}

// Usage
static void Main()
{
    Shape circle = new Circle(5);
    Shape rectangle = new Rectangle(4, 6);
    
    circle.Display();     // "This shape has an area of 78.54..."
    rectangle.Display();  // "This shape has an area of 24"
}
                            

Abstraction mechanisms in C#:

  • Abstract classes - Cannot be instantiated directly, may contain abstract and concrete methods
  • Abstract methods - Methods without implementation that must be overridden
  • Interfaces - Define a contract that implementing classes must follow
  • Public vs. private members - Expose only what's necessary

Other Important OOP Concepts

Interfaces

Interfaces define a contract that classes must implement. Unlike abstract classes, interfaces cannot provide implementation details.


// Interface definition
public interface IPayable
{
    decimal CalculatePayment();
    void ProcessPayment();
}

// Classes implementing the interface
public class Employee : IPayable
{
    public string Name { get; set; }
    public decimal HourlyRate { get; set; }
    public int HoursWorked { get; set; }
    
    public decimal CalculatePayment()
    {
        return HourlyRate * HoursWorked;
    }
    
    public void ProcessPayment()
    {
        decimal amount = CalculatePayment();
        Console.WriteLine($"Paid ${amount} to employee {Name}");
    }
}

public class Invoice : IPayable
{
    public string Number { get; set; }
    public decimal Amount { get; set; }
    
    public decimal CalculatePayment()
    {
        return Amount;
    }
    
    public void ProcessPayment()
    {
        Console.WriteLine($"Paid invoice #{Number} for ${Amount}");
    }
}

// Usage - process different types of payments
public void ProcessPayables(List payables)
{
    foreach(var payable in payables)
    {
        payable.ProcessPayment();
    }
}
                            

Composition

Composition is a design principle where a class contains an instance of another class. It represents a "has-a" relationship, as opposed to inheritance's "is-a" relationship.


// Component class
public class Engine
{
    public int Horsepower { get; set; }
    public string Type { get; set; }
    
    public void Start()
    {
        Console.WriteLine($"Starting {Type} engine with {Horsepower} HP");
    }
    
    public void Stop()
    {
        Console.WriteLine("Engine stopped");
    }
}

// Composite class using composition
public class Car
{
    // Car "has-a" Engine - composition relationship
    private Engine engine;
    
    public string Make { get; set; }
    public string Model { get; set; }
    
    public Car(string make, string model, int horsepower, string engineType)
    {
        Make = make;
        Model = model;
        
        // Create the component object
        engine = new Engine
        {
            Horsepower = horsepower,
            Type = engineType
        };
    }
    
    public void StartCar()
    {
        Console.WriteLine($"Starting {Make} {Model}");
        engine.Start();  // Delegate to the component
    }
    
    public void StopCar()
    {
        Console.WriteLine($"Stopping {Make} {Model}");
        engine.Stop();   // Delegate to the component
    }
}
                            

Benefits of composition over inheritance:

  • Flexibility - Can change components at runtime
  • Looser coupling - Components can be developed independently
  • Avoid complex inheritance hierarchies - "Favor composition over inheritance" is a design principle
  • Reuse without forcing "is-a" relationships - Not everything is a specialized form of something else

OOP Best Practices in C#

Key Guidelines for OOP in C#

  • Single Responsibility Principle - A class should have only one reason to change
  • Open/Closed Principle - Classes should be open for extension but closed for modification
  • Liskov Substitution Principle - Derived classes must be substitutable for their base classes
  • Interface Segregation Principle - Many specific interfaces are better than one general-purpose interface
  • Dependency Inversion Principle - Depend on abstractions, not concrete implementations
  • Encapsulate What Varies - Identify aspects of your application that vary and separate them from what stays the same
  • Favor Composition Over Inheritance - Build complex functionality by combining simple objects rather than through inheritance

Common OOP Mistakes to Avoid

  • Deep inheritance hierarchies - More than 2-3 levels can become difficult to understand and maintain
  • Tight coupling - Classes that are too dependent on each other
  • God classes - Classes that try to do too much
  • Leaky abstractions - When implementation details "leak" through abstraction layers
  • Inappropriate inheritance - Using inheritance when composition would be more appropriate
  • Overuse of static methods/classes - Leads to procedural rather than object-oriented code
  • Not using interfaces - Missing opportunities for abstraction and flexibility