Do we need interfaces for dependency injection?

2020-02-26 04:28发布

问题:

I have an ASP.NET Core application. The application has few helper classes that does some work. Each class has different signature method. I see lot of .net core examples online that create interface for each class and then register types with DI framework. For example

 public interface IStorage
 {
    Task Download(string file);
 }

 public class Storage
 {
    public Task Download(string file)
    {
    }
 }

 public interface IOcr
 {
     Task Process();
 }

 public class Ocr:IOcr
 {
    public Task Process()
    {

    }
 }

Basically for each interface there is only one class. Then i register these types with DI as

 services.AddScoped<IStorage, Storage>();
 services.AddScoped<IOcr,Ocr>();

But i can register type without having interfaces so interfaces here look redundant. eg

 services.AddScoped<Storage>();
 services.AddScoped<Ocr>();

So do i really need interfaces?

回答1:

No, you don't need interfaces for dependency injection. But dependency injection is much more useful with them!

As you noticed, you can register concrete types with the service collection and ASP.NET Core will inject them into your classes without problems. The only benefit you get over simply creating instances with new Storage() is service lifetime management (transient vs. scoped vs. singleton).

That's useful, but only part of the power of using DI. As @DavidG pointed out, the big reason why interfaces are so often paired with DI is because of testing. Making your consumer classes depend on interfaces (abstractions) instead of other concrete classes makes them much easier to test.

For example, you could create a MockStorage that implements IStorage for use during testing, and your consumer class shouldn't be able to tell the difference. Or, you can use a mocking framework to easily create a mocked IStorage on the fly. Doing the same thing with concrete classes is much harder. Interfaces make it easy to replace implementations without changing the abstraction.



回答2:

Does it work? Yes. Should you do it? No.

Dependency Injection is a tool for the principle of Dependency Inversion : https://en.wikipedia.org/wiki/Dependency_inversion_principle

Or as it's described in SOLID

one should “depend upon abstractions, [not] concretions."

You can just inject concrete classes all over the place and it will work. But it's not what DI was designed to achieve.



回答3:

I won't try to cover what others have already mentioned, using interfaces with DI will often be the best option. But it's worth mentioning that using object inheritance at times may provide another useful option. So for example:

public class Storage
 {
    public virtual Task Download(string file)
    {
    }
 }


public class DiskStorage: Storage
 {
    public override Task Download(string file)
    {
    }
 }

and registering it like so:

services.AddScoped<Storage, DiskStorage>();


回答4:

No, you don't need interfaces. In addition to injecting classes or interfaces you can also inject delegates. It's comparable to injecting an interface with one method.

Example:

public delegate int DoMathFunction(int value1, int value2);

public class DependsOnMathFunction
{
    private readonly DoMathFunction _doMath;

    public DependsOnAFunction(DoMathFunction doMath)
    {
        _doMath = doMath;
    }

    public int DoSomethingWithNumbers(int number1, int number2)
    {
        return _doMath(number1, number2);
    }
}

You could do it without declaring a delegate, just injecting a Func<Something, Whatever> and that will also work. I'd lean toward the delegate because it's easier to set up DI. You might have two delegates with the same signature that serve unrelated purposes.

One benefit to this is that it steers the code toward interface segregation. Someone might be tempted to add a method to an interface (and its implementation) because it's already getting injected somewhere so it's convenient.

That means

  • The interface and implementation gain responsibility they possibly shouldn't have just because it's convenient for someone in the moment.
  • The class that depends on the interface can also grow in its responsibility but it's harder to identify because the number of its dependencies hasn't grown.
  • Other classes end up depending on the bloated, less-segregated interface.

I've seen cases where a single dependency eventually grows into what should really be two or three entirely separate classes, all because it was convenient to add to an existing interface and class instead of injecting something new. That in turn helped some classes on their way to becoming 2,500 lines long.

You can't prevent someone doing what they shouldn't. You can't stop someone from just making a class depend on 10 different delegates. But it can set a pattern that guides future growth in the right direction and provides some resistance to growing interfaces and classes out control.

(This doesn't mean don't use interfaces. It means that you have options.)