The Socket Is Not the Toaster
What an interface actually is in C#, and how to know you understood it
Interfaces were one of the concepts I struggled with most when I first learned to code. Recently, a junior engineer started working with us, and while talking to him, I realised that he had run into the same obstacle.
So I told him the story of the toaster, the analogy that had finally made interfaces click for me. By the end, I think it had clicked for him too. That is why I decided to share it.
A wall socket doesn’t toast bread. It doesn’t know whether you will plug in a lamp, a phone charger, or a toaster.
The socket only specifies a shape (depending on where you are geographically) it has two slots, a ground pin and an active voltage on the other side. Anything that fits that shape can be used. Anything that doesn’t, cannot. It might even go boom.
The promise
That’s how an interface works in C#. It defines what a type must be able to do, but not how it does it.
public interface IToaster
{
void Insert(string slice);
string Eject();
bool IsHot { get; }
}This interface promises three members. Something that calls an IToaster may insert a slice, eject toast, and ask whether the toaster is hot.
The interface shown here contains no method bodies or instance state. It has no constructor and isn’t an object you create with new. It describes a capability that another type must provide. Modern C# permits certain interface members to have default implementations, but that does not turn the interface into the concrete object.
A class implements the interface by listing it after a colon and providing the promised members.
public sealed class CheapToaster : IToaster
{
private string? _slice;
private bool _hot;public void Insert(string slice)
{
_slice = slice;
_hot = true;
} public string Eject()
{
if (_slice is null)
throw new InvalidOperationException(
"The toaster is empty."); string toast = $"toasted {_slice}";
_slice = null;
_hot = false; return toast;
} public bool IsHot => _hot;
}
If CheapToaster forgets Eject, misspells IsHot, or returns the wrong type, the project does not compile. The compiler has become a contract lawyer who works for free and never gets tired.
Where does the behaviour come from?
If the interface contains only the promise, where does it get its actual behaviour? The behaviour comes from the concrete object assigned to the variable.
IToaster toaster = new CheapToaster();The two sides of this line perform different jobs. IToaster determines which members may be used through the variable. new CheapToaster() creates the real object containing the fields and method bodies.
When you call this method:
toaster.Insert("sourdough");C# follows the reference to the actual object and runs CheapToaster.Insert. If the object were an IndustrialToaster, it would run IndustrialToaster.Insert instead.
The interface is never filled with behaviour. It provides a shared way to reach an object that already has behaviour.
What refers to what?
A notional machine is a simplified picture of what we imagine the program doing while the code runs.
(I explore this way of thinking in more detail in Working Code Is Not the Same as Understood Code, where notional machines make the invisible behaviour behind code easier to inspect.)
One tempting but incorrect picture places the interface between the variable and the object:
toaster ──▶ IToaster ──▶ CheapToasterBut the interface isn’t this middleman receiving the call and passing it along. The variable refers directly to the concrete object:
┌──────────────────────┐
│ toaster │
│ visible as IToaster │
│ reference ───────────┼──────▶ ● CheapToaster
└──────────────────────┘ _slice
_hot
Insert()
Eject()
IsHotFor a call such as toaster.Eject(), the compiler first checks whether IToaster makes that operation available. At runtime, the reference leads to the CheapToaster object, so CheapToaster.Eject() runs.
Get coding | seaphylla | omnivorous’s stories in your inbox
Join Medium for free to get updates from this writer.
The interface doesn’t sit at the end of the arrow. It determines which operations are visible through the variable. The concrete object at the tip of the arrow determines what those operations do.
Why not use CheapToaster directly?
At first, an interface can look like extra paperwork. Why describe a class you have already written?
Because the rest of the program probably does not care whether the toaster is cheap.
public sealed class BreakfastService
{
private readonly IToaster _toaster;public BreakfastService(IToaster toaster)
{
_toaster = toaster;
} public string MakeBreakfast(string slice)
{
_toaster.Insert(slice);
return _toaster.Eject();
}
}
BreakfastService asks for an IToaster, not a CheapToaster. You may give it either of these:
new BreakfastService(new CheapToaster());
new BreakfastService(new IndustrialToaster());Nothing inside BreakfastService changes, provided both classes honour the same contract. This is what people mean by programming against an interface. Code depends on the capability it needs rather than one particular implementation.
That doesn’t mean every class needs an interface. The separation is useful when implementations may change, several implementations already exist, or a test needs a controlled substitute.
The deliberately boring toaster
A test can use a fake that fits the same socket. This lets you test BreakfastService without depending on the behaviour of a real toaster.
public sealed class FakeToaster : IToaster
{
public bool IsHot { get; private set; }public void Insert(string slice)
=> IsHot = true; public string Eject()
{
IsHot = false;
return "test toast";
}
}
The fake does not inherit the machinery of CheapToaster. It simply makes the same promises. An interface lets otherwise unrelated classes share a usable shape without forcing them to share an implementation.
One object, several sockets
A concrete object is not limited to one interface. The same smart toaster might implement IToaster, IHeatable, and IDisposable.
Each interface presents a different view of that object. Code using IToaster can insert and eject bread. Code using IHeatable can control heat. Code using IDisposable can release resources when the appliance is no longer needed.
toaster : IToaster ─────────▶ ● SmartToaster
heater : IHeatable ────────▶ ●
cleanup : IDisposable ──────▶ ●These variables do not refer to three different appliances. They refer to the same concrete object through three different sockets. Each socket exposes only the operations relevant to that particular role.
This is why a class can implement several interfaces. A single object may offer several capabilities without every caller gaining access to everything it can do.
An abstract class serves a different purpose. It creates a family of related classes that may share state and implementation. A class can inherit from only one base class, but it can implement several interfaces.
Inheritance describes where an object comes from. Interfaces describe how other code is allowed to use it.
One final test
Trace the following code in your head. Picture how it operates and why.
IToaster toaster = new CheapToaster();var breakfast =
new BreakfastService(toaster);string result =
breakfast.MakeBreakfast("sourdough");
Now answer four questions.
- Which type does the compiler allow
BreakfastServiceto see? - Which concrete object does
toasterrefer to? - Whose
InsertandEjectmethod bodies run? - If
CheapToasteris replaced withFakeToaster, what must change insideBreakfastService?
If you can explain why those answers are true, you understand the interface. You are no longer looking only at code that works. You can see what the code means.
Like this article? For more deep dives into creative obsessions, literary inspirations, game development, philosophy, coding, AI, and other hyperfixations turned into writing, visit OnlyFangirling.










