Advanced OOP Concepts


Advanced OOP Concepts Interview with follow-up questions

1. Can you explain what are constructors and destructors in C#?

Constructors initialize objects; destructors (finalizers) clean up before an object is reclaimed by the garbage collector.

Constructors:

public class Connection
{
    // Instance constructor
    public Connection(string host, int port)
    {
        Host = host;
        Port = port;
    }

    // Constructor chaining — calls another constructor in this class
    public Connection(string host) : this(host, 443) { }

    // Static constructor — runs once, before first use of the type
    static Connection()
    {
        ServicePointManager.DefaultConnectionLimit = 100;
    }

    public string Host { get; }
    public int Port { get; }
}

Primary constructors (C# 12):

public class Connection(string host, int port = 443)
{
    public string Host { get; } = host;
    public int Port { get; } = port;
}

Destructors / finalizers:

public class NativeWrapper
{
    private IntPtr _handle;

    public NativeWrapper() => _handle = NativeLib.Open();

    ~NativeWrapper() // finalizer — called by GC, non-deterministic
    {
        NativeLib.Close(_handle);
    }
}

Why finalizers are problematic:

  • Run on a dedicated finalizer thread, non-deterministically (you don't know when)
  • Objects with finalizers survive their first GC collection, adding GC pressure
  • Can run even after the application exits, in undefined order

The preferred pattern — IDisposable:

public class Connection : IDisposable
{
    private bool _disposed;
    private readonly SqlConnection _conn;

    public void Dispose()
    {
        if (!_disposed)
        {
            _conn.Dispose();
            _disposed = true;
            GC.SuppressFinalize(this); // prevents finalizer from running
        }
    }
}
// Usage
using var conn = new Connection(...); // auto-disposed at end of scope

Interviewers probe: why call GC.SuppressFinalize(this) in Dispose()? — If the user correctly called Dispose(), there's no need for the finalizer to run; suppressing it removes the object from the finalizer queue, reducing GC pressure.

↑ Back to top

Follow-up 1

What is the difference between a constructor and a destructor?

The main difference between a constructor and a destructor in C# is their purpose and when they are called.

A constructor is called automatically when an object is created, and it is used to initialize the object's data members and perform any necessary setup operations. Constructors have the same name as the class and do not have a return type.

A destructor, on the other hand, is called automatically when an object is about to be destroyed or garbage collected. It is used to release any resources or perform any cleanup operations that the object may have acquired during its lifetime. Destructors have the same name as the class, preceded by a tilde (~), and do not have any parameters or return type.

Follow-up 2

Can you provide an example of when you would use a destructor?

Sure! Here's an example of when you would use a destructor in C#:

public class FileHandler
{
    private FileStream fileStream;

    public FileHandler(string filePath)
    {
        fileStream = new FileStream(filePath, FileMode.Open);
    }

    ~FileHandler()
    {
        fileStream.Close();
    }
}

In this example, the FileHandler class is responsible for opening a file and managing the associated FileStream object. The destructor is used to automatically close the FileStream when the FileHandler object is about to be destroyed or garbage collected, ensuring that the file is properly closed and any resources are released.

Follow-up 3

What happens if a constructor or destructor is not defined in a class?

If a constructor is not defined in a class, the compiler automatically generates a default constructor for the class. This default constructor initializes all the data members of the class to their default values.

If a destructor is not defined in a class, the compiler also automatically generates a default destructor for the class. However, this default destructor does not perform any cleanup operations or release any resources. It is generally recommended to define a destructor explicitly if the class needs to perform any cleanup operations or release any resources.

Follow-up 4

Can you explain the concept of constructor overloading in C#?

Constructor overloading in C# allows a class to have multiple constructors with different parameter lists. Each constructor can have a different number or types of parameters, allowing for different ways to initialize an object.

Constructor overloading is useful when you want to provide different ways to create objects of a class, depending on the specific requirements or initializations needed. It provides flexibility and convenience to the users of the class.

Here's an example of constructor overloading in C#:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person()
    {
        // Default constructor
    }

    public Person(string name)
    {
        Name = name;
    }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

In this example, the Person class has three constructors: a default constructor, a constructor that takes only the name parameter, and a constructor that takes both the name and age parameters. This allows users of the class to create Person objects in different ways, depending on their needs.

Follow-up 5

What is a static constructor and when would you use one?

A static constructor in C# is a special constructor that is used to initialize the static members of a class. It is called automatically before any static members are accessed or any static methods are called.

A static constructor is defined using the static keyword and does not have any access modifiers or parameters. It cannot be called directly and is executed only once, regardless of the number of instances of the class.

You would use a static constructor when you need to perform one-time initialization of static members or set up any static resources that are required by the class. It is commonly used to initialize static variables or register event handlers.

Here's an example of a static constructor in C#:

public class Logger
{
    private static readonly string logFilePath;

    static Logger()
    {
        logFilePath = GetLogFilePath();
    }

    // Rest of the class
}

In this example, the Logger class has a static constructor that initializes the logFilePath static variable by calling the GetLogFilePath method. This ensures that the log file path is set before any static members or methods of the Logger class are accessed.

2. What are abstract classes in C# and how are they different from interfaces?

Abstract classes and interfaces both define contracts but differ in capabilities and use cases. C# 8+ blurred the line somewhat, so it's important to know the current state.

Comparison:

Feature Abstract Class Interface
Instantiation Cannot be instantiated Cannot be instantiated
State (fields) Yes No (only static/const in C# 8+)
Implementation Can provide concrete methods Default implementations (C# 8+)
Constructors Yes (called by derived classes) No
Inheritance Single (class can only extend one) Multiple (class can implement many)
Access modifiers on members Any public by default (C# 8: can be private for helpers)
Static abstract members No Yes (C# 11+)

Example:

// Abstract class — IS-A with shared code
public abstract class Repository
{
    protected readonly DbContext Db;
    protected Repository(DbContext db) => Db = db;
    public abstract Task GetByIdAsync(int id);
    public async Task> GetAllAsync() => await Db.Set().ToListAsync();
}

// Interface — CAN-DO contract
public interface IRepository
{
    Task GetByIdAsync(int id);
    Task> GetAllAsync();
}

When to use each:

  • Abstract class: related types that share implementation (e.g., a base repository with common GetAll logic), need protected/constructor members, or represent a true IS-A hierarchy.
  • Interface: defining capabilities across unrelated types (IDisposable, IComparable), when you need multiple "inheritance," or for dependency injection targets.

C# 8+ default implementations: interfaces can now have method bodies, enabling library evolution without forcing all implementors to update. However, this doesn't make interfaces a replacement for abstract classes — interfaces still lack instance state.

Interviewers probe: can an abstract class implement an interface? Yes, and it can leave some interface members unimplemented (abstract) for derived classes to handle. Which do you prefer for DI targets? Interfaces — they are more flexible and easier to mock in tests.

↑ Back to top

Follow-up 1

Can you provide an example of when you would use an abstract class versus an interface?

Sure! Let's say you have a hierarchy of animals: Animal is the base class, Mammal and Bird are derived classes. You want to define a common behavior for all animals, such as Eat and Sleep. In this case, you would use an abstract class Animal with abstract methods Eat and Sleep. Now, let's say you also want to define a behavior specific to birds, such as Fly. In this case, you can create an interface IFlyable with a method Fly and have the Bird class implement this interface. This way, you can have both a common base implementation for all animals and specific behaviors for certain types of animals.

Follow-up 2

Can an abstract class have a constructor?

Yes, an abstract class can have a constructor. The constructor of an abstract class is used to initialize the common state of the derived classes. However, an abstract class cannot be instantiated directly, so the constructor of an abstract class is typically called from the constructors of the derived classes using the base keyword.

Follow-up 3

Can an abstract class be instantiated?

No, an abstract class cannot be instantiated directly. It can only be used as a base class for other classes. To create an instance of a class that inherits from an abstract class, you need to create an instance of the derived class.

Follow-up 4

What is the purpose of an abstract method?

The purpose of an abstract method is to provide a method signature without an implementation. It is meant to be overridden by the derived classes. Abstract methods are declared in abstract classes or interfaces and they do not have a body. The derived classes are required to provide an implementation for the abstract methods. This allows for polymorphism and the ability to define a common method signature that can be implemented differently by different classes.

Follow-up 5

Can an interface contain fields or constructors?

No, an interface cannot contain fields or constructors. Interfaces can only contain method signatures, properties, events, and indexers. Fields and constructors are implementation details specific to classes and cannot be defined in an interface.

3. Can you explain the concept of design patterns in C#?

Design patterns are reusable, named solutions to recurring software design problems. In C# interviews, familiarity with the most common GoF (Gang of Four) patterns and modern .NET idioms is expected.

Three categories:

Creational — object construction:

  • Singleton: one instance per application. Modern .NET approach: register as services.AddSingleton() in the DI container rather than hand-rolling with static. If hand-rolling, use Lazy for thread-safe lazy init.
  • Factory / Abstract Factory: create objects without specifying the concrete type. Common as DI registrations (services.AddTransient()).
  • Builder: fluent API for constructing complex objects. Used heavily in .NET: WebApplication.CreateBuilder(args).Build(), new HttpClient()HttpClientBuilder.

Structural — how objects are composed:

  • Decorator: adds behavior by wrapping an object. ASP.NET Core middleware pipeline is a chain of decorators.
  • Adapter: wraps an incompatible interface to make it compatible.
  • Facade: simplified interface over a complex subsystem.

Behavioral — communication between objects:

  • Observer / Event: C# delegates and event keyword are the language-level implementation.
  • Strategy: inject interchangeable algorithms via interfaces. Classic dependency injection scenario.
  • Repository: data access abstraction (widely used with EF Core).
  • Command / CQRS: separate read and write operations; popular with MediatR in modern .NET APIs.
  • Mediator: decouples senders and receivers (MediatR library in .NET).
  • Chain of Responsibility: ASP.NET Core middleware pipeline.

Interviewers probe: describe the Singleton pattern and its pitfalls — it introduces global state, makes testing harder, and can cause threading issues if not thread-safe. Prefer DI container lifetime management over manual singletons.

↑ Back to top

Follow-up 1

Can you provide an example of a design pattern you have used in your projects?

Yes, one example of a design pattern I have used in my projects is the Observer design pattern. This pattern is used to establish a one-to-many dependency between objects, where the subject (or observable) object maintains a list of its dependents (or observers) and notifies them automatically of any state changes. This pattern is useful in scenarios where multiple objects need to be notified and updated when a particular object's state changes.

Follow-up 2

What is the Singleton design pattern and when would you use it?

The Singleton design pattern is a creational design pattern that ensures a class has only one instance, and provides a global point of access to that instance. It is useful in scenarios where you want to restrict the instantiation of a class to a single object, such as when you need to control access to a shared resource or when you want to limit the number of instances of a class to save memory.

Follow-up 3

Can you explain the difference between Creational, Structural and Behavioral design patterns?

Creational design patterns focus on the process of object creation, providing mechanisms for creating objects in a manner suitable for a particular situation. Structural design patterns focus on the composition of classes and objects, providing ways to form larger structures from individual objects. Behavioral design patterns focus on the interaction between objects, defining how objects communicate and collaborate to achieve a desired behavior.

Follow-up 4

What is the Factory design pattern and how is it implemented in C#?

The Factory design pattern is a creational design pattern that provides an interface for creating objects, but allows subclasses to decide which class to instantiate. It encapsulates the object creation logic, providing a way to delegate the responsibility of object instantiation to subclasses. In C#, the Factory pattern can be implemented using either a simple factory or an abstract factory. A simple factory uses a single factory class to create objects, while an abstract factory uses a hierarchy of factory classes to create related objects.

Follow-up 5

What is the Observer design pattern and can you provide a practical example of its use?

The Observer design pattern is a behavioral design pattern that establishes a one-to-many dependency between objects, where the subject (or observable) object maintains a list of its dependents (or observers) and notifies them automatically of any state changes. A practical example of its use is in a messaging system, where multiple subscribers (observers) can subscribe to a topic (subject) and receive notifications whenever a new message is published. This allows for decoupling between the publisher and the subscribers, as the publisher does not need to know about the specific subscribers, and the subscribers can dynamically subscribe and unsubscribe from topics.

4. What is the purpose of using interfaces in C#?

Interfaces in C# define a contract — a set of members that any implementing type must provide. They are the primary abstraction mechanism in modern C# development.

Core purposes:

  1. Dependency inversion / loose coupling:

    // Depend on the abstraction, not the concrete type
    public class OrderService(IOrderRepository repo, IEmailSender email)
    {
    public async Task PlaceOrderAsync(Order order)
    {
        await repo.SaveAsync(order);
        await email.SendConfirmationAsync(order.CustomerEmail);
    }
    }
    

    OrderService can be tested with mock implementations of both interfaces.

  2. Multiple "inheritance" of behavior:

    public class FileLogger : ILogger, IDisposable, IAsyncDisposable { ... }
    
  3. Cross-cutting capability contracts: IDisposable, IComparable, IEnumerable, IAsyncEnumerable, IAsyncDisposable — these work across unrelated types.

  4. Default implementations (C# 8+): allow adding new interface members without breaking all existing implementors:

    public interface IGreeter
    {
    string Greet(string name);
    string GreetAll(IEnumerable names) => string.Join(", ", names.Select(Greet));
    }
    
  5. static abstract members (C# 11): enable generic algorithms across numeric types:

    T Sum(IEnumerable values) where T : INumber =>
    values.Aggregate(T.Zero, (acc, v) => acc + v);
    
  6. Explicit interface implementation: when a class implements two interfaces with the same member name:

    void ILogger.Log(string msg) { ... }      // only callable via ILogger reference
    void IAuditor.Log(string msg) { ... }     // only callable via IAuditor reference
    

Interviewers probe: interface vs abstract class for a DI target? Prefer interface — easier to mock (Moq, NSubstitute), allows multiple implementation, and signals a capability rather than a type hierarchy.

↑ Back to top

Follow-up 1

Can you provide an example of when you would use an interface?

Sure! Let's say you have a program that needs to work with different types of shapes, such as circles, squares, and triangles. You can define an interface called 'IShape' that declares methods like 'CalculateArea' and 'CalculatePerimeter'. Then, you can create separate classes for each shape (e.g., 'Circle', 'Square', 'Triangle') that implement the 'IShape' interface and provide their own implementation for these methods. This way, you can write code that works with any shape object, as long as it implements the 'IShape' interface.

Follow-up 2

What is the difference between an interface and a class?

In C#, a class is a blueprint for creating objects, while an interface is a contract that defines a set of methods and properties that a class must implement. A class can inherit from another class, but it can implement multiple interfaces. A class can have fields, properties, methods, and events, while an interface can only have method and property declarations. Additionally, a class can provide default implementations for its methods, but an interface can only declare method signatures.

Follow-up 3

Can an interface inherit from another interface?

Yes, an interface can inherit from one or more interfaces. This is known as interface inheritance. When an interface inherits from another interface, it inherits all the members (methods and properties) of the base interface. The derived interface can then add additional members or override the inherited members if needed. Interface inheritance allows for creating a hierarchy of interfaces, which can be useful for organizing and reusing code.

Follow-up 4

Can you explain the concept of multiple inheritance in C# using interfaces?

In C#, a class can only inherit from a single base class, but it can implement multiple interfaces. This is known as multiple inheritance through interfaces. By implementing multiple interfaces, a class can inherit and provide the functionality defined by each interface. This allows for achieving multiple inheritance-like behavior without the complexities and ambiguities associated with multiple inheritance through classes. It provides a way to achieve code reuse and polymorphism while avoiding the issues that arise from inheriting from multiple classes.

Follow-up 5

What happens if two interfaces declare a method with the same name?

If two interfaces declare a method with the same name, a class that implements both interfaces must provide an implementation for that method. The class can choose to implement the method differently for each interface or provide a single implementation that satisfies both interfaces. The method implementation in the class must explicitly specify which interface it is implementing by using the interface name followed by the method name. This allows for disambiguation when multiple interfaces define a method with the same name.

5. Can you explain the concept of encapsulation in C#?

Encapsulation is the object-oriented principle of bundling data and the operations that act on it within a class, while restricting direct external access to internal state.

Access modifiers — the tools:

public class Temperature
{
    private double _celsius;            // hidden from outside

    public double Celsius
    {
        get => _celsius;
        set => _celsius = value >= -273.15
            ? value
            : throw new ArgumentOutOfRangeException(nameof(value), "Below absolute zero");
    }

    public double Fahrenheit
    {
        get => _celsius * 9 / 5 + 32;
        set => Celsius = (value - 32) * 5 / 9;
    }
}

External code sees only Celsius and Fahrenheit properties; the internal _celsius field and validation logic are hidden. You can later change the storage representation without affecting callers.

Access modifier summary:

Modifier Scope
private Class only (default for members)
protected Class + derived classes
internal Same assembly
protected internal Same assembly OR derived
private protected Same assembly AND derived
public Anywhere

Benefits of encapsulation:

  • Invariant enforcement: validation in property setters keeps objects in a valid state.
  • Implementation flexibility: internals can change without breaking callers (API stability).
  • Reduced coupling: callers depend only on the public interface.
  • Thread safety: controlling mutation paths makes it easier to add synchronization.

Modern patterns:

  • init-only setters (C# 9+): settable during object initialization, immutable after — encapsulates mutation to the construction phase.
  • required (C# 11): compiler enforces that callers supply certain properties.
  • sealed class: prevents external code from subclassing and overriding encapsulated behavior.
  • internal + InternalsVisibleTo: expose implementation details only to test assemblies.

Interviewers probe: why is a public field worse than a property? Properties are versioning-safe (add logic without breaking callers), support data binding in UI frameworks, and can be defined in interfaces. Public fields cannot.

↑ Back to top

Follow-up 1

Why is encapsulation considered a fundamental concept in object-oriented programming?

Encapsulation is considered a fundamental concept in object-oriented programming because it helps to achieve the principles of data abstraction, data hiding, and modularity. By bundling data and methods together, encapsulation allows for the creation of self-contained and reusable objects. It also helps to improve the maintainability and flexibility of code by providing a clear separation between the internal implementation details and the external interface of an object.

Follow-up 2

Can you provide an example of encapsulation in C#?

Sure! Here's an example of encapsulation in C#:

public class Person
{
    private string name;
    private int age;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public int Age
    {
        get { return age; }
        set { age = value; }
    }
}

// Usage
Person person = new Person();
person.Name = "John Doe";
person.Age = 25;
Console.WriteLine(person.Name); // Output: John Doe
Console.WriteLine(person.Age); // Output: 25

Follow-up 3

How does encapsulation improve the robustness of a codebase?

Encapsulation improves the robustness of a codebase by providing a way to control access to the internal data and methods of an object. By encapsulating data within a class and exposing it through public properties or methods, we can enforce validation rules, perform error handling, and ensure that the data is always in a valid state. This helps to prevent unexpected modifications or misuse of the data, leading to more reliable and bug-free code.

Follow-up 4

What is the relationship between encapsulation and data hiding?

Encapsulation and data hiding are closely related concepts. Data hiding is a principle of object-oriented programming that involves hiding the internal details of an object and only exposing a limited interface to the outside world. Encapsulation is the mechanism by which data hiding is achieved. It allows for the bundling of data and methods together within a class, and the use of access modifiers to control the visibility and accessibility of the data. By encapsulating data, we can hide the implementation details and provide a controlled way to access and modify the data.

Follow-up 5

How does encapsulation relate to the concept of abstraction?

Encapsulation and abstraction are two fundamental concepts in object-oriented programming that are closely related. Abstraction involves the creation of simplified models or representations of real-world objects, focusing on the essential characteristics and behaviors of those objects. Encapsulation, on the other hand, involves bundling data and methods together within a class. Encapsulation provides a way to implement abstraction by hiding the internal implementation details of an object and exposing a simplified interface to the outside world. By encapsulating data and methods, we can create abstract representations of objects that can be easily understood and used by other parts of the code.

Live mock interview

Mock interview: Advanced OOP Concepts

Intermediate ~5 min Your own free AI key

Your voice and your AI key never touch our servers; the key stays in this browser and is sent only to Google. Only your round scores are saved to track progress.