Menu

C# & ASP.NET Topic

C# Interfaces & Abstract Classes

Interfaces - Pure Contracts

Basic Interface Syntax

csharp

// Simple interface
public interface ILogger
{
    void Log(string message);
    void LogError(string error);
}

// Implementing an interface
public class FileLogger : ILogger
{
    public void Log(string message)
    {
        File.AppendAllText("log.txt", $"{DateTime.Now}: {message}\n");
    }

    public void LogError(string error)
    {
        File.AppendAllText("log.txt", $"{DateTime.Now}: ERROR - {error}\n");
    }
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"LOG: {message}");
    }

    public void LogError(string error)
    {
        Console.WriteLine($"ERROR: {error}");
    }
}

Interface Members

csharp

public interface IRepository<T>
{
    // Properties
    int Count { get; }
    
    // Methods
    void Add(T item);
    void Remove(T item);
    T GetById(int id);
    IEnumerable<T> GetAll();
    
    // Events
    event Action<T> ItemAdded;
    
    // Indexers
    T this[int index] { get; }
}

// Implementing interface with all members
public class ProductRepository : IRepository<Product>
{
    private List<Product> _products = new List<Product>();
    
    public int Count => _products.Count;
    
    public event Action<Product> ItemAdded;
    
    public Product this[int index] => _products[index];
    
    public void Add(Product item)
    {
        _products.Add(item);
        ItemAdded?.Invoke(item);
    }
    
    public void Remove(Product item)
    {
        _products.Remove(item);
    }
    
    public Product GetById(int id)
    {
        return _products.FirstOrDefault(p => p.Id == id);
    }
    
    public IEnumerable<Product> GetAll()
    {
        return _products;
    }
}

Multiple Interface Implementation

csharp

public interface ISerializable
{
    string Serialize();
    void Deserialize(string data);
}

public interface ICloneable<T>
{
    T Clone();
}

public class Product : ISerializable, ICloneable<Product>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    public string Serialize()
    {
        return $"{Id},{Name},{Price}";
    }
    
    public void Deserialize(string data)
    {
        var parts = data.Split(',');
        Id = int.Parse(parts[0]);
        Name = parts[1];
        Price = decimal.Parse(parts[2]);
    }
    
    public Product Clone()
    {
        return new Product { Id = this.Id, Name = this.Name, Price = this.Price };
    }
}

Abstract Classes - Partial Implementation

Basic Abstract Class

csharp

// Abstract class with both abstract and concrete members
public abstract class Shape
{
    // Abstract method - must be implemented by derived classes
    public abstract double CalculateArea();
    
    // Abstract property
    public abstract string Name { get; }
    
    // Concrete method - inherited as-is
    public void Display()
    {
        Console.WriteLine($"Shape: {Name}, Area: {CalculateArea()}");
    }
    
    // Virtual method - can be overridden
    public virtual string GetDescription()
    {
        return "This is a shape";
    }
}

// Concrete implementations
public class Circle : Shape
{
    public double Radius { get; set; }
    
    public override string Name => "Circle";
    
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
    
    public override string GetDescription()
    {
        return $"This is a circle with radius {Radius}";
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public override string Name => "Rectangle";
    
    public override double CalculateArea()
    {
        return Width * Height;
    }
}

Abstract Classes with Constructors

csharp

public abstract class BankAccount
{
    public string AccountNumber { get; }
    public string Owner { get; }
    public decimal Balance { get; protected set; }
    
    // Abstract class can have constructors
    protected BankAccount(string accountNumber, string owner, decimal initialBalance)
    {
        AccountNumber = accountNumber;
        Owner = owner;
        Balance = initialBalance;
    }
    
    // Abstract methods
    public abstract void Deposit(decimal amount);
    public abstract bool Withdraw(decimal amount);
    public abstract decimal CalculateInterest();
    
    // Concrete method
    public string GetAccountInfo()
    {
        return $"Account: {AccountNumber}, Owner: {Owner}, Balance: {Balance:C}";
    }
}

public class SavingsAccount : BankAccount
{
    public decimal InterestRate { get; }
    
    public SavingsAccount(string accountNumber, string owner, decimal initialBalance, decimal interestRate) 
        : base(accountNumber, owner, initialBalance)
    {
        InterestRate = interestRate;
    }
    
