OOP and SOLID interview questions

12 questions on object-oriented design and SOLID: interfaces and abstract classes, the five principles, IoC, composition, and the Liskov and fragile base class traps.

01OOP & SOLIDFoundation

Abstract Classes vs Interfaces

Answer

  • An abstract class represents an incomplete base type in an is-a hierarchy. It can contain state (fields), constructors, and a mix of concrete and abstract members. A class can inherit from only one base class.
  • An interface defines a behavioral contract in a can-do relationship. A class can implement multiple interfaces. Interfaces decouple what an object does from how it is implemented.
  • Modern C# Enhancements: C# 8 introduced Default Interface Methods, allowing interfaces to provide default method bodies without breaking existing implementations. C# 11 introduced static abstract members in interfaces, enabling generic math and static factory contracts.
Flow diagram: Abstract class shared state vs interface capability contracts
C# (Template Method and Interface Seams)
public interface IOrderProcessor { Task ProcessAsync(Order order, CancellationToken ct); } // Abstract base controls lifecycle template while delegating specifics public abstract class OrderProcessorBase : IOrderProcessor { protected readonly ILogger Logger; protected OrderProcessorBase(ILogger logger) => Logger = logger; public async Task ProcessAsync(Order order, CancellationToken ct) { Logger.LogInformation("Validating order {Id}", order.Id); await ValidateAsync(order, ct); Logger.LogInformation("Executing payment for order {Id}", order.Id); await ExecutePaymentAsync(order, ct); } protected abstract Task ValidateAsync(Order order, CancellationToken ct); protected abstract Task ExecutePaymentAsync(Order order, CancellationToken ct); }

Decision Framework: When to Use Which

  • Choose an Abstract Class when: multiple closely related classes share internal state and helper methods; you need non-public members (protected); you want to implement the Template Method pattern where the base class controls the algorithm execution skeleton.
  • Choose an Interface when: unrelated classes across different modules share the same capability (IDisposable, IComparable); you need dependency injection seams for unit testing and mocking; you need multiple behavioral contracts on a single type.
  • Pitfall: Stacking deep inheritance trees using abstract base classes creates fragile hierarchies. Prefer small, cohesive interfaces combined with composition.
02OOP & SOLIDFoundation

Sealed and interfaces

Answer

  • sealed on a class blocks further inheritance. Callers know the concrete type will not grow a surprise subclass.
  • sealed on an override stops a derived type from overriding that member again. Use it when the override must stay final.
  • Interfaces define a contract. Unrelated types can implement the same interface without sharing a base class.
  • Seal types you own when further extension would break invariants, or when the type is a leaf DTO or helper with no planned hierarchy.
  • Prefer an interface when many implementations should plug in under DIP. Prefer a sealed class when you want one fixed concrete type.
  • Example: seal OrderDto and Money helpers; expose IOrderPricing so new pricing rules register without subclassing a sealed helper.
  • Pitfall: sealing everything blocks legitimate extension. Seal when the API surface must stay closed, not as a default for every class.
  • In .NET, records are often sealed by design for value-like DTOs. Interfaces remain the open point for behavior.
Flow diagram: sealed
C#
public interface IClock { DateTimeOffset UtcNow { get; } } public sealed class SystemClock : IClock { public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; }
03OOP & SOLIDFoundation

Single Responsibility Principle (SRP)

Answer

  • The Single Responsibility Principle (defined by Robert C. Martin) states: A class should have one, and only one, reason to change.
  • Actor-Based Cohesion: A "reason to change" is anchored in the people or business actors who request that change. If a single class contains methods requested by the Accounting department (calculating salary), the Operations department (scheduling hours), and the IT department (saving employee records to SQL), the class violates SRP because changes requested by Accounting risk introducing regressions into Operations.
  • High Cohesion and Low Coupling: SRP promotes high cohesion (keeping related logic together) and low coupling (isolating unrelated concerns into independent classes).
  • The God Class Antipattern: Classes that handle business validation, database queries, email formatting, and logging inside a single file violate SRP, resulting in code that is difficult to test, maintain, and merge concurrently.
