Abstraction
Abstraction is the process of hiding implementation complexity and exposing only the essential interface. It allows you to focus on what an object does rather than how it does it.
Why Abstraction Matters
Consider a car: you donโt need to understand the internal combustion engine to drive. You just use the steering wheel, pedals, and gear shift. The complexity is abstracted away.
In software, abstraction:
- Reduces complexity for consumers of your code
- Allows implementation changes without affecting callers
- Creates cleaner, more maintainable APIs
- Enables better separation of concerns
Abstraction Through Interfaces
Interfaces define a contract - they specify what methods a class must implement without dictating how.
// The interface defines WHAT operations are available
public interface IPaymentProcessor
{
Task<PaymentResult> ProcessAsync(PaymentDetails details);
}
// Implementation details are hidden
public class CreditCardProcessor : IPaymentProcessor
{
private readonly ICardValidator _validator;
private readonly IFraudDetection _fraudDetection;
private readonly IEncryptionService _encryption;
public CreditCardProcessor(
ICardValidator validator,
IFraudDetection fraudDetection,
IEncryptionService encryption)
{
_validator = validator;
_fraudDetection = fraudDetection;
_encryption = encryption;
}
public async Task<PaymentResult> ProcessAsync(PaymentDetails details)
{
// Complex implementation hidden from callers
await _validator.ValidateAsync(details);
await _fraudDetection.CheckAsync(details);
var encryptedData = _encryption.Encrypt(details);
// Process payment...
return new PaymentResult { Success = true };
}
}
// Consumer doesn't need to know about validation, fraud detection, etc.
public class CheckoutService
{
private readonly IPaymentProcessor _paymentProcessor;
public CheckoutService(IPaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}
public async Task CompleteCheckoutAsync(Order order)
{
// Simple, clean interface
var result = await _paymentProcessor.ProcessAsync(order.PaymentDetails);
if (result.Success)
{
// Continue with order...
}
}
}
Abstraction Through Abstract Classes
Abstract classes provide partial implementation along with a contract.
public abstract class BaseRepository<T> where T : class
{
protected DbContext Context { get; }
protected BaseRepository(DbContext context)
{
Context = context;
}
// Abstract methods - must be implemented by derived classes
public abstract Task<T> GetByIdAsync(int id);
public abstract Task<List<T>> GetAllAsync();
// Virtual methods - can be overridden, but have default implementation
protected virtual void ValidateEntity(T entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
}
// Concrete methods - shared implementation
public async Task AddAsync(T entity)
{
ValidateEntity(entity);
await Context.Set<T>().AddAsync(entity);
await Context.SaveChangesAsync();
}
}
// Derived class provides specific implementations
public class ProductRepository : BaseRepository<Product>
{
public ProductRepository(DbContext context) : base(context) { }
public override async Task<Product> GetByIdAsync(int id)
{
return await Context.Set<Product>()
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);
}
public override async Task<List<Product>> GetAllAsync()
{
return await Context.Set<Product>()
.Include(p => p.Category)
.ToListAsync();
}
}
Levels of Abstraction
Good software design has multiple abstraction layers:
Level 5: API Layer (Highest abstraction)
[HttpPost("complete")]
public Task<IActionResult> CompleteOrder(OrderRequest request)
โ
Level 4: Service Layer
public class OrderService
{
public Task CompleteOrderAsync(Order order)
}
โ
Level 3: Interface
public interface IPaymentProcessor
{
Task<PaymentResult> ProcessAsync(PaymentDetails details);
}
โ
Level 2: Public Method Implementation
public async Task<PaymentResult> ProcessAsync(PaymentDetails details)
โ
Level 1: Private Implementation Details (Lowest abstraction)
private void ValidateBankConnection() { /* 100 lines */ }
private void EncryptSensitiveData() { /* 50 lines */ }
Bad vs Good Abstraction
// Bad: Over-complicated interface exposing too many details
public class PaymentProcessor
{
public void ProcessCreditCard(
string cardNumber,
string cvv,
string expiry,
decimal amount,
string name,
string email,
string address,
int zipCode,
string country,
bool is3DSecure,
string bankCode)
{
// Too many parameters!
// Implementation details leak through the interface
}
}
// Good: Clean abstraction with encapsulated details
public interface IPaymentProcessor
{
Task<PaymentResult> ProcessAsync(PaymentDetails details);
}
public class PaymentDetails
{
public string CardNumber { get; set; }
public string CVV { get; set; }
public string Expiry { get; set; }
public decimal Amount { get; set; }
public BillingAddress Address { get; set; }
}
Interview Tips
Common Questions:
- โExplain abstraction with an example from your experienceโ
- โWhatโs the difference between abstraction and encapsulation?โ
- โWhen would you use an interface vs. an abstract class?โ
Key Points to Remember:
- Abstraction focuses on hiding complexity
- It defines what operations are available, not how they work
- Interfaces provide pure abstraction; abstract classes provide partial implementation
- Good abstraction makes code easier to use, test, and maintain
- Abstraction vs Encapsulation: Abstraction hides complexity; encapsulation hides data
Real-World Examples:
IEnumerable<T>- abstracts how collections are iteratedStream- abstracts different data sources (file, network, memory)DbContext- abstracts database operations