    public override void Deposit(decimal amount)
    {
        if (amount > 0)
        {
            Balance += amount;
        }
    }
    
    public override bool Withdraw(decimal amount)
    {
        if (amount > 0 && Balance >= amount)
        {
            Balance -= amount;
            return true;
        }
        return false;
    }
    
    public override decimal CalculateInterest()
    {
        return Balance * InterestRate;
    }
}

public class CheckingAccount : BankAccount
{
    public decimal OverdraftLimit { get; }
    
    public CheckingAccount(string accountNumber, string owner, decimal initialBalance, decimal overdraftLimit) 
        : base(accountNumber, owner, initialBalance)
    {
        OverdraftLimit = overdraftLimit;
    }
    
    public override void Deposit(decimal amount)
    {
        if (amount > 0)
        {
            Balance += amount;
        }
    }
    
    public override bool Withdraw(decimal amount)
    {
        if (amount > 0 && (Balance + OverdraftLimit) >= amount)
        {
            Balance -= amount;
            return true;
        }
        return false;
    }
    
    public override decimal CalculateInterest()
    {
        return 0; // Checking accounts typically don't earn interest
    }
}

Contract-Based Programming

Programming to Interfaces, Not Implementations

csharp

// Without interface - tightly coupled
public class OrderProcessor
{
    private FileLogger _logger = new FileLogger(); // Tight coupling
    
    public void Process(Order order)
    {
        _logger.Log($"Processing order {order.Id}");
        // Process order...
    }
}

// With interface - loosely coupled
public class OrderProcessor
{
    private readonly ILogger _logger;
    
    // Dependency injection through constructor
    public OrderProcessor(ILogger logger)
    {
        _logger = logger;
    }
    
    public void Process(Order order)
    {
        _logger.Log($"Processing order {order.Id}");
        // Process order...
    }
}

// Usage - we can easily swap implementations
ILogger logger = new FileLogger();
// ILogger logger = new ConsoleLogger();
// ILogger logger = new DatabaseLogger();

var processor = new OrderProcessor(logger);
processor.Process(order);

Interface Segregation Principle

csharp

// BAD: One large interface
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
}

// Good: Segregated interfaces
public interface IWorkable
{
    void Work();
}

public interface IEatable
{
    void Eat();
}

public interface ISleepable
{
    void Sleep();
}

// Classes implement only what they need
public class Human : IWorkable, IEatable, ISleepable
{
    public void Work() => Console.WriteLine("Working...");
    public void Eat() => Console.WriteLine("Eating...");
    public void Sleep() => Console.WriteLine("Sleeping...");
}

public class Robot : IWorkable
{
    public void Work() => Console.WriteLine("Working 24/7...");
    // Doesn't need Eat() or Sleep()
}

Dependency Inversion Principle

The Principle

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.

  2. Abstractions should not depend on details. Details should depend on abstractions.

Without Dependency Inversion

csharp

// High-level module depends on low-level modules (violates DIP)
public class OrderService
{
    private SqlDatabase _database; // Concrete dependency
    private EmailNotifier _notifier; // Concrete dependency
    
    public OrderService()
    {
        _database = new SqlDatabase();
        _notifier = new EmailNotifier();
    }
    
    public void ProcessOrder(Order order)
    {
        _database.Save(order);
        _notifier.SendEmail(order.CustomerEmail, "Order processed");
    }
}

// Low-level modules
public class SqlDatabase
{
    public void Save(Order order) { /* SQL implementation */ }
}

public class EmailNotifier
{
    public void SendEmail(string to, string message) { /* Email implementation */ }
}

With Dependency Inversion

csharp

// Abstractions (interfaces)
public interface IOrderRepository
{
    void Save(Order order);
}

public interface INotificationService
{
    void Notify(string recipient, string message);
}

// High-level module depends on abstractions
public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly INotificationService _notificationService;
    
    // Constructor injection - dependencies are provided
    public OrderService(IOrderRepository repository, INotificationService notificationService)
    {
        _repository = repository;
        _notificationService = notificationService;
    }
    
    public void ProcessOrder(Order order)
    {
        _repository.Save(order);
        _notificationService.Notify(order.CustomerEmail, "Order processed");
    }
}

