Skip to main content

.NET Abstraction

Introduction

Abstraction is one of the four fundamental pillars of Object-Oriented Programming (OOP), alongside encapsulation, inheritance, and polymorphism. In the context of .NET development, abstraction allows you to hide complex implementation details while exposing only the necessary parts of an object that are relevant to the external world.

Think of abstraction as a way to focus on what an object does rather than how it does it. When you drive a car, you don't need to understand how the engine works internally—you just need to know how to use the steering wheel, brakes, and accelerator.

In this tutorial, we'll explore how abstraction is implemented in .NET using C#, why it's important, and how you can use it effectively in your applications.

Understanding Abstraction in .NET

Abstraction in .NET can be achieved in two primary ways:

  1. Abstract Classes: Provide a partial implementation with some methods that must be implemented by derived classes
  2. Interfaces: Define a contract that implementing classes must fulfill

Both approaches allow you to define a common structure that derived or implementing classes must follow, enabling you to work with diverse objects through a unified interface.

Abstract Classes in .NET

An abstract class serves as a blueprint for other classes but cannot be instantiated directly. It can contain both complete methods with implementations and abstract methods (without implementation) that derived classes must override.

Key Characteristics of Abstract Classes

  • Declared using the abstract keyword
  • Cannot be instantiated directly
  • Can contain abstract methods and non-abstract methods
  • Can have constructors and destructors
  • Can have fields, properties, and events
  • Derived classes must implement all abstract methods

Example: Creating an Abstract Class

Let's create an abstract Shape class that defines common functionality for different geometric shapes:

csharp
// Abstract class
public abstract class Shape
{
// Regular property
public string Color { get; set; }

// Constructor
public Shape(string color)
{
Color = color;
}

// Regular method with implementation
public void DisplayColor()
{
Console.WriteLine($"This shape is {Color}");
}

// Abstract methods (no implementation)
public abstract double CalculateArea();
public abstract double CalculatePerimeter();
}

Now let's implement concrete shapes that inherit from this abstract class:

csharp
// Concrete implementation of the Shape class
public class Rectangle : Shape
{
public double Length { get; set; }
public double Width { get; set; }

// Constructor
public Rectangle(double length, double width, string color) : base(color)
{
Length = length;
Width = width;
}

// Implementation of abstract methods
public override double CalculateArea()
{
return Length * Width;
}

public override double CalculatePerimeter()
{
return 2 * (Length + Width);
}
}

// Another concrete implementation
public class Circle : Shape
{
public double Radius { get; set; }

// Constructor
public Circle(double radius, string color) : base(color)
{
Radius = radius;
}

// Implementation of abstract methods
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}

public override double CalculatePerimeter()
{
return 2 * Math.PI * Radius;
}
}

Using the Abstract Class

Here's how you can use these classes:

csharp
class Program
{
static void Main(string[] args)
{
// Cannot instantiate an abstract class
// Shape shape = new Shape("Red"); // This will cause a compilation error

// Create derived class objects
Rectangle rectangle = new Rectangle(5.0, 3.0, "Blue");
Circle circle = new Circle(4.0, "Red");

// We can reference derived class objects through the abstract base type
Shape shape1 = rectangle;
Shape shape2 = circle;

// Call methods
Console.WriteLine("Rectangle:");
shape1.DisplayColor(); // From base class
Console.WriteLine($"Area: {shape1.CalculateArea()}");
Console.WriteLine($"Perimeter: {shape1.CalculatePerimeter()}");

Console.WriteLine("\nCircle:");
shape2.DisplayColor(); // From base class
Console.WriteLine($"Area: {shape2.CalculateArea()}");
Console.WriteLine($"Perimeter: {shape2.CalculatePerimeter()}");
}
}

Output:

Rectangle:
This shape is Blue
Area: 15
Perimeter: 16

Circle:
This shape is Red
Area: 50.26548245743669
Perimeter: 25.132741228718345

Interfaces in .NET

Interfaces provide another way to achieve abstraction in .NET. An interface defines a contract that implementing classes must fulfill, but unlike abstract classes, interfaces cannot provide any implementation.

