Inheritance
Inheritance is a mechanism where a new class (derived/child) is created from an existing class (base/parent), inheriting its properties and methods. It establishes an “is-a” relationship between classes.
Why Inheritance Matters
- Code Reuse: Share common functionality across related classes
- Hierarchy: Model real-world relationships
- Polymorphism: Enable treating derived objects as their base type
- Extensibility: Add new behavior without modifying existing code
Basic Inheritance
// Base class
public class Animal
{
public string Name { get; set; }
public virtual void Speak()
{
Console.WriteLine($"{Name} makes a sound");
}
public abstract void Move(); // Must be implemented
}
// Derived class - inherits from Animal
public class Dog : Animal
{
public string Breed { get; set; }
public override void Speak()
{
Console.WriteLine($"{Name} barks: Woof!");
}
public override void Move()
{
Console.WriteLine($"{Name} runs on four legs");
}
// New method specific to Dog
public void Fetch()
{
Console.WriteLine($"{Name} fetches the ball");
}
}
// Multi-level inheritance
public class Terrier : Dog
{
public override void Speak()
{
Console.WriteLine($"{Name} yaps: Yip yip!");
}
}
// Usage
Animal myDog = new Dog { Name = "Rex", Breed = "German Shepherd" };
myDog.Speak(); // "Rex barks: Woof!"
myDog.Move(); // "Rex runs on four legs"
Keywords: virtual, override, abstract, sealed
public abstract class Shape
{
// Abstract method - MUST be overridden, no implementation
public abstract double GetArea();
// Virtual method - CAN be overridden, has default implementation
public virtual void Draw()
{
Console.WriteLine("Drawing shape");
}
// Regular method - cannot be overridden
public void LogInfo()
{
Console.WriteLine($"Shape with area: {GetArea()}");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
// Required: implementing abstract method
public override double GetArea() => Math.PI * Radius * Radius;
// Optional: overriding virtual method
public override void Draw()
{
Console.WriteLine($"Drawing circle with radius {Radius}");
}
}
// Sealed class - cannot be inherited
public sealed class Square : Shape
{
public double Side { get; set; }
public override double GetArea() => Side * Side;
}
// Error: Cannot derive from sealed class
// public class SpecialSquare : Square { }
// Sealed method - cannot be overridden in further derived classes
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public sealed override double GetArea() => Width * Height;
}
Calling Base Class Members
public class Employee
{
public string Name { get; set; }
public decimal BaseSalary { get; protected set; }
public Employee(string name, decimal baseSalary)
{
Name = name;
BaseSalary = baseSalary;
}
public virtual decimal CalculatePay()
{
return BaseSalary;
}
}
public class SalesEmployee : Employee
{
public decimal Commission { get; set; }
public decimal SalesAmount { get; set; }
// Call base constructor
public SalesEmployee(string name, decimal baseSalary, decimal commission)
: base(name, baseSalary)
{
Commission = commission;
}
public override decimal CalculatePay()
{
// Call base implementation and add to it
return base.CalculatePay() + (SalesAmount * Commission);
}
}
When NOT to Use Inheritance
Inheritance is powerful but often overused. Here are anti-patterns to avoid:
Anti-Pattern 1: Deep Inheritance Hierarchies
// BAD: Too many levels (fragile base class problem)
public class BasePersistence { }
public class DataAccessLayer : BasePersistence { }
public class RepositoryBase : DataAccessLayer { }
public class ProductRepository : RepositoryBase { }
public class CachedProductRepository : ProductRepository { }
// 5 levels deep! Changes at any level can break everything below
Anti-Pattern 2: Inheriting Just for Code Reuse
// BAD: Using inheritance just to reuse methods
public class Logger
{
protected void Log(string message) { /* ... */ }
}
public class OrderService : Logger // OrderService is NOT a Logger!
{
public void ProcessOrder(Order order)
{
Log("Processing order"); // Works, but wrong design!
}
}
The Right Way: Composition Over Inheritance
// GOOD: Use composition instead
public class OrderService
{
private readonly ILogger _logger;
public OrderService(ILogger logger)
{
_logger = logger;
}
public void ProcessOrder(Order order)
{
_logger.Log("Processing order");
}
}
// GOOD: Decorators instead of deep inheritance
public class CachedProductRepository : IProductRepository
{
private readonly IProductRepository _inner;
private readonly ICache _cache;
public CachedProductRepository(IProductRepository inner, ICache cache)
{
_inner = inner;
_cache = cache;
}
public async Task<Product> GetByIdAsync(int id)
{
var cached = await _cache.GetAsync<Product>($"product_{id}");
if (cached != null) return cached;
var product = await _inner.GetByIdAsync(id);
await _cache.SetAsync($"product_{id}", product);
return product;
}
}
Inheritance Rules for Senior Developers
| Rule | Description |
|---|---|
| Use for “is-a” only | A Dog IS an Animal, not “Dog uses Animal” |
| Keep hierarchies shallow | Maximum 3 levels recommended |
| Prefer composition | “Has-a” relationships are more flexible |
| Prefer interfaces | For contracts without shared implementation |
| Follow LSP | Derived types must be substitutable for base |
Interview Tips
Common Questions:
- “What is inheritance? Give an example.”
- “Explain the difference between
virtualandabstract” - “When would you use inheritance vs. composition?”
- “What is the Liskov Substitution Principle?”
Key Points:
- Inheritance creates “is-a” relationships
- Use
virtualfor methods that CAN be overridden - Use
abstractfor methods that MUST be overridden - Use
sealedto prevent further inheritance - Prefer composition over inheritance for flexibility
- Keep hierarchies flat (max 3 levels)
Red Flags in Code Reviews:
- Deep inheritance hierarchies (4+ levels)
- Inheriting just to reuse code
- Base classes that are too large or do too much
- Violations of Liskov Substitution Principle