Skip to main content

.NET Abstract Classes

Introduction

Abstract classes are a fundamental concept in object-oriented programming (OOP) that provide a powerful way to define common behavior while enforcing certain implementation requirements on derived classes. In .NET, abstract classes serve as intermediaries between interfaces and concrete classes, allowing you to create partially implemented base classes that can share code while requiring inheriting classes to implement specific methods.

In this tutorial, we'll explore what abstract classes are, why they're useful, and how to implement them effectively in your .NET applications.

What is an Abstract Class?

An abstract class is a special type of class that:

  1. Cannot be instantiated directly
  2. May contain both implemented methods and abstract methods (methods without implementation)
  3. Must be inherited by another class to be useful
  4. Can define common behavior shared by all derived classes

Abstract classes are declared using the abstract keyword in C#:

csharp
public abstract class Shape
{
// Abstract class implementation
}

Abstract Classes vs. Interfaces vs. Concrete Classes

Before diving deeper, let's understand how abstract classes relate to other types:

FeatureAbstract ClassInterfaceConcrete Class
Can be instantiated❌ No❌ No✅ Yes
Can contain implementations✅ Yes⚠️ Only default (C# 8+)✅ Yes
Can contain fields✅ Yes❌ No✅ Yes
Can contain constructors✅ Yes❌ No✅ Yes
Can inherit from other classes✅ Yes (1)❌ No✅ Yes (1)
Can implement interfaces✅ Yes (many)✅ Yes (many)✅ Yes (many)

When to Use Abstract Classes

Abstract classes are particularly useful when:

  1. You want to share code among related classes
  2. You need to enforce a certain structure on inheriting classes
  3. The base class doesn't make sense as a standalone object
  4. You want to provide default implementations while requiring specific methods to be overridden

Creating Abstract Classes in C#

Let's create a simple abstract class example to understand the concept better:

csharp
using System;

// Abstract class declaration
public abstract class Animal
{
// Regular property
public string Name { get; set; }

// Constructor
public Animal(string name)
{
Name = name;
}

// Regular method with implementation
public void Breathe()
{
Console.WriteLine($"{Name} is breathing.");
}

// Abstract method - no implementation
public abstract void MakeSound();

// Virtual method - has implementation but can be overridden
public virtual void Sleep()
{
Console.WriteLine($"{Name} is sleeping.");
}
}

// Concrete class inheriting from abstract class
public class Dog : Animal
{
public Dog(string name) : base(name)
{
}

// Must implement abstract method
public override void MakeSound()
{
Console.WriteLine($"{Name} says: Woof!");
}

// Optional: Override virtual method
public override void Sleep()
{
Console.WriteLine($"{Name} is sleeping and dreaming about bones.");
}
}

// Another concrete class
public class Cat : Animal
{
public Cat(string name) : base(name)
{
}

// Must implement abstract method
public override void MakeSound()
{
Console.WriteLine($"{Name} says: Meow!");
}

// Using the base class implementation of Sleep()
}

Using Abstract Classes

Now let's see how to use the abstract classes we've defined:

csharp
using System;

class Program
{
static void Main(string[] args)
{
// Cannot create an instance of an abstract class
// Animal animal = new Animal("Generic"); // This would cause a compilation error

// Create instances of concrete classes
Dog rex = new Dog("Rex");
Cat whiskers = new Cat("Whiskers");

// Call methods
rex.Breathe(); // From base class
rex.MakeSound(); // Overridden abstract method
rex.Sleep(); // Overridden virtual method

Console.WriteLine();

whiskers.Breathe(); // From base class
whiskers.MakeSound(); // Overridden abstract method
whiskers.Sleep(); // Using base class implementation

// Polymorphism - treat derived classes as their abstract base type
Animal[] animals = { rex, whiskers };
Console.WriteLine("\nPolymorphism in action:");
foreach (Animal animal in animals)
{
Console.WriteLine($"Animal: {animal.Name}");
animal.MakeSound();
}
}
}

Output

Rex is breathing.
Rex says: Woof!
Rex is sleeping and dreaming about bones.

Whiskers is breathing.
Whiskers says: Meow!
Whiskers is sleeping.

Polymorphism in action:
Animal: Rex
Rex says: Woof!
Animal: Whiskers
Whiskers says: Meow!

Abstract Properties

Abstract classes can also contain abstract properties that derived classes must implement:

csharp
public abstract class Vehicle
{
// Abstract property
public abstract int NumberOfWheels { get; }

public void DisplayWheelCount()
{
Console.WriteLine($"This vehicle has {NumberOfWheels} wheels.");
}
}

public class Bicycle : Vehicle
{
// Implementing the abstract property
public override int NumberOfWheels => 2;
}

public class Car : Vehicle
{
// Implementing the abstract property
public override int NumberOfWheels => 4;
}

Using the above classes:

csharp
Bicycle bike = new Bicycle();
Car car = new Car();

bike.DisplayWheelCount(); // Output: This vehicle has 2 wheels.
car.DisplayWheelCount(); // Output: This vehicle has 4 wheels.

Real-World Example: Document Processing System

Let's look at a practical example where abstract classes are useful. Consider a document processing system:

csharp
using System;
using System.Collections.Generic;

// Abstract base class for all document types
public abstract class Document
{
public string Title { get; set; }
public string Author { get; set; }
public DateTime CreatedDate { get; private set; }

// Constructor
public Document(string title, string author)
{
Title = title;
Author = author;
CreatedDate = DateTime.Now;
}

// Common functionality for all documents
public void DisplayInfo()
{
Console.WriteLine($"Title: {Title}");
Console.WriteLine($"Author: {Author}");
Console.WriteLine($"Created: {CreatedDate}");
}

// Abstract methods that all documents must implement
public abstract void Save();
public abstract string GetFileExtension();

// Virtual method with default implementation
public virtual long GetSize()
{
return 0; // Default implementation
}
}

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

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

public override void Save()
{
Console.WriteLine($"Saving text document '{Title}' as a .txt file");
// Implementation to save as text file
}

public override string GetFileExtension()
{
return ".txt";
}

public override long GetSize()
{
return Content.Length;
}
}