Key Characteristics of Interfaces

  • Declared using the interface keyword
  • Cannot contain implementation code (before C# 8.0)
  • A class can implement multiple interfaces
  • All members are implicitly public
  • Cannot have fields, constructors, or destructors
  • Can contain methods, properties, events, and indexers

Example: Creating and Implementing Interfaces

Let's create a drawing application with interfaces:

csharp
// Interface definition
public interface IDrawable
{
void Draw();
}

// Another interface
public interface IResizable
{
void Resize(double factor);
}

Now let's implement these interfaces:

csharp
// Class implementing multiple interfaces
public class DrawableRectangle : IDrawable, IResizable
{
public double Width { get; private set; }
public double Height { get; private set; }

public DrawableRectangle(double width, double height)
{
Width = width;
Height = height;
}

// Implementation of IDrawable
public void Draw()
{
Console.WriteLine($"Drawing a rectangle with width {Width} and height {Height}");
}

// Implementation of IResizable
public void Resize(double factor)
{
Width *= factor;
Height *= factor;
Console.WriteLine($"Rectangle resized: width = {Width}, height = {Height}");
}
}

public class DrawableCircle : IDrawable, IResizable
{
public double Radius { get; private set; }

public DrawableCircle(double radius)
{
Radius = radius;
}

// Implementation of IDrawable
public void Draw()
{
Console.WriteLine($"Drawing a circle with radius {Radius}");
}

// Implementation of IResizable
public void Resize(double factor)
{
Radius *= factor;
Console.WriteLine($"Circle resized: radius = {Radius}");
}
}

Using Interfaces

Here's how you can use these interfaces:

csharp
class Program
{
static void Main(string[] args)
{
// Create objects
DrawableRectangle rectangle = new DrawableRectangle(10, 5);
DrawableCircle circle = new DrawableCircle(7);

// Store in a list of IDrawable objects
List<IDrawable> shapes = new List<IDrawable> { rectangle, circle };

// Draw all shapes
foreach (var shape in shapes)
{
shape.Draw();
}

// Resize shapes (need to cast to IResizable because our list is of type IDrawable)
Console.WriteLine("\nResizing shapes...");
foreach (var shape in shapes)
{
if (shape is IResizable resizable)
{
resizable.Resize(1.5);
}
}

// Draw shapes again to see the changes
Console.WriteLine("\nAfter resizing:");
foreach (var shape in shapes)
{
shape.Draw();
}
}
}

Output:

Drawing a rectangle with width 10 and height 5
Drawing a circle with radius 7

Resizing shapes...
Rectangle resized: width = 15, height = 7.5
Circle resized: radius = 10.5

After resizing:
Drawing a rectangle with width 15 and height 7.5
Drawing a circle with radius 10.5

Default Interface Methods (C# 8.0+)

Starting with C# 8.0, interfaces can have default implementations for their members:

csharp
public interface ILogger
{
void LogError(string message);

// Default implementation
void LogInfo(string message)
{
Console.WriteLine($"INFO: {message}");
}

// Default implementation
void LogWarning(string message)
{
Console.WriteLine($"WARNING: {message}");
}
}

// Class only needs to implement required methods
public class FileLogger : ILogger
{
public void LogError(string message)
{
Console.WriteLine($"ERROR TO FILE: {message}");
}

// LogInfo and LogWarning use the default implementations from the interface
}

Abstract Classes vs. Interfaces

FeatureAbstract ClassInterface
InheritanceSingle inheritanceMultiple interface implementation
ImplementationCan provide implementationCan provide only default implementation in C# 8.0+
FieldsCan have fieldsCannot have fields
Access ModifiersCan use access modifiersAll members implicitly public
ConstructorCan have constructorsCannot have constructors
Usage"Is-a" relationship"Can-do" relationship

Real-World Example: Document Processing System

Let's create a more substantial example of a document processing system that demonstrates abstraction:

csharp
// Abstract document class
public abstract class Document
{
public string Title { get; set; }
public string Author { get; set; }
public DateTime CreationDate { get; private set; }

protected Document(string title, string author)
{
Title = title;
Author = author;
CreationDate = DateTime.Now;
}

// Abstract methods
public abstract void Open();
public abstract void Save();

// Concrete method
public void ShowInfo()
{
Console.WriteLine($"Document: {Title}");
Console.WriteLine($"Author: {Author}");
Console.WriteLine($"Created: {CreationDate}");
}
}

// Interface for printing functionality
public interface IPrintable
{
void Print();
int GetNumberOfPages();
}

// Interface for email functionality
public interface IEmailable
{
void SendAsEmail(string recipient);
}

// TextDocument concrete class
public class TextDocument : Document, IPrintable, IEmailable
{
public string Content { get; set; }

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

public override void Open()
{
Console.WriteLine($"Opening text document: {Title}");
Console.WriteLine($"Content: {Content}");
}

public override void Save()
{
Console.WriteLine($"Saving text document: {Title}");
}

public void Print()
{
Console.WriteLine($"Printing text document: {Title}");
Console.WriteLine($"Content: {Content}");
}

public int GetNumberOfPages()
{
// Simple calculation: one page per 500 characters
return (int)Math.Ceiling(Content.Length / 500.0);
}

public void SendAsEmail(string recipient)
{
Console.WriteLine($"Sending document '{Title}' to {recipient}");
}
}

// SpreadsheetDocument concrete class
public class SpreadsheetDocument : Document, IPrintable
{
public int Rows { get; set; }
public int Columns { get; set; }

public SpreadsheetDocument(string title, string author, int rows, int columns)
: base(title, author)
{
Rows = rows;
Columns = columns;
}

public override void Open()
{
Console.WriteLine($"Opening spreadsheet: {Title}");
Console.WriteLine($"Dimensions: {Rows}x{Columns}");
}

public override void Save()
{
Console.WriteLine($"Saving spreadsheet: {Title}");
}

public void Print()
{
Console.WriteLine($"Printing spreadsheet: {Title} ({GetNumberOfPages()} pages)");
}

public int GetNumberOfPages()
{
// Simple calculation: one page per 50 cells
return (int)Math.Ceiling((Rows * Columns) / 50.0);
}
}

And now let's use our document system:

csharp
class Program
{
static void Main(string[] args)
{
// Create documents
TextDocument letter = new TextDocument(
"Resignation Letter",
"John Doe",
"Dear Sir/Madam, I regret to inform you that I am resigning from my position..."
);

SpreadsheetDocument budget = new SpreadsheetDocument(
"Annual Budget",
"Jane Smith",
20,
10
);

// Using polymorphism with the abstract class
List<Document> documents = new List<Document> { letter, budget };

foreach (var doc in documents)
{
Console.WriteLine("--- Document Information ---");
doc.ShowInfo();
doc.Open();
doc.Save();
Console.WriteLine();
}

// Using interfaces
Console.WriteLine("--- Printing Documents ---");
List<IPrintable> printableDocuments = new List<IPrintable> { letter, budget };

foreach (var doc in printableDocuments)
{
doc.Print();
Console.WriteLine($"Number of pages: {doc.GetNumberOfPages()}");
Console.WriteLine();
}

// Using specific interface
Console.WriteLine("--- Emailing Documents ---");
if (letter is IEmailable emailableDoc)
{
emailableDoc.SendAsEmail("[email protected]");
}
}
}

Output:

--- Document Information ---
Document: Resignation Letter
Author: John Doe
Created: 5/10/2023 10:15:32 AM
Opening text document: Resignation Letter
Content: Dear Sir/Madam, I regret to inform you that I am resigning from my position...
Saving text document: Resignation Letter

--- Document Information ---
Document: Annual Budget
Author: Jane Smith
Created: 5/10/2023 10:15:32 AM
Opening spreadsheet: Annual Budget
Dimensions: 20x10
Saving spreadsheet: Annual Budget

--- Printing Documents ---
Printing text document: Resignation Letter
Content: Dear Sir/Madam, I regret to inform you that I am resigning from my position...
Number of pages: 1

Printing spreadsheet: Annual Budget (4 pages)
Number of pages: 4

--- Emailing Documents ---
Sending document 'Resignation Letter' to [email protected]

When to Use Abstraction

Abstraction is useful in several scenarios:

  1. When defining a framework: Use abstractions to define core functionality that others will extend.
  2. When hiding complexity: Hide implementation details that users of your code don't need to know.
  3. When providing a consistent interface: Allow multiple types to be used through a common interface.
  4. When designing for extension: Make your code future-proof by defining abstractions that can be implemented in different ways.
  5. When you want to prevent direct instantiation: Force users to work with concrete implementations rather than the base type.

Best Practices for Abstraction

  1. Keep interfaces focused: Follow the Interface Segregation Principle—define small, specific interfaces rather than large, general ones.
  2. Use abstract classes when you want to provide some common functionality: Abstract classes are ideal when you have base functionality to share.
  3. Use interfaces when you want to define a contract: Interfaces work best when defining a capability that unrelated classes can implement.
  4. Consider composition over inheritance: Sometimes it's better to compose objects rather than create deep inheritance hierarchies.
  5. Document the intent: Clearly communicate the purpose of abstract classes and interfaces through naming and documentation.

Summary

Abstraction is a powerful concept in .NET and Object-Oriented Programming that lets you:

  • Hide implementation details and complexity
  • Provide a clear, simplified interface
  • Enable polymorphic behavior
  • Create flexible, extensible code

In .NET, abstraction is primarily achieved through:

  • Abstract classes: Provide partial implementations with some methods that must be implemented by derived classes
  • Interfaces: Define contracts that implementing classes must fulfill

Effective use of abstraction leads to more maintainable, flexible, and robust code by separating what an object does from how it does it.

Exercises

  1. Create an abstract Vehicle class with properties for make, model, and year, and abstract methods for StartEngine() and StopEngine(). Implement concrete classes for Car and Motorcycle.

  2. Create an interface IPayable with methods for calculating payment amounts. Implement it for classes like Employee, Contractor, and Invoice.

  3. Design a simple game with an abstract GameCharacter class and interfaces like IAttackable and IHealable. Create concrete implementations for different types of characters.

  4. Enhance the document processing system example by adding more document types and implementing a searchable interface.

  5. Create a banking system with abstract accounts and interfaces for different banking operations like deposits, withdrawals, and transfers.

Additional Resources



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