OOP Concepts in C#
- Encapsulation — private state with a controlled public API.
BankAccountkeeps_balanceprivate and exposesDeposit()that rejects negative amounts. - Abstraction — expose what, hide how.
IPaymentGateway.ChargeAsync()hides HTTP calls, signatures and retries. - Inheritance — a derived class reuses and extends a base.
Contractor : EmployeereusesNameandDescribe(). - Polymorphism — the same call behaves differently by type.
employee.MonthlyPay()runs thePermanentorContractoroverride at runtime.
In short: A class is a type definition (blueprint) describing fields, properties and methods; it exists in code and metadata.
- An object is an instance of that class created at runtime with
new, occupying memory with its own state. - One class, many objects.
| Overloading | Overriding |
|---|---|
| Same name, different parameters | Same signature in a derived class |
| Same class (or derived) | Requires virtual/abstract + override |
| Resolved at compile time | Resolved at runtime by the object's type |
| Return type alone can't differ | Return type the same (covariant returns allowed since C# 9) |
In short: An abstract class can hold state, constructors and shared implementation, and a class can inherit only one.
- An interface is a contract; a class can implement many, and it has no instance state (default methods are allowed since C# 8).
- Use an interface for capabilities and dependency injection (
IEmailSender) and an abstract class when related types share code and state (Shape,Employee). - A common design is to use both: the interface for consumers, an abstract base for shared code.
In short: Not for classes — a class has exactly one base class, which avoids the diamond problem (ambiguity when two parents implement the same member).
- A class can implement multiple interfaces.
- If two interfaces declare the same member, use explicit interface implementation to disambiguate.
In short: public, private, protected, internal, protected internal, private protected (plus file for types).
- Defaults: class members are
private; top-level types areinternal; interface members arepublic.
In short: A constructor initialises a new object and has the class's name and no return type.
- A private constructor prevents outside instantiation (Singleton, factory methods, static-like classes).
- A static constructor takes no parameters and no access modifier, runs once before the type is first used, and initialises static data.
- Constructors can be overloaded and chained with
this(...)/base(...). - They are not inherited.
sealed keyword?In short: A sealed class can't be inherited (e.g. string).
- A
sealed overridemethod can't be overridden further. - Use it to lock down security-sensitive or invariant-heavy types.
- It also gives a small performance gain because the JIT can devirtualise calls.
new)?In short: With override, the runtime type decides which method runs — true polymorphism.
- With hiding (
new), the compile-time variable type decides:A x = new B(); x.Name()callsA.Name(). - Hiding breaks polymorphism and is usually a design smell.
- The compiler warns if you hide a member without the
newkeyword.
In short: Inheritance is a static "is-a" relationship fixed at compile time; subclasses depend on base-class internals, so base changes can break them.
- Composition is "has-a": an object holds other objects (often via interfaces) and delegates to them.
- You can swap them at runtime, test them in isolation and avoid class explosions.
- Use inheritance only when the subtype really is a substitutable kind of the base (see LSP).
In short: All three are "uses/has" relationships of different strength.
- Association: two classes know about each other and have independent lifetimes (Doctor–Patient).
- Aggregation: whole–part where the part can exist alone (Team–Player; delete the team and the players remain).
- Composition: whole–part with shared lifetime (Order–OrderLine, House–Room; delete the order and its lines go too).
In short: You can't instantiate it with new.
- It can have constructors, usually
protected, which derived classes call via: base(...)to initialise shared state. - An abstract class can also have no abstract members at all; it's then just a base that must not be used directly.
In short: A virtual method can be overridden in derived classes.
- C# makes methods non-virtual by default (unlike Java) for performance (non-virtual calls are direct and can be inlined) and versioning safety: designers must explicitly allow overriding, so a new base method can't accidentally change the behaviour of subclasses.
In short: A static class can't be instantiated or inherited and contains only static members (e.g. Math, extension method holders).
- A singleton is a normal class with one instance.
- It can implement interfaces, be passed as a dependency, be lazily created and be mocked.
- In ASP.NET Core you usually get singleton behaviour by registering a service with
AddSingletonrather than writing the pattern by hand.
In short: Hash-based collections (Dictionary, HashSet) first compare hash codes to find a bucket, then call Equals.
- If two equal objects return different hash codes, lookups miss and duplicates appear.
- The contract:
a.Equals(b)⇒a.GetHashCode() == b.GetHashCode(). - Use
HashCode.Combine(...), or use arecord, which generates both.
In short: Interfaces can now include a method body.
- It was added so library authors can add members to a published interface without breaking existing implementers, and for Java/Swift interop.
- The default implementation is only reachable through the interface type, not through the class variable.
In short: Cohesion is how strongly related a class's responsibilities are; coupling is how much a class depends on others.
- Aim for high cohesion and low coupling.
- Signs of trouble: classes named
Manager/Helper/Utilswith unrelated methods, constructors with 8+ dependencies, and changes that ripple across many files. - Tools: Visual Studio code metrics (class coupling, maintainability index), NDepend, SonarQube.
In short: An override can return a more derived type than the base method.
- For example,
Animal.Clone()returnsAnimal, andDog.Clone()can be declared to returnDog. - Callers then don't need to cast.
Bird with a Fly() method and now need Penguin. How do you model it?In short: Making Penguin : Bird and throwing NotSupportedException from Fly() violates Liskov substitution: callers that loop over birds and call Fly() will crash.
- Split the capability out:
Bird(common: eat, lay eggs) plus anIFlyableinterface implemented only by birds that fly. - Code that needs flying asks for
IFlyable. - This is composition of capabilities over a deep hierarchy.
ReportGenerator class has methods for fetching data, formatting PDF and Excel, emailing and logging. It's 3,000 lines. How would you refactor it?In short: Identify the responsibilities and extract each into a focused class behind an interface: IReportDataSource, IReportFormatter (with PdfFormatter and ExcelFormatter), IReportSender and ILogger.
ReportGeneratorthen orchestrates them, receiving them by constructor injection.- Add unit tests around the current behaviour before extracting (characterisation tests), then refactor in small steps.
- Result: SRP, OCP (new format = new formatter class) and easy mocking.
PaymentService. What design would you suggest?In short: Replace the switch with polymorphism: an IPaymentMethod interface with one class per method (Card, UPI, Wallet, BankTransfer).
- Register all of them in DI, and have a resolver pick the right one by key.
- Adding a method means adding a class and a DI registration, without touching existing code (Open/Closed principle, Strategy pattern).
In short: Start with the nouns: ParkingLot (has Levels), Level (has Spots), ParkingSpot (abstract: Compact, Large, Motorbike, EvCharging), Vehicle (abstract: Car, Bike, Truck), Ticket, Payment, EntryGate/ExitGate.
- Behaviour:
ParkingLot.Park(vehicle)finds a fitting spot (Strategy for allocation), issues aTicket; Exit(ticket)calculates the fee (Strategy for pricing) and frees the spot.- Mention encapsulation (spot availability private), polymorphism (
CanFit(vehicle)), concurrency (two cars taking the same spot: lock per level or an atomic DB update), and extensibility (new vehicle types).