Flow diagram: Splitting multi-responsibility God Class into cohesive actor-aligned services
C# (SRP Cohesive Separation)
public class UserRegistrationService { private readonly IUserValidator _validator; private readonly IUserRepository _repository; private readonly INotificationService _notifications; public UserRegistrationService( IUserValidator validator, IUserRepository repository, INotificationService notifications) { _validator = validator; _repository = repository; _notifications = notifications; } public async Task RegisterAsync(UserRegistrationDto dto, CancellationToken ct) { _validator.Validate(dto); // Single responsibility: Validation var user = new User(dto.Email, dto.PasswordHash); await _repository.AddAsync(user, ct); // Single responsibility: Persistence await _notifications.SendWelcomeEmailAsync(user.Email, ct); // Single responsibility: Delivery } }
04OOP & SOLIDFoundation

Open/Closed Principle (OCP)

Answer

  • The Open/Closed Principle states: Software entities (classes, modules, functions) should be open for extension, but closed for modification.
  • Extending Behavior without Editing Source: You should be able to add new functionality to a module without opening, altering, and recompiling the existing, tested source code.
  • Achieving OCP via Polymorphism: Instead of writing monolithic if/else or switch statements that inspect type flags (which must be modified every time a new business type is introduced), define an interface or abstract base class. New behaviors are introduced by creating new classes implementing the interface.
  • Real-world Pattern: Strategy Pattern and Dependency Injection are the primary mechanisms used in .NET to enforce the Open/Closed Principle.
Flow diagram: Extending discounts via OCP polymorphism vs modifying switch statement
C# (OCP Discount Strategy)
public interface IDiscountStrategy { bool IsApplicable(Customer customer); decimal ApplyDiscount(decimal total); } public class VipDiscountStrategy : IDiscountStrategy { public bool IsApplicable(Customer customer) => customer.IsVip; public decimal ApplyDiscount(decimal total) => total * 0.80m; } // New discount types are added as new classes without modifying OrderPricingCalculator public class OrderPricingCalculator { private readonly IEnumerable<IDiscountStrategy> _strategies; public OrderPricingCalculator(IEnumerable<IDiscountStrategy> strategies) => _strategies = strategies; public decimal Calculate(Order order, Customer customer) { var strategy = _strategies.FirstOrDefault(s => s.IsApplicable(customer)); return strategy is not null ? strategy.ApplyDiscount(order.Subtotal) : order.Subtotal; } }
05OOP & SOLIDFoundation

Liskov Substitution Principle (LSP)

Answer

  • The Liskov Substitution Principle states that objects of a superclass must be replaceable with objects of a subclass without breaking application correctness, contracts, or expected behavior.
  • Behavioral Subtyping: Inheritance is not just about sharing method signatures; it enforces a behavioral contract. If code expects type T, providing a subtype S must not violate any assumptions callers make about T.
  • Preconditions and Postconditions: A subtype may not strengthen preconditions (it cannot demand more than the base type demands) and may not weaken postconditions (it must deliver at least as much as the base type guarantees).
  • The NotSupportedException Smell: If a derived class overrides a base method with throw new NotSupportedException() or NotImplementedException(), it violates LSP. The classic example is a Square inheriting from Rectangle and breaking width/height mutability, or a Penguin inheriting from Bird where Bird defines Fly().
Flow diagram: LSP violation vs sound behavioral hierarchy
C# (LSP Refactoring)
// Broken: Subclass breaks base class contract public class ReadOnlySqlRepository<T> : IRepository<T> { public T GetById(int id) => /* fetch */; public void Save(T entity) => throw new NotSupportedException("Read-only repository cannot save."); // Violates LSP } // Sound: Segregate read and write contracts public interface IReadOnlyRepository<T> { Task<T?> GetByIdAsync(int id, CancellationToken ct); } public interface IWriteRepository<T> { Task SaveAsync(T entity, CancellationToken ct); } public class ReadOnlySqlStore<T> : IReadOnlyRepository<T> { public Task<T?> GetByIdAsync(int id, CancellationToken ct) => /* query */; }

Fixing LSP Violations with Interface Segregation

  • When a subtype cannot fulfill all methods of a base class, the hierarchy is wrong. Split the base contract into focused, cohesive interfaces.
  • Instead of forcing all Birds to implement Fly(), extract IFlyingBird and ISwimmingBird, or model locomotion with a generic Move() method.
06OOP & SOLIDFoundation

SOLID: Interface segregation

Answer