// Low-level modules depend on abstractions
public class SqlOrderRepository : IOrderRepository
{
    public void Save(Order order)
    {
        // SQL-specific implementation
        Console.WriteLine($"Saving order {order.Id} to SQL database");
    }
}

public class MongoOrderRepository : IOrderRepository
{
    public void Save(Order order)
    {
        // MongoDB-specific implementation
        Console.WriteLine($"Saving order {order.Id} to MongoDB");
    }
}

public class EmailNotificationService : INotificationService
{
    public void Notify(string recipient, string message)
    {
        Console.WriteLine($"Sending email to {recipient}: {message}");
    }
}

public class SmsNotificationService : INotificationService
{
    public void Notify(string recipient, string message)
    {
        Console.WriteLine($"Sending SMS to {recipient}: {message}");
    }
}

Practical Examples

Payment Processing System

csharp

// Contracts
public interface IPaymentGateway
{
    PaymentResult ProcessPayment(PaymentRequest request);
    bool ValidatePayment(string paymentId);
}

public interface IPaymentValidator
{
    ValidationResult Validate(PaymentRequest request);
}

public interface IPaymentLogger
{
    void LogPayment(PaymentRequest request, PaymentResult result);
}

// High-level module
public class PaymentProcessor
{
    private readonly IPaymentGateway _paymentGateway;
    private readonly IPaymentValidator _validator;
    private readonly IPaymentLogger _logger;
    
    public PaymentProcessor(
        IPaymentGateway paymentGateway,
        IPaymentValidator validator,
        IPaymentLogger logger)
    {
        _paymentGateway = paymentGateway;
        _validator = validator;
        _logger = logger;
    }
    
    public PaymentResult Process(PaymentRequest request)
    {
        // Validate payment
        var validationResult = _validator.Validate(request);
        if (!validationResult.IsValid)
        {
            return PaymentResult.Failed(validationResult.Errors);
        }
        
        // Process payment
        var result = _paymentGateway.ProcessPayment(request);
        
        // Log result
        _logger.LogPayment(request, result);
        
        return result;
    }
}

// Concrete implementations
public class CreditCardPaymentGateway : IPaymentGateway
{
    public PaymentResult ProcessPayment(PaymentRequest request)
    {
        // Process credit card payment
        Console.WriteLine("Processing credit card payment...");
        return PaymentResult.Success(Guid.NewGuid().ToString());
    }
    
    public bool ValidatePayment(string paymentId)
    {
        // Validate with credit card processor
        return true;
    }
}

public class PayPalPaymentGateway : IPaymentGateway
{
    public PaymentResult ProcessPayment(PaymentRequest request)
    {
        // Process PayPal payment
        Console.WriteLine("Processing PayPal payment...");
        return PaymentResult.Success(Guid.NewGuid().ToString());
    }
    
    public bool ValidatePayment(string paymentId)
    {
        // Validate with PayPal API
        return true;
    }
}

public class DatabasePaymentLogger : IPaymentLogger
{
    public void LogPayment(PaymentRequest request, PaymentResult result)
    {
        // Log to database
        Console.WriteLine($"Logged payment {result.PaymentId} to database");
    }
}

// Usage
var creditCardGateway = new CreditCardPaymentGateway();
var validator = new BasicPaymentValidator();
var logger = new DatabasePaymentLogger();

var processor = new PaymentProcessor(creditCardGateway, validator, logger);
var result = processor.Process(paymentRequest);

Caching System with Strategy Pattern

csharp

// Cache strategy contract
public interface ICacheStrategy<T>
{
    void Add(string key, T value, TimeSpan expiration);
    bool TryGet(string key, out T value);
    void Remove(string key);
    void Clear();
}

// Concrete strategies
public class MemoryCacheStrategy<T> : ICacheStrategy<T>
{
    private readonly Dictionary<string, (T Value, DateTime Expiration)> _cache = new();
    
    public void Add(string key, T value, TimeSpan expiration)
    {
        _cache[key] = (value, DateTime.UtcNow + expiration);
    }
    
