Easy Learn C#

Abstraction in C#

Introduction to Abstraction

Abstraction is one of the four fundamental principles of Object-Oriented Programming (OOP). It's the process of hiding complex implementation details and showing only the necessary features of an object.

Why Use Abstraction?

  • Simplifies complex systems by hiding unnecessary details
  • Reduces complexity and increases code maintainability
  • Allows focusing on what an object does rather than how it does it
  • Creates a clear separation between interface and implementation
  • Promotes code reusability and modular design
  • Enables changes to implementation without affecting client code

Abstraction vs. Encapsulation

Although abstraction and encapsulation are related concepts, they serve different purposes in OOP:

Differences Between Abstraction and Encapsulation

Abstraction Encapsulation
Focuses on what an object does Focuses on how the object achieves its functionality
Hides complexity by providing simple interfaces Wraps data and methods into a single unit
About creating a conceptual boundary About restricting access to internal details
Implemented using interfaces and abstract classes Implemented using access modifiers (private, protected, etc.)
Design level concept Implementation level concept

Abstract Classes and Methods

C# provides the abstract keyword to implement abstraction. Abstract classes cannot be instantiated and may contain abstract methods that derived classes must implement.

Abstract Class Syntax


// Abstract class
public abstract class Shape
{
    // Regular property
    public string Color { get; set; }
    
    // Regular constructor
    public Shape(string color)
    {
        Color = color;
    }
    
    // Regular method with implementation
    public void SetColor(string color)
    {
        Color = color;
        Console.WriteLine($"Shape color set to {color}");
    }
    
    // Abstract method - no implementation
    public abstract double CalculateArea();
    
    // Abstract method - no implementation
    public abstract void Draw();
    
    // Virtual method with implementation that can be overridden
    public virtual string GetDescription()
    {
        return $"A shape with color {Color}";
    }
}

// Concrete class inheriting from abstract class
public class Circle : Shape
{
    public double Radius { get; set; }
    
    public Circle(double radius, string color) : base(color)
    {
        Radius = radius;
    }
    
    // Implementation of abstract method
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
    
    // Implementation of abstract method
    public override void Draw()
    {
        Console.WriteLine($"Drawing a circle with radius {Radius} and color {Color}");
    }
    
    // Override of virtual method
    public override string GetDescription()
    {
        return $"A circle with radius {Radius} and color {Color}";
    }
}

// Usage
// Shape shape = new Shape("Red");  // Error: Cannot create an instance of an abstract class

Shape circle = new Circle(5, "Blue");
circle.Draw();  // Outputs: "Drawing a circle with radius 5 and color Blue"
Console.WriteLine($"Area: {circle.CalculateArea()}");  // Outputs: "Area: 78.54..."
Console.WriteLine(circle.GetDescription());  // Outputs: "A circle with radius 5 and color Blue"

Key points about abstract classes and methods:

  • Abstract classes cannot be instantiated directly
  • Abstract classes can contain both abstract methods (without implementation) and concrete methods (with implementation)
  • Abstract methods are declared with the abstract keyword and have no body
  • Classes that inherit from an abstract class must implement all its abstract methods
  • Abstract classes can have constructors, fields, properties, and non-abstract methods
  • A class with at least one abstract method must be declared abstract

Interfaces as Abstraction Tools

Interfaces represent another way to achieve abstraction in C#. An interface defines a contract but contains no implementation.

Interface Example


// Interface definition
public interface IDatabase
{
    void Connect(string connectionString);
    void Disconnect();
    List ExecuteQuery(string query);
    int ExecuteCommand(string command);
}

// Concrete implementation for SQL Server
public class SqlServerDatabase : IDatabase
{
    private SqlConnection _connection;
    
    public void Connect(string connectionString)
    {
        Console.WriteLine("Connecting to SQL Server...");
        _connection = new SqlConnection(connectionString);
        _connection.Open();
    }
    
    public void Disconnect()
    {
        Console.WriteLine("Disconnecting from SQL Server...");
        if (_connection != null && _connection.State == ConnectionState.Open)
        {
            _connection.Close();
        }
    }
    