  • Interface Segregation: clients should not depend on methods they do not use.
  • Prefer several small interfaces over one fat interface that every implementer must stub.
  • CQRS-style designs often split read and write ports so query handlers never see mutating methods.
  • Fat interfaces force empty or throwing implementations in adapters. That is a design smell, not a language limit.
  • Example: IPrinter with Print, Staple, Fax. A simple printer stubs Fax. Split into IPrint, IStaple, IFax so clients take only what they need.
  • Pitfall: dozens of one-method interfaces with no cohesion also hurt. Group methods that change together.
  • In .NET, ISP shows up in focused repository and service interfaces rather than IUnitOfWorkWithEverything.
  • When an adapter throws NotImplementedException for half the members, redraw the interface boundary.
Flow diagram: Small interfaces per client need
C#
public interface IPrinter { void Print(Document doc); } public interface IScanner { Document Scan(); } public sealed class SimplePrinter : IPrinter { public void Print(Document doc) { } } public sealed class MultiFunction : IPrinter, IScanner { public void Print(Document doc) { } public Document Scan() => new(); } public sealed class Document { }
07OOP & SOLIDFoundation

Dependency Inversion Principle (DIP)

Answer

  • The Dependency Inversion Principle states: High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
  • DIP vs DI vs IoC: Inversion of Control (IoC) is the overarching architectural pattern where framework code calls application code. Dependency Injection (DI) is a specific technique for supplying dependencies via constructors. Dependency Inversion (DIP) is the software design principle stating where boundaries and abstractions should sit.
  • Architectural Impact: Without DIP, business logic directly instantiates and depends on database drivers (SqlConnection), disk IO, or external APIs (SendGrid). With DIP, business logic declares interfaces (IOrderRepository, INotificationService) and infrastructure implements them.
Flow diagram: Direct dependency vs inverted dependency via abstractions
C# (DIP Architectural Seam)
// Domain / High-level policy owns the abstraction public interface IPaymentGateway { Task<PaymentResult> ChargeAsync(decimal amount, string customerToken, CancellationToken ct); } // High-level service depends on the abstraction, not Stripe or PayPal public class OrderCheckoutService { private readonly IPaymentGateway _paymentGateway; public OrderCheckoutService(IPaymentGateway paymentGateway) => _paymentGateway = paymentGateway; public async Task CheckoutAsync(Order order, string token, CancellationToken ct) { var result = await _paymentGateway.ChargeAsync(order.Total, token, ct); if (!result.IsSuccess) throw new PaymentFailedException(result.ErrorMessage); } }
08OOP & SOLIDFoundation

IoC vs dependency inversion

Answer

  • Dependency Inversion (DIP) is a design rule: high-level code depends on abstractions, not on concrete infrastructure.
  • Inversion of Control (IoC) is a runtime style: the host calls your code and supplies dependencies, instead of your code calling new everywhere.
  • A DI container is one way to do IoC. Constructor injection is the usual .NET style.
  • You can follow DIP with manual factories and no container. The container does not replace the principle.
  • DIP is the design rule. IoC and DI are how the wiring runs.
  • Example: OrderService takes IOrderRepository (DIP). The host resolves EfOrderRepository and injects it (IoC/DI).
  • Pitfall: saying "we use DI so we follow SOLID" confuses the tool with the principle. A container with concrete-only registrations still violates DIP.
  • Interview tip: define DIP without naming Autofac or MS.DI, then show how a container supports that design.
Flow diagram: DIP is the rule; DI wires it
C#
public interface IClock { DateTimeOffset UtcNow { get; } } public sealed class SystemClock : IClock { public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; } public sealed class ExpiryService { private readonly IClock _clock; public ExpiryService(IClock clock) => _clock = clock; // DIP via constructor injection public bool IsExpired(DateTimeOffset at) => at <= _clock.UtcNow; } // IoC/DI registration at composition root: // services.AddSingleton<IClock, SystemClock>(); // services.AddScoped<ExpiryService>();
09OOP & SOLIDFoundation

Encapsulation

Answer

