C# Interfaces, Composition, and Inheritance
Program to a small capability, combine objects to build behavior, and reserve inheritance for genuine substitutable relationships.
Before this lesson
- Classes and encapsulation
- Methods and constructors
- Collections
Define and implement a focused interface
Use constructor-injected composition
Evaluate inheritance by substitutability rather than surface similarity
The short answer
An interface defines a capability that different types can provide. Composition gives an object collaborators to do its work. Inheritance shares a base contract and implementation, but should be used only when every derived object can safely stand in for the base type.
Depend on the capability you actually need
ReminderService needs something that can send a message. It does not need to know whether delivery uses the console, email, SMS, or a test recorder. IMessageSender names that small capability through a method contract.
An interface contains member contracts that an implementing type must provide. A variable of interface type can refer to any conforming object, which lets calling code focus on what it can do rather than its concrete implementation.
Composition assembles behavior from collaborators
ReminderService receives an IMessageSender in its constructor and stores it. This is composition: the service has a sender and delegates delivery to it. The collaborator is explicit, required, and replaceable without editing the reminder rules.
Composition is often safer than inheritance because relationships can vary independently. A Report can use a formatter and a destination without claiming a report is a formatter or destination. Small interfaces also allow tests to supply a fake or recording implementation.
class RecordingSender : IMessageSender
{
public string LastRecipient { get; private set; }
public string LastMessage { get; private set; }
public void Send(string recipient, string message)
{
LastRecipient = recipient;
LastMessage = message;
}
}Inheritance is an is-a promise
A derived class inherits accessible members from a base class and can override virtual behavior. The important test is substitutability: code written for the base type should remain correct when given the derived type. Similar fields or a desire to reuse ten lines are not enough.
Deep inheritance trees couple types to base implementation details and make behavior difficult to locate. Use a base class when the abstraction is stable and shared implementation genuinely belongs to it. Otherwise prefer composition, an interface, or an ordinary helper method.
abstract class Shape
{
public abstract double Area();
}
class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double Area()
{
return Width * Height;
}
}Choose the least coupled design that expresses the rule
Use an interface when callers need a capability with multiple possible providers. Use composition when one object needs another to perform part of its work. Use inheritance when a true base abstraction exists and derived types preserve its promises. Use none of them when a simple method or concrete class is enough.
Adding an interface for every class creates extra files without flexibility. Add an abstraction at a boundary that varies, needs isolated testing, or represents a real domain capability. Concrete code is not a failure when there is only one stable implementation and no useful separation.
| Tool | Relationship | Good signal |
|---|---|---|
| Interface | can-do contract | Several providers or a replaceable boundary |
| Composition | has-a collaborator | Behavior assembled from focused parts |
| Inheritance | is-a base type | Safe substitution plus shared abstraction |
| Concrete class | direct use | No real variation or boundary yet |
Good habits
- Keep interfaces focused on caller needs rather than mirroring every public member.
- Require essential collaborators through constructors.
- Prefer composition before creating a deep inheritance hierarchy.
- Do not add abstractions solely to predict hypothetical future changes.
Quick knowledge check
Answer before you reveal.
01Does sharing two properties prove that one class should inherit another?
No. Inheritance requires a meaningful is-a relationship and safe substitutability, not merely shared fields.
02Why accept IMessageSender instead of ConsoleMessageSender?
The service needs the sending capability, allowing delivery mechanisms or test implementations to vary independently.
Practice challenge
Now build it without copying.
Create an IPriceRule interface with Calculate(decimal subtotal). Implement RegularPriceRule and MemberPriceRule, then compose one into a CheckoutService through its constructor.
You are done when
- CheckoutService depends on IPriceRule rather than a concrete rule
- Both implementations can be substituted without editing CheckoutService
- The pricing rule returns data rather than printing
Stretch: Add a recording or fixed-price implementation to test CheckoutService without relying on a real discount calculation.
Open challenge in playgroundLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
Can a class implement multiple interfaces?
Yes. A class has one direct base class but can implement multiple capability contracts.
What is an abstract class?
It cannot be instantiated directly and can combine shared implementation with abstract members derived classes must implement. Use it for a genuine family with shared base behavior.
Is dependency injection a framework?
It is a design technique: provide dependencies from outside rather than constructing them internally. Containers can automate wiring, but constructor injection works without a framework.