    public List ExecuteQuery(string query)
    {
        Console.WriteLine($"Executing SQL query: {query}");
        // Implementation specific to SQL Server
        return new List();
    }
    
    public int ExecuteCommand(string command)
    {
        Console.WriteLine($"Executing SQL command: {command}");
        // Implementation specific to SQL Server
        return 0;
    }
}

// Concrete implementation for MongoDB
public class MongoDatabase : IDatabase
{
    private MongoClient _client;
    private IMongoDatabase _database;
    
    public void Connect(string connectionString)
    {
        Console.WriteLine("Connecting to MongoDB...");
        _client = new MongoClient(connectionString);
        _database = _client.GetDatabase("mydb");
    }
    
    public void Disconnect()
    {
        Console.WriteLine("Disconnecting from MongoDB...");
        // MongoDB handles connection pooling automatically
    }
    
    public List ExecuteQuery(string query)
    {
        Console.WriteLine($"Executing MongoDB query: {query}");
        // Implementation specific to MongoDB
        return new List();
    }
    
    public int ExecuteCommand(string command)
    {
        Console.WriteLine($"Executing MongoDB command: {command}");
        // Implementation specific to MongoDB
        return 0;
    }
}

// Client code using abstraction
public class DataRepository
{
    private IDatabase _database;
    
    public DataRepository(IDatabase database)
    {
        _database = database;
    }
    
    public void Initialize(string connectionString)
    {
        _database.Connect(connectionString);
    }
    
    public List GetAllUsers()
    {
        return _database.ExecuteQuery("SELECT * FROM Users");
    }
    
    public User GetUserById(int id)
    {
        return _database.ExecuteQuery($"SELECT * FROM Users WHERE Id = {id}").FirstOrDefault();
    }
    
    public void Close()
    {
        _database.Disconnect();
    }
}

// Usage
public void Main()
{
    // Using SQL Server
    IDatabase sqlDb = new SqlServerDatabase();
    DataRepository sqlRepo = new DataRepository(sqlDb);
    sqlRepo.Initialize("Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;");
    
    // Or using MongoDB
    IDatabase mongoDb = new MongoDatabase();
    DataRepository mongoRepo = new DataRepository(mongoDb);
    mongoRepo.Initialize("mongodb://localhost:27017");
    
    // Client code works the same regardless of the database implementation
}

Key points about interfaces as abstraction tools:

  • Interfaces provide a blueprint for functionality without implementation details
  • They enable complete separation between what is done and how it is done
  • Client code can be written against interfaces rather than concrete types
  • Implementation details can change without affecting the client code
  • Multiple implementations can be swapped at runtime

Real-World Abstraction

Abstraction is commonly used in frameworks, libraries, and application architecture to hide complexity and provide clean APIs.

Layered Architecture Example


// Abstraction for the data access layer
public interface IUserRepository
{
    User GetById(int id);
    List GetAll();
    void Add(User user);
    void Update(User user);
    void Delete(int id);
}

// Abstraction for the business logic layer
public interface IUserService
{
    User GetUser(int id);
    List GetAllUsers();
    void CreateUser(UserDto userDto);
    void UpdateUser(UserDto userDto);
    void DeleteUser(int id);
}

// Concrete implementation of the repository
public class SqlUserRepository : IUserRepository
{
    // Implementation using SQL database
    public User GetById(int id) { /* Implementation */ }
    public List GetAll() { /* Implementation */ }
    public void Add(User user) { /* Implementation */ }
    public void Update(User user) { /* Implementation */ }
    public void Delete(int id) { /* Implementation */ }
}

// Concrete implementation of the service
public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;
    
    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }
    
    public User GetUser(int id)
    {
        return _userRepository.GetById(id);
    }
    
    public List GetAllUsers()
    {
        return _userRepository.GetAll();
    }
    
    public void CreateUser(UserDto userDto)
    {
        // Validate the user data
        if (string.IsNullOrEmpty(userDto.Username))
        {
            throw new ArgumentException("Username cannot be empty");
        }
        
        // Convert DTO to entity
        var user = new User
        {
            Username = userDto.Username,
            Email = userDto.Email,
            CreatedDate = DateTime.Now
        };
        
        // Save to repository
        _userRepository.Add(user);
    }
    
    public void UpdateUser(UserDto userDto)
    {
        // Implementation with business rules
    }
    
    public void DeleteUser(int id)
    {
        // Implementation with business rules
    }
}

