Implementing constructor injection – Dependency Injection

At this point, you should be familiar with constructor injection. Nonetheless, next is the controller’s code after migrating the Service Locator pattern to constructor injection:

namespace ServiceLocator;
public class ConstructorInjectionController : ControllerBase
{
    private readonly IMyService _myService;
    public ConstructorInjectionController(IMyService myService)
    {
        _myService = myService ??
throw new ArgumentNullException(nameof(myService));
    }
    [Route(“/constructor-injection”)]
    public IActionResult GetUsingConstructorInjection()
    {
        _myService.Execute();
        return Ok(“Success!”);
    }
}

When using constructor injection, we ensure that IMyService is not null upon class instantiation. Since it is a class member, it is even less tempting to call its Dispose() method in an action method, leaving that responsibility to the container (as it should be).Let’s analyze the code before moving to the next possibility:

  • We implemented the strategy pattern with constructor injection.
  • We added a guard clause to ensure no null value could get in at runtime.
  • We simplified the action to the bare minimum.

Both techniques are an acceptable replacement for the Service Locator pattern.

Implementing a minimal API

Of course, we can do the same with a minimal API. Here is the code of that endpoint:

app.MapGet(“/minimal-api”, (IMyService myService) =>
{
    myService.Execute();
    return “Success!”;
});

That code does the same as the method injection sample without the guard clause that I omitted because no external consumer will likely inject nulls into it: the endpoint is a delegate passed directly to the MapGet method.Refactoring out the Service Locator pattern is often as trivial as this.

Conclusion

Most of the time, by following the Service Locator anti-pattern, we only hide that we are taking control of objects instead of decoupling our components. The code sample demonstrated a problem when disposing of an object, which could also happen using constructor injection. However, when thinking about it, it is more tempting to dispose of an object that we create than one we inject.Moreover, the service locator takes control away from the container and moves it into the consumer, against the Open-Closed Principle. You should be able to update the consumer by updating the composition root’s bindings.In the case of the sample code, we could change the binding, and it would work. In a more advanced case, binding two implementations to the same interface would be tough when contextual injection is required.

The IoC container is responsible for weaving the program’s thread, connecting its pieces together where each independent piece should be as clueless as possible about the others.

On top of that, the Service Locator pattern complicates testing. When unit testing your class, you must mock a container that returns a mocked service instead of mocking only the service.One place where I can see its usage justified is in the composition root, where bindings are defined, and sometimes, especially when using the built-in container, we can’t avoid it to compensate for the lack of advanced features. Another good place would be a library that adds functionalities to the container. Other than that, try to stay away!

Beware

Moving the service locator elsewhere does not make it disappear; it only moves it around, like any dependency. However, moving it to the composition root can improve the maintainability of that code and remove the tight coupling.

Next, we revisit our third and final pattern of this chapter.

Leave a Reply

Your email address will not be published. Required fields are marked *



         


          Terms of Use | Accessibility Privacy