Leveraging Abstract Classes for Code Reusability in C#

Why Abstract Classes Improve Code Reuse

Abstract classes let you define common behavior in a base class that multiple derived classes share. They combine the contract-defining power of interfaces with the implementation-sharing capabilities of regular classes.

When building class hierarchies, you often find that related types share some behavior but differ in specific details. Abstract classes capture this shared logic while forcing derived classes to implement type-specific behavior through abstract methods.

You'll learn how to create abstract classes, use abstract and virtual methods together, and design effective inheritance hierarchies that reduce code duplication.

Abstract Class Basics

You mark a class as abstract using the abstract keyword. Abstract classes cannot be instantiated directly. You must create a derived class that implements all abstract members.

Simple abstract class
public abstract class Shape
{
    public string Name { get; set; }

    // Abstract method - no implementation
    public abstract double GetArea();

    // Concrete method - shared by all shapes
    public void Display()
    {
        Console.WriteLine($"{Name}: Area = {GetArea():F2}");
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(double radius)
    {
        Radius = radius;
        Name = "Circle";
    }

    public override double GetArea()
    {
        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;
        Name = "Rectangle";
    }

    public override double GetArea()
    {
        return Width * Height;
    }
}

// Usage
Shape circle = new Circle(5);
circle.Display();  // Circle: Area = 78.54

Shape rect = new Rectangle(4, 6);
rect.Display();    // Rectangle: Area = 24.00

The abstract GetArea() method defines a contract that all shapes must fulfill. The concrete Display() method provides shared functionality that uses the abstract method.

Constructors and Fields in Abstract Classes

Abstract classes can have constructors, fields, and properties just like regular classes. Derived classes call the base constructor to initialize shared state.

Abstract class with constructor
public abstract class Employee
{
    public string Name { get; set; }
    public int Id { get; set; }
    protected decimal BaseSalary { get; set; }

    protected Employee(string name, int id, decimal baseSalary)
    {
        Name = name;
        Id = id;
        BaseSalary = baseSalary;
    }

    public abstract decimal CalculateSalary();

    public void PrintDetails()
    {
        Console.WriteLine($"ID: {Id}, Name: {Name}");
        Console.WriteLine($"Salary: ${CalculateSalary():N2}");
    }
}

public class FullTimeEmployee : Employee
{
    public FullTimeEmployee(string name, int id, decimal salary)
        : base(name, id, salary)
    {
    }

    public override decimal CalculateSalary()
    {
        return BaseSalary;
    }
}

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

    public ContractEmployee(string name, int id, int hours, decimal rate)
        : base(name, id, 0)
    {
        HoursWorked = hours;
        HourlyRate = rate;
    }

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

// Usage
Employee emp1 = new FullTimeEmployee("Alice", 101, 75000);
emp1.PrintDetails();

Employee emp2 = new ContractEmployee("Bob", 102, 160, 50);
emp2.PrintDetails();

The base constructor initializes common properties for all employees. Each derived class provides its own salary calculation logic while inheriting the shared PrintDetails method.

Combining Virtual and Abstract Methods

Abstract classes can mix abstract methods that must be overridden with virtual methods that can optionally be overridden. This gives derived classes flexibility in customization.

Virtual and abstract methods together
public abstract class Document
{
    public string Title { get; set; }
    public DateTime CreatedDate { get; set; }

    protected Document(string title)
    {
        Title = title;
        CreatedDate = DateTime.Now;
    }

    // Abstract - must be implemented
    public abstract void Save();

    // Virtual - can be overridden but has default
    public virtual string GetSummary()
    {
        return $"{Title} (Created: {CreatedDate:d})";
    }

    // Concrete - inherited as-is
    public void Print()
    {
        Console.WriteLine(GetSummary());
    }
}

public class TextDocument : Document
{
    public string Content { get; set; }

    public TextDocument(string title, string content)
        : base(title)
    {
        Content = content;
    }

    public override void Save()
    {
        File.WriteAllText($"{Title}.txt", Content);
        Console.WriteLine($"Saved {Title}.txt");
    }

