Inheritance and Abstract Classes

Inheritance is core to understanding object hierarchy.

First thing's first: ALL objects in C# inherit from object.

A class that inherits from another class is called a derived class.

Inheriting from Other Classes

How to do it

In C#, you can inherit from a class using the : syntax. This allows the derived class to use the members of the base class.

public class BaseEmployee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual string GetEmployeeDetails()
    {
        return $"{FirstName} {LastName}";
    }
}

public class HourlyEmployee : BaseEmployee
{
    public decimal HourlyRate { get; set; }

    public decimal CalculateWeeklyPay(int hoursWorked)
    {
        return HourlyRate * hoursWorked;
    }
}

Why to do it

  • Code Reusability: Inheritance allows you to reuse code from base classes, reducing redundancy.
  • Organizing Code: Helps structure code hierarchically, improving readability and maintenance.
  • Polymorphism: Enables derived classes to be treated as instances of the base class, supporting flexible designs.

Sealed vs. Virtual

Sealed

A sealed class cannot be inherited. Use this to prevent further derivation.

public sealed class FinalEmployee
{
    // Implementation
}

Fun fact: The most common sealed class in C# is string. You can't inherit from string (and thank God for that, Spencer has no idea how devs would abuse that!)

You can also seal methods and properties:

public class BaseEmployee
{
    public sealed override string ToString()    //sealed prevents overriding in derived classes
    {
        return "Base employee details";
    }
}

Virtual

A virtual method/property in the base class can be overridden in derived classes to provide specific implementations.

Example:

public class BaseEmployee
{
    public virtual string GetEmployeeDetails()
    {
        return "Base employee details";
    }
}

public class DerivedEmployee : BaseEmployee
{
    public override string GetEmployeeDetails()
    {
        return "Derived employee details";
    }
}

Abstract Classes

Abstract classes serve as a base class that other classes can derive from. They may contain abstract methods that derived classes must implement, providing shared behaviors and properties.

Abstract classes cannot be instantiated directly.

public abstract class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public abstract decimal CalculatePay(); // Abstract method

    public virtual string GetEmployeeDetails() // Virtual method
    {
        return $"{FirstName} {LastName}";
    }

    public sealed override string ToString() // Sealed method
    {
        return GetEmployeeDetails();
    }
}

Examples of Derived Classes

public class HourlyEmployee : Employee
{
    public decimal HourlyRate { get; set; }
    public int HoursWorked { get; set; }

    public override decimal CalculatePay()
    {
        return HourlyRate * HoursWorked;
    }
}

public class SalariedEmployee : Employee
{
    public decimal Salary { get; set; }

    public override decimal CalculatePay()
    {
        return Salary / 52; // Weekly pay calculation
    }
}

Move salary to the SalariedEmployee class and implement different pay calculations in each derived class.

Demonstrate Methods

  • Abstract Method: CalculatePay() must be implemented in derived classes.
  • Virtual Method: GetEmployeeDetails() can be overridden in derived classes.
  • Sealed Method: ToString() cannot be overridden in derived classes.

U-Turn back to access modifiers

Abstract classes are a great place to discuss access modifiers a bit more, as good software design includes thinking about properties and methods that should be visible to other consumers. This is the object-oriented programming principle of encapsulation - exposing only what is necessary for consumers.

The best way to think about this is thinking about the properties and behaviors that are important to the behavior to your program internally as opposed to an external consumer. You should only expose what you need to expose, and nothing more.

Here's an example using our derived Employee classes:

public abstract class Employee
{
    protected Guid Id { get; set; }  //accessible to derived classes only!
    public string FirstName { get; set; }  //accessible to anyone who has access to this object
    public string LastName { get; set; }

    public abstract decimal CalculatePay();
    private void GenerateEmployeeId();  //not accessible to derived classes
}

Another access modifier I see a lot is internal which simply keeps other assemblies from accessing the type or member. E.g. the functionality is "internal" to the assembly only.

public, private, protected, and internal are my most common access modifiers. You can find a complete list here.

🌶️🌶️🌶️ Accessibility is sometimes tricky to get right as you get into the weeds. I often start by making things private (e.g. as closed off as possible) and refactor to other access modifiers as needed.

🌶️🌶️🌶️ Spencer's Opinions about Inheritance 🌶️🌶️🌶️

Beware, 🌶️ takes inbound.

Inheritance is often overused, especially when you're new to C# and object-oriented programming.

Spencer tends to put common methods and behaviors in base classes, but usually I don't go further than one level of inheritance.

Great thing about C# is that there’s lots of flexibility. Bad thing about C#... lots of flexibility. Applies to most programming languages. C# offers flexibility with inheritance, which can lead to complexity and misuse. This is a common challenge in many programming languages.

TBH, the Salary and Hourly employee derived classes are meant to demonstrate the nuance of the language features. They do not model how I approach software design. For that particular instance, I tend to model things like that with properties as discriminators. E.g. I'd have something like a PaymentType enum and add a PaymentType property to the Employee class and preserve a flat inheritance hierarchy.

public enum EmployeePaymentType
{
    Hourly,
    Salary
}

public class Employee
{
    public EmployeePaymentType PaymentType { get; set; }
}

As far as calculating salaries is concerned, I'd much sooner do that in a service class:

public class EmployeePayCalculator
{
    public decimal CalculatePay(Employee employee)
    {
        if (employee.PaymentType == EmployeePaymentType.Hourly)
        {
            //...do this
        }

        //...rest of your logic here
    }
}

Also, I almost never inherit when using classes with common properties, preferring to simply copy/paste similar properties around.

DRY vs MOIST

  • DRY (Don't Repeat Yourself): A principle aimed at reducing code duplication. It's a good rule of thumb, but developers tend to be really dogmatic about this, to the point of absurdity.
  • MOIST (Moderate Overlap Is Sometimes Tolerable): Reducing duplication is important, but trying to achieve 0% duplication can lead to overly complex code and unnecessary abstraction.

🌶️🌶️🌶️ Spencer's rule of thumb: I'll often copy/paste similar code up to 2 times. Afterwards, I'll start looking to refactor to reduce duplication. Total elimination of duplication is usually a waste of time.