Okay, so let's start from the world where you use class types as your injected dependency types.
public class MyDependency {}
public class MyService
{
public MyService(MyDependency myDependency)
{
//...
}
}
You're absolutely correct that this works just as well in terms of the real production code, assuming that that class adequately covers all the possible implementations of that dependency.
And even if multiple classes exist, you could still let those inherit from the same base class instead of the same interface, so you'd still be using class-based injection.
public class MyDependency {}
public class MyDependencyOne : MyDependency {}
public class MyDependencyTwo : MyDependency {}
public class MyService
{
public MyService(MyDependency myDependency)
{
//...
}
}
Production code wise, all good. But we also want to write tests. Specifically, when I call MyService.UploadUser(user)
, I want to make sure that MyService
correctly combines the first and last name of the user and sends that as a single string to the dependency (which you can imagine to be a database repository).
Clearly, my unit test is going to use a real MyService
object. But I don't actually want to upload real data to a real database, and that's what MyDependency
precisely does in this scenario. Since the constructor for MyService
demands a MyDependency
, I can't avoid needing to supply a real MyDependency
.
It would be a lot better if I could inject a fake MyDependency
, sort of a secret agent that pretends to be the real thing but does not actually talk to the database and instead just tracks what it's being told.
But the MyService
code should be written in a way that it works agnostic of getting a real/fake dependency. I'm referring to the type of the injected dependency here. So the question becomes: can we achieve this using a class type?
There's no shared logic between the real and fake dependency, so there's no benefit to inheritance. The only thing that the real and fake dependency share is that they have the same public interface (hint hint), their internals are completely different.
public interface IMyDependency
{
void SaveUserName(string fullName);
}
public class MyRealDependency : IMyDependency { /* same code as before */ }
public class MyFakeDependency : IMyDependency
{
// Snitch list which captures all values sent to the secret agent
public List<string> CapturedValues { get; } = new();
public void SaveUserName(string fullName)
{
this.CapturedValues.Add(fullName);
}
}
public class MyService
{
public MyService(MyDependency myDependency)
{
//...
}
}
When you look at MyFakeDependency
, you can see that it collects and records anything that is sent to it. This means I can later check what was sent to it, and this is something I can use in my tests. For example:
public void Concatenates_first_and_last_name_of_user()
{
var user = new User("Rick", "Sanchez");
var fakeDependency = new MyFakeDependency();
var service = new MyService(fakeDependency);
service.UploadUser(user);
// Confirm that MyService did call IMyDependency.SaveUserName
fakeDependency.CapturedValues.Should().HaveCount(1);
// Confirm that the sent name is first + last name
fakeDependency.CapturedValues[0].Should().Be("Rick Sanchez");
}
An interface
is, in a very oversimplified way to put it, a base class which only allows you to define public signatures (methods and properties), but not implementations (method bodies).
Specifically in C#, you could also use an abstract class
to the same effect as an interface
here. However, C# does not allow multiple inheritance. A class can only inherit one abstract base class, but it can implement ("inherit") as many interfaces as it likes.
Because of this, in cases where you do not need any shared implementation logic (method bodies), an interface
is a better choice over an abstract class
.
Ask yourself how you would've done it using a class-type as the injected dependency. You'll find that you need to define an implementationless base class that covers both the fake and real dependency, and that's precisely what an interface
is. Can you do it with a class on a technical level? Sure. But you're really just using it as a pseudo-interface
.