Polymorphism with Interfaces

An interface can be a variable's, parameter's or list's type, so it can hold objects of unrelated classes that keep the same contract. Depending on an interface, such as IClock, lets a test pass any fake that implements it.

Freelancer · C# · OOP · Updated

After this lesson you can

  • Hold objects of different classes in an interface variable or list, and call only the interface's members
  • Inject a dependency through an interface, so a test can pass any fake that implements it

Key takeaways

  • An object fits in a variable of any interface its class implements, and the variable's type decides which members you can call.
  • Unrelated classes that keep the same contract, such as Teenager and ProAthlete, fit in one List<IAthlete>.
  • Ask for a dependency through an interface, such as IClock, so the app can pass a SystemClock and a test a FixedClock.
  • In Program.cs, AddScoped<IProductDao> decides which class keeps the contract that a controller asks for.

In Changing Roles, Chloe was a student in class, a singer at rehearsal and an athlete at volleyball. Each role saw only the part of her it needed. Interfaces put those roles into code.

A role is an interface

Each role becomes an interface, and Chloe's class implements all three:

Teenager chloe = new Teenager("Chloe");

IStudent student = chloe;
student.Study();

IAthlete athlete = chloe;
athlete.Train();

public interface IStudent
{
    void Study();
    void TakeTest();
}

public interface ISinger
{
    void Rehearse();
    void Perform();
}

public interface IAthlete
{
    void Train();
    void Compete();
}

public class Teenager : IStudent, ISinger, IAthlete
{
    public string Name { get; }

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

    public void Study() => Console.WriteLine(quot;{Name} studies for the math test");
    public void TakeTest() => Console.WriteLine(quot;{Name} takes the test");
    public void Rehearse() => Console.WriteLine(quot;{Name} rehearses her lines");
    public void Perform() => Console.WriteLine(quot;{Name} sings her solo");
    public void Train() => Console.WriteLine(quot;{Name} practices her serve");
    public void Compete() => Console.WriteLine(quot;{Name} plays the match");
}
Chloe studies for the math test
Chloe practices her serve

A Teenager is an IStudent, an ISinger and an IAthlete, so it can go in a variable of any of those types. And the variable's type decides what you can ask, just as with base classes. Through student, the code only knows a student:

Teenager chloe = new Teenager("Chloe");

IStudent student = chloe;
student.Train();

public interface IStudent
{
    void Study();
}

public interface IAthlete
{
    void Train();
}

public class Teenager : IStudent, IAthlete
{
    public string Name { get; }

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

    public void Study() => Console.WriteLine(quot;{Name} studies");
    public void Train() => Console.WriteLine(quot;{Name} trains");
}
error CS1061: 'IStudent' does not contain a definition for 'Train' and no accessible extension method 'Train' accepting a first argument of type 'IStudent' could be found

Chloe can still train. But not every student can, so code that only knows a student can't ask.

Unrelated classes, one list

Here's what interfaces give you that base classes can't. The volleyball coach needs a team of athletes. Chloe is one, and so is Maya, a professional player who isn't a teenager at all. ProAthlete and Teenager share no base class, yet both keep the IAthlete contract, so both fit in one List<IAthlete>:

List<IAthlete> team = new List<IAthlete>
{
    new Teenager("Chloe"),
    new ProAthlete("Maya"),
    new Teenager("Sam")
};

foreach (IAthlete player in team)
{
    player.Train();
}

public interface IStudent
{
    void Study();
}

public interface IAthlete
{
    void Train();
}

public class Teenager : IStudent, IAthlete
{
    public string Name { get; }

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

    public void Study() => Console.WriteLine(quot;{Name} studies");
    public void Train() => Console.WriteLine(quot;{Name} practices after school");
}

public class ProAthlete : IAthlete
{
    public string Name { get; }

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

    public void Train() => Console.WriteLine(quot;{Name} trains with her coach at 6 AM");
}
Chloe practices after school
Maya trains with her coach at 6 AM
Sam practices after school

The coach's loop doesn't know who studies chemistry or who gets paid to play. It treats everyone as an athlete, and each one trains in their own way. A base class could only gather classes from one family tree; an interface gathers any class that can do the job.

Back to dependency injection

In Dependency Injection, the Greeter asked for a Clock in its constructor, and tests passed a FixedClock that derived from it. That worked, but it needed two things: Clock.Hour had to be virtual, and the fake inherited a real clock it never used.

An interface says exactly what the greeter needs, and nothing more: something that knows the hour.

Greeter morning = new Greeter(new FixedClock(9));
Greeter now = new Greeter(new SystemClock());

Console.WriteLine(morning.Greet("Ada"));
Console.WriteLine(now.Greet("Ada").StartsWith("Good"));

public interface IClock
{
    int Hour { get; }
}

// The real clock, for the app.
public class SystemClock : IClock
{
    public int Hour => DateTime.Now.Hour;
}

// A fake clock, for tests: it's always the hour it was given.
public class FixedClock : IClock
{
    private readonly int hour;

    public FixedClock(int hour)
    {
        this.hour = hour;
    }

    public int Hour => hour;
}

public class Greeter
{
    private readonly IClock clock;

    public Greeter(IClock clock)
    {
        this.clock = clock;
    }

    public string Greet(string name)
    {
        return clock.Hour < 12 ? quot;Good morning, {name}!" : quot;Good afternoon, {name}!";
    }
}
Good morning, Ada!
True

Greeter now depends on a contract, not on any class. The app passes a SystemClock, a test passes a FixedClock, and the two classes are equals: neither derives from the other, and nothing needs to be virtual.

This is how professional .NET code is written. A controller asks for an IProductDao, not a ProductDao, and Program.cs decides which class keeps that contract:

builder.Services.AddScoped<IProductDao>(_ => new ProductDao(connectionString));

The controller works with the real DAO in the app, and a test can give it a fake DAO with a few products in a list, with no database at all.

Check yourself

2 questions to try on the lesson page.

So what

An interface is a role. Code that asks for a role, an IAthlete, an IClock, an IProductDao, works with every class that can play it: today's, tomorrow's, and the fakes in your tests.

Key terms

Cheat sheets