    public bool TryGet(string key, out T value)
    {
        if (_cache.TryGetValue(key, out var item) && item.Expiration > DateTime.UtcNow)
        {
            value = item.Value;
            return true;
        }
        
        value = default(T);
        return false;
    }
    
    public void Remove(string key)
    {
        _cache.Remove(key);
    }
    
    public void Clear()
    {
        _cache.Clear();
    }
}

public class RedisCacheStrategy<T> : ICacheStrategy<T>
{
    public void Add(string key, T value, TimeSpan expiration)
    {
        // Implementation using Redis
        Console.WriteLine($"Storing {key} in Redis with expiration {expiration}");
    }
    
    public bool TryGet(string key, out T value)
    {
        // Implementation using Redis
        Console.WriteLine($"Retrieving {key} from Redis");
        value = default(T);
        return false;
    }
    
    public void Remove(string key)
    {
        Console.WriteLine($"Removing {key} from Redis");
    }
    
    public void Clear()
    {
        Console.WriteLine("Clearing Redis cache");
    }
}

// Cache service using strategy
public class CacheService<T>
{
    private readonly ICacheStrategy<T> _cacheStrategy;
    
    public CacheService(ICacheStrategy<T> cacheStrategy)
    {
        _cacheStrategy = cacheStrategy;
    }
    
    public T GetOrAdd(string key, Func<T> valueFactory, TimeSpan expiration)
    {
        if (_cacheStrategy.TryGet(key, out T value))
        {
            return value;
        }
        
        value = valueFactory();
        _cacheStrategy.Add(key, value, expiration);
        return value;
    }
    
    public async Task<T> GetOrAddAsync(string key, Func<Task<T>> valueFactory, TimeSpan expiration)
    {
        if (_cacheStrategy.TryGet(key, out T value))
        {
            return value;
        }
        
        value = await valueFactory();
        _cacheStrategy.Add(key, value, expiration);
        return value;
    }
}

// Usage
ICacheStrategy<string> memoryStrategy = new MemoryCacheStrategy<string>();
ICacheStrategy<string> redisStrategy = new RedisCacheStrategy<string>();

var memoryCache = new CacheService<string>(memoryStrategy);
var redisCache = new CacheService<string>(redisStrategy);

// Can easily swap strategies without changing CacheService

Interface vs Abstract Class Comparison

When to Use Interfaces

csharp

// Use interfaces when:
// 1. You need to define a contract for multiple unrelated classes
public interface IExportable
{
    byte[] Export();
}

public class PdfDocument : IExportable { /* ... */ }
public class ExcelReport : IExportable { /* ... */ }
public class ImageFile : IExportable { /* ... */ }

// 2. You need multiple inheritance of behavior
public class SmartDevice : IConnectable, IUpdatable, IDiagnosable
{
    // Implements multiple interfaces
}

// 3. You want to define a capability or role
public interface IAuditable
{
    DateTime CreatedAt { get; }
    string CreatedBy { get; }
}

public interface ISoftDeletable
{
    bool IsDeleted { get; }
    void SoftDelete();
}
When to Use Abstract Classes
csharp

// Use abstract classes when:
// 1. You want to provide common base implementation
public abstract class EntityBase
{
    public int Id { get; set; }
    public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
    
    // Common functionality
    public virtual bool IsNew() => Id == 0;
    public abstract void Validate(); // Force derived classes to implement
}

public class User : EntityBase
{
    public string Name { get; set; }
    
    public override void Validate()
    {
        if (string.IsNullOrEmpty(Name))
            throw new InvalidOperationException("Name is required");
    }
}

// 2. You need to control object creation with constructors
public abstract class Vehicle
{
    public string Make { get; }
    public string Model { get; }
    
    protected Vehicle(string make, string model)
    {
        Make = make;
        Model = model;
    }
    
    public abstract void Start();
}

public class Car : Vehicle
{
    public Car(string make, string model) : base(make, model) { }
    
    public override void Start()
    {
        Console.WriteLine($"Starting {Make} {Model} car");
    }
}

Advanced Patterns

Factory Pattern with Interfaces

csharp

public interface IShapeFactory
{
    IShape CreateShape(ShapeType type);
}