  • Encapsulation hides internal state and exposes behavior through methods and properties that protect invariants.
  • Use private set or init when a value must not change after construction. Public setters invite invalid intermediate states.
  • Validation belongs inside the type that owns the data. Do not rely on every caller to remember the rules.
  • A property can wrap a field so you can change storage later without breaking callers.
  • Example: Money stores amount and currency privately. Add() enforces same-currency rules; callers never mutate Amount directly.
  • Pitfall: anemic models with public fields and all logic in services lose the protection encapsulation is for.
  • In DDD terms, aggregates and value objects encapsulate invariants. Outside code talks through the root or factory methods.
  • Readonly collections returned to callers should be copies or AsReadOnly views so callers cannot mutate internal lists.
Flow diagram: Hide state, expose behavior
C#
public sealed class BankAccount { private decimal _balance; public decimal Balance => _balance; public void Withdraw(decimal amount) { if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount)); if (amount > _balance) throw new InvalidOperationException("Insufficient funds."); _balance -= amount; } }
10OOP & SOLIDFoundation

Composition over inheritance

Answer

  • Deep inheritance trees get brittle. Prefer has-a relationships for behavior you can swap at runtime.
  • Inject behaviors as strategies or decorators. The owner type composes them instead of subclassing for each mix.
  • Inheritance still fits true is-a polymorphism with a stable base and LSP-safe subtypes.
  • Records and value objects cover many DTO cases without inheritance hierarchies.
  • Example: Car has an IEngine. ElectricEngine and GasEngine plug in. CarElectric and CarGas subclasses multiply for every feature mix.
  • Pitfall: composition without clear ownership creates object graphs that are hard to construct. Keep factories or DI registration clear.
  • GoF and modern .NET guidance both favor composition for cross-cutting or optional behaviors.
  • When a subclass only overrides to turn features off, that is often a composition or interface-segregation problem.
Flow diagram: Has-a over deep is-a trees
C#
public interface IEngine { void Start(); } public sealed class GasEngine : IEngine { public void Start() { } } public sealed class Car { private readonly IEngine _engine; public Car(IEngine engine) => _engine = engine; public void Start() => _engine.Start(); }
11OOP & SOLIDAdvanced

Advanced: Liskov traps

Answer

  • A subtype that throws NotSupportedException for a base method breaks callers that only know the base type.
  • Stronger preconditions or weaker postconditions also break the base contract. Callers wrote code against the weaker rules.
  • Square as a Rectangle: code that sets Width and Height separately expects independent sides. A Square that forces equal sides violates that.
  • Model roles the subtype can honor (Move vs Fly). Split interfaces instead of one hierarchy that over-promises.
  • Example: Stream.Seek may be unsupported on some streams. Documented capability checks exist; silent throws still surprise generic Stream users.
  • Pitfall: inheriting for code reuse while narrowing the API is the common LSP trap in code reviews.
  • Prefer composing a Square with a side length, or a Shape hierarchy with Area(), over forcing Square into Rectangle setters.
  • In interviews, walk through a failing unit test that uses the base type and shows the subtype surprise.
Flow diagram: Contract the subtype must keep
C#
public class Rectangle { public virtual int Width { get; set; } public virtual int Height { get; set; } public int Area => Width * Height; } // Fragile: Square overrides setters to keep sides equal. // Code that does r.Width = 4; r.Height = 5; expects Area 20. public class Square : Rectangle { public override int Width { get => base.Width; set { base.Width = value; base.Height = value; } } public override int Height { get => base.Height; set { base.Width = value; base.Height = value; } } }
12OOP & SOLIDAdvanced

Advanced: fragile base class

Answer

  • Changing a base class can break subclasses that override methods, call base, or depend on call order.
  • Protected override hooks are a hidden protocol. Reordering base steps runs overrides at the wrong time.
  • Compose swappable behavior. Keep inheritance shallow when the base must stay free to change.
  • Seal methods or classes when you do not want further override.
  • Example: Base.Save() calls Validate then Persist. A subclass override of Validate assumes Persist never ran first. A base refactor breaks it.
  • Pitfall: "just add a protected virtual hook" for every extension need creates a fragile template method surface.
  • Framework authors seal or document override contracts carefully. Application code should prefer interfaces and composition.
  • If subclasses exist only to customize steps, a strategy or callback may be safer than an open inheritance API.
Flow diagram: Base change breaks subclasses
C#
public class Reporter { public virtual void Run() { Gather(); Write(); } protected virtual void Gather() { } protected virtual void Write() { } } // Subclass overrides Gather and also calls Write. // If Run() later changes order or skips Write, the subclass breaks silently.
Sources and further reading