// Controller/API layer uses the service abstraction
public class UserController
{
    private readonly IUserService _userService;
    
    public UserController(IUserService userService)
    {
        _userService = userService;
    }
    
    public User GetUser(int id)
    {
        return _userService.GetUser(id);
    }
    
    public List GetAllUsers()
    {
        return _userService.GetAllUsers();
    }
    
    // More controller methods
}

This example demonstrates how abstraction is used in a layered architecture:

  • Each layer defines an abstraction (interface) that hides implementation details
  • Higher layers depend on abstractions of lower layers, not concrete implementations
  • Changes to implementation within a layer don't affect other layers
  • Testing is simplified by being able to mock or stub the abstractions
  • Different implementations can be easily swapped (e.g., changing from SQL to NoSQL)

When to Use Abstract Classes vs. Interfaces

Choosing Between Abstract Classes and Interfaces

Use Abstract Classes when:

  • You want to share code among related classes
  • You need to provide a base implementation of some methods
  • Your abstraction requires state (fields) or constructors
  • You want to declare non-public members or methods
  • You expect the abstraction to evolve with new methods that derived classes don't need to implement

Use Interfaces when:

  • You want to define a contract that unrelated classes can implement
  • You need multiple inheritance (a class can implement multiple interfaces)
  • You're defining capability that can be used across different class hierarchies
  • You're designing for interchangeable components
  • You want to ensure complete separation between specification and implementation

// Example showing when to use abstract class
public abstract class Animal
{
    // State (fields)
    protected string Name;
    protected int Age;
    
    // Constructor
    public Animal(string name, int age)
    {
        Name = name;
        Age = age;
    }
    
    // Shared implementation
    public void Sleep()
    {
        Console.WriteLine($"{Name} is sleeping");
    }
    
    // Abstract method that must be implemented
    public abstract void MakeSound();
}

// Example showing when to use interface
public interface IPayable
{
    decimal CalculatePayment();
    void ProcessPayment();
}

// Unrelated classes can implement the same interface
public class Employee : IPayable
{
    public decimal HourlyRate { get; set; }
    public int HoursWorked { get; set; }
    
    public decimal CalculatePayment()
    {
        return HourlyRate * HoursWorked;
    }
    
    public void ProcessPayment()
    {
        // Process employee payment
    }
}

public class Invoice : IPayable
{
    public decimal Amount { get; set; }
    
    public decimal CalculatePayment()
    {
        return Amount;
    }
    
    public void ProcessPayment()
    {
        // Process invoice payment
    }
}

// A class using both abstract class and interface
public class Dog : Animal, ITrainable
{
    public Dog(string name, int age) : base(name, age) { }
    
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
    
    public void Train()
    {
        Console.WriteLine($"{Name} is being trained");
    }
}

Abstraction Best Practices

Guidelines for Effective Abstraction

  • Design for change - Create abstractions that can accommodate future changes
  • Program to abstractions - Depend on interfaces or abstract classes, not concrete implementations
  • Follow SOLID principles - Especially the Dependency Inversion Principle
  • Keep abstractions focused - Each abstraction should have a single, clear purpose
  • Balance abstraction and complexity - Too much abstraction can make code hard to understand
  • Consider the client perspective - Design abstractions from the client code's point of view
  • Hide implementation details - Don't leak implementation details through your abstractions
  • Be consistent - Use consistent patterns and naming across your abstractions

Common Abstraction Mistakes to Avoid

  • Premature abstraction - Creating abstractions before understanding the problem space
  • Over-abstracting - Adding abstraction layers that don't provide meaningful benefits
  • Leaky abstractions - Exposing implementation details that clients shouldn't need to know
  • Incorrect abstraction level - Abstracting at too low or too high a level
  • "One size fits all" abstractions - Creating abstractions that try to do too much
  • Breaking the Interface Segregation Principle - Creating "fat" interfaces with too many methods
  • Rigid abstractions - Designing abstractions that are difficult to adapt as requirements change