public class ShapeFactory : IShapeFactory
{
    public IShape CreateShape(ShapeType type)
    {
        return type switch
        {
            ShapeType.Circle => new Circle(),
            ShapeType.Rectangle => new Rectangle(),
            ShapeType.Triangle => new Triangle(),
            _ => throw new ArgumentException($"Unknown shape type: {type}")
        };
    }
}

// Usage
IShapeFactory factory = new ShapeFactory();
IShape circle = factory.CreateShape(ShapeType.Circle);

Repository Pattern with Generic Interface

csharp

public interface IRepository<T> where T : class
{
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Add(T entity);
    void Update(T entity);
    void Delete(T entity);
    IEnumerable<T> Find(Expression<Func<T, bool>> predicate);
}

public abstract class RepositoryBase<T> : IRepository<T> where T : class
{
    protected readonly DbContext _context;
    
    protected RepositoryBase(DbContext context)
    {
        _context = context;
    }
    
    public virtual T GetById(int id)
    {
        return _context.Set<T>().Find(id);
    }
    
    public virtual IEnumerable<T> GetAll()
    {
        return _context.Set<T>().ToList();
    }
    
    public virtual void Add(T entity)
    {
        _context.Set<T>().Add(entity);
    }
    
    public virtual void Update(T entity)
    {
        _context.Set<T>().Update(entity);
    }
    
    public virtual void Delete(T entity)
    {
        _context.Set<T>().Remove(entity);
    }
    
    public virtual IEnumerable<T> Find(Expression<Func<T, bool>> predicate)
    {
        return _context.Set<T>().Where(predicate).ToList();
    }
}

// Specific repository with additional operations
public interface IUserRepository : IRepository<User>
{
    User GetByEmail(string email);
    IEnumerable<User> GetActiveUsers();
}

public class UserRepository : RepositoryBase<User>, IUserRepository
{
    public UserRepository(DbContext context) : base(context) { }
    
    public User GetByEmail(string email)
    {
        return _context.Set<User>().FirstOrDefault(u => u.Email == email);
    }
    
    public IEnumerable<User> GetActiveUsers()
    {
        return Find(u => u.IsActive);
    }
}

Best Practices

1. Interface Design

csharp

// Good interface design
public interface IOrderService
{
    Order CreateOrder(CreateOrderRequest request);
    Order GetOrder(int orderId);
    void UpdateOrder(Order order);
}

// Avoid "God interfaces" - too many responsibilities
// public interface IOrderService // BAD
// {
//     void CreateOrder();
//     void ProcessPayment();
//     void SendNotification();
//     void GenerateReport();
//     void UpdateInventory();
// }
2. Naming Conventions
csharp

// Interfaces start with 'I'
public interface IRepository { }
public interface ILogger { }
public interface INotificationService { }

// Abstract classes often end with 'Base'
public abstract class RepositoryBase { }
public abstract class ServiceBase { }
3. Dependency Injection
csharp

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register interfaces with their implementations
        services.AddScoped<ILogger, FileLogger>();
        services.AddScoped<IOrderRepository, OrderRepository>();
        services.AddScoped<IOrderService, OrderService>();
        
        // This allows easy swapping of implementations
    }
}

4. Testing with Interfaces

csharp

// Mock implementations for testing
public class MockLogger : ILogger
{
    public List<string> LogMessages { get; } = new List<string>();
    
    public void Log(string message)
    {
        LogMessages.Add(message);
    }
}

[Test]
public void OrderProcessor_Should_Log_Message()
{
    // Arrange
    var mockLogger = new MockLogger();
    var processor = new OrderProcessor(mockLogger);
    var order = new Order();
    
    // Act
    processor.Process(order);
    
    // Assert
    Assert.That(mockLogger.LogMessages, Has.Count.EqualTo(1));
}

Key Differences Summary

Aspect Interface Abstract Class
Inheritance Multiple interfaces Single abstract class
Implementation No implementation Partial implementation allowed
Members Methods, properties, events, indexers All class members + abstract members
Constructors No constructors Can have constructors
Access Modifiers All members are public Can have various access levels
Fields No fields Can have fields
When to Use Defining contracts, multiple inheritance Sharing common implementation

This comprehensive guide shows how interfaces and abstract classes enable flexible, maintainable, and testable code through contract-based programming and dependency inversion!

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?