// Concrete SpreadsheetDocument class
public class SpreadsheetDocument : Document
{
public Dictionary<string, List<object>> Sheets { get; set; } = new Dictionary<string, List<object>>();

public SpreadsheetDocument(string title, string author)
: base(title, author)
{
}

public void AddSheet(string sheetName)
{
Sheets[sheetName] = new List<object>();
}

public override void Save()
{
Console.WriteLine($"Saving spreadsheet '{Title}' as an .xlsx file");
// Implementation to save as spreadsheet
}

public override string GetFileExtension()
{
return ".xlsx";
}

public override long GetSize()
{
// Calculate size based on sheets and data
return Sheets.Count * 1024; // Simplified size calculation
}
}

// Document processor that works with any type of Document
public class DocumentProcessor
{
public void ProcessDocument(Document document)
{
Console.WriteLine("Processing document:");
document.DisplayInfo();
Console.WriteLine($"File extension: {document.GetFileExtension()}");
Console.WriteLine($"Size: {document.GetSize()} bytes");
document.Save();
Console.WriteLine("Processing complete.\n");
}
}

Using the document processing system:

csharp
class Program
{
static void Main(string[] args)
{
DocumentProcessor processor = new DocumentProcessor();

// Create different document types
TextDocument reportDoc = new TextDocument(
"Quarterly Report",
"Jane Smith",
"This quarter showed a 15% increase in revenue..."
);

SpreadsheetDocument budgetDoc = new SpreadsheetDocument(
"Annual Budget",
"John Doe"
);
budgetDoc.AddSheet("Q1");
budgetDoc.AddSheet("Q2");
budgetDoc.AddSheet("Q3");
budgetDoc.AddSheet("Q4");

// Process various document types polymorphically
processor.ProcessDocument(reportDoc);
processor.ProcessDocument(budgetDoc);
}
}

Output

Processing document:
Title: Quarterly Report
Author: Jane Smith
Created: 2023-08-15 14:23:45
File extension: .txt
Size: 46 bytes
Saving text document 'Quarterly Report' as a .txt file
Processing complete.

Processing document:
Title: Annual Budget
Author: John Doe
Created: 2023-08-15 14:23:45
File extension: .xlsx
Size: 4096 bytes
Saving spreadsheet 'Annual Budget' as an .xlsx file
Processing complete.

Best Practices for Abstract Classes

  1. Use abstract classes when:

    • You want to share code among related classes
    • The base behavior makes sense for derived classes
    • You want to enforce specific method implementations
  2. Favor interfaces over abstract classes when:

    • You need multiple inheritance
    • You're defining a contract without default behavior
    • Classes that implement the functionality are unrelated
  3. Naming conventions:

    • Consider prefixing abstract class names with "Base" or "Abstract" (e.g., BaseShape, AbstractShape)
    • Use meaningful names that reflect the abstraction
  4. Design considerations:

    • Keep the class hierarchy shallow (avoid deep inheritance chains)
    • Use abstract classes as templates for families of objects
    • Don't force unrelated classes to inherit from an abstract class

Summary

Abstract classes in .NET provide a powerful mechanism for defining partially implemented base classes that enforce common structure across derived types. They sit between interfaces and concrete classes, allowing for both shared code and enforcement of specific implementations.

Key points to remember:

  • Abstract classes cannot be instantiated directly
  • They can contain both implemented and abstract members
  • Abstract methods must be implemented by derived classes
  • They support regular methods, properties, fields, and constructors
  • Abstract classes enable polymorphic behavior

By leveraging abstract classes effectively, you can create more maintainable and structured object hierarchies in your .NET applications.

Exercises

  1. Create an abstract Shape class with an abstract GetArea() method and implement it in Circle, Rectangle, and Triangle classes.

  2. Design an abstract DatabaseConnection class with common connection operations and implement it for different database types (SQL Server, MySQL, SQLite).

  3. Create an abstract PaymentProcessor class with methods for processing payments, and implement concrete classes for different payment types (Credit Card, PayPal, Cryptocurrency).

  4. Extend the document processing example to include new document types like PresentationDocument and PdfDocument.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)