    // Uses default GetSummary from base
}

public class PdfDocument : Document
{
    public byte[] Data { get; set; }
    public int PageCount { get; set; }

    public PdfDocument(string title, byte[] data, int pages)
        : base(title)
    {
        Data = data;
        PageCount = pages;
    }

    public override void Save()
    {
        File.WriteAllBytes($"{Title}.pdf", Data);
        Console.WriteLine($"Saved {Title}.pdf");
    }

    // Override virtual method for custom summary
    public override string GetSummary()
    {
        return base.GetSummary() + $" - {PageCount} pages";
    }
}

TextDocument uses the default GetSummary while PdfDocument customizes it. Both must implement Save. This pattern gives you required customization points and optional ones in the same base class.

The Template Method Pattern

Abstract classes enable the template method pattern where the base class defines the algorithm structure and derived classes fill in specific steps.

Template method pattern
public abstract class DataProcessor
{
    // Template method - defines the algorithm
    public void ProcessData(string filePath)
    {
        var data = LoadData(filePath);
        ValidateData(data);
        var processed = Transform(data);
        SaveResults(processed);
    }

    protected virtual string LoadData(string path)
    {
        return File.ReadAllText(path);
    }

    protected abstract void ValidateData(string data);
    protected abstract string Transform(string data);

    protected virtual void SaveResults(string results)
    {
        Console.WriteLine($"Results: {results}");
    }
}

public class CsvProcessor : DataProcessor
{
    protected override void ValidateData(string data)
    {
        if (!data.Contains(","))
        {
            throw new InvalidDataException("Not valid CSV");
        }
    }

    protected override string Transform(string data)
    {
        var lines = data.Split('\n');
        return $"Processed {lines.Length} CSV rows";
    }
}

public class JsonProcessor : DataProcessor
{
    protected override void ValidateData(string data)
    {
        if (!data.TrimStart().StartsWith("{"))
        {
            throw new InvalidDataException("Not valid JSON");
        }
    }

    protected override string Transform(string data)
    {
        return $"Parsed JSON with {data.Length} characters";
    }

    protected override void SaveResults(string results)
    {
        File.WriteAllText("output.json", results);
        Console.WriteLine("Saved to output.json");
    }
}

The ProcessData template method orchestrates the workflow. Derived classes customize specific steps while the overall algorithm stays consistent. This pattern reduces duplication and enforces structure.

Frequently Asked Questions (FAQ)

When should you use an abstract class instead of an interface?

Use abstract classes when you want to share implementation code between related types. Abstract classes can have fields, constructors, and concrete methods alongside abstract ones. Interfaces only define contracts without implementation, though C# 8 added default interface methods. Choose abstract classes for shared behavior in inheritance hierarchies.

Can abstract classes have constructors?

Yes, abstract classes can have constructors even though you cannot instantiate them directly. Derived classes call the base constructor when creating instances. This lets you initialize fields and run setup logic common to all subclasses. Protected constructors are common in abstract classes.

Must derived classes implement all abstract methods?

Yes, any non-abstract class that inherits from an abstract class must provide implementations for all abstract methods. If a derived class doesn't implement them all, it must also be declared abstract. This ensures the contract defined by the abstract class is fulfilled.

Can abstract classes contain non-abstract methods?

Yes, abstract classes typically contain both abstract and concrete methods. The concrete methods provide shared implementation that all derived classes inherit automatically. This combination lets you define required behavior through abstract methods while providing default implementation for common functionality.

Can you use virtual methods in abstract classes?

Yes, abstract classes can declare virtual methods that provide default implementation but allow derived classes to override them optionally. This differs from abstract methods which must be overridden. Virtual methods give derived classes flexibility while abstract methods enforce required customization.

What's the difference between abstract and sealed classes?

Abstract classes cannot be instantiated and must be inherited. Sealed classes cannot be inherited but can be instantiated. They represent opposite approaches: abstract classes exist to be extended, while sealed classes prevent extension. You cannot combine these modifiers on the same class.

Back to Articles