Design Pattern Discussions #1: Dependency Injection 💉

by Charlie Taylor

October 6, 2022 • 7 min read

Do you need your services now? Do you need them to be able to be swapped for others with a shared interface? Are you tired of instantiating everything where and when you need it? Well, there’s already a solution to this called dependency injection!

But what does that mean?

Dependencies

To know what we’re talking about, we need to define what we’re talking about. In this case, a dependency is simply a class that offers functionality to another.

Example:

Class A creates, retrieves, updates, and deletes (CRUD) posts on a specific arcade game forum.

But when we want to retrieve those posts, we also want to retrieve comments made on it.

Class B handles the CRUD for comments.

If Class A wants to send off a post with all of its comments, it must use some functionality from Class B, making Class B a dependency of Class A

That makes sense, we should be able to use our code from anywhere so we can reuse already written tools. So let’s take a look at this with some example code. (We’ll be looking at C#)

public class A
{
 private B bService;
 public A()
 {
   bService = new B();
 }
 
 //...
}

Not bad so far, we have A and it can use B! The real world is not so forgiving though, A and B are sure to have more dependencies right? We need to call the database to get the posts and comments, as well as the users who are making them. Alright, let’s see how that looks.

public class A
{
 private B bService;
 private PostRepository postRepository;
 private UserRepository userRepository;
 
 public A()
 {
   postRepository = new PostRepository();
   userRepository = new UserRepository();
   bService = new B();
 }
 
 // ...
}
 
public class B
{
 private CommentRepository commentRepository;
 private UserRepository userRepository;
 
 public B()
 {
   commentRepository = new CommentRepository();
   userRepository = new UserRepository();
 }
 
 // ...
}

And what if we have something that needs even more dependencies?

public class CompanyService
{
   private AuthenticatedUserModelProvider _authenticatedUserModelProvider;
   private CompanyRepository _companyRepository;
   private CompanyViewsRepository _companyViewsRepository;
   private CustomFieldDefinitionService _customFieldDefinitionService;
   private Mapper _mapper;
   private UserAccountService _userAccountService;
   private SearchlightEngine _searchlightEngine;
   private CodeDefinitionService _codeDefinitionService;
   private CacheClient _cache;
   private CacheInvalidator _cacheInvalidator;
   private Logger<CompanyService> _logger;
   private Validator<CompanyModel> _validator;
 
 public CompanyService()
 {
     _authenticatedUserModelProvider = new AuthenticatedUserModelProvider(...);
     _companyRepository = new CompanyRepository(...);
     _companyViewsRepository = new CompanyViewsRepository(...);
     _mapper = new Mapper(...);
     _searchlightEngine = new SearchlightEngine(...);
     _customFieldDefinitionService = new CustomFieldDefinitionService(...);
     _userAccountService = new UserAccountService(...);
     _codeDefinitionService = new CodeDefinitionService(...);
     _cache = new CacheClient(...);
     _cacheInvalidator = new CacheInvalidator(cacheName);
     _logger = new Logger<CompanyService>(...);
     _validator = new Validator<CompanyModel>(...);
 }
}

Oh… this is getting a bit complicated! Imagine if all of the ellipses were more classes being instantiated to allow this CompanyService to be made, it would be a total mess. And what if we wanted to use a different cache or mapper? Would we have to change this code to allow other ones to be used?

What if we had a separate tool that provided these dependencies to A, B, and the CompanyService? That’s exactly what dependency injection (DI) sets out to do. At runtime, A will ask DI for B, the PostRepository, and the UserRepository. It is DI’s job to construct these classes and provide them, rather than the classes themselves needing to worry about managing the dependencies of the dependencies. This idea that the class should not worry about managing dependencies adheres to the idea of Inversion of Control.

An example of a dependency injection is constructor injection, where the classes made by DI are passed to the class’s constructor. This is done by having interface parameters for the class that can allow any class that implements the interface to be injected into it.

What is an interface?

Also called an abstract class, an interface is just a type definition that can not be directly instantiated like a normal class. It is only to define what methods/members will be in a class. In C#, it is good practice to name interfaces starting with a capital I to denote that it is an Interface.

Example:

public interface IQuadrilateral
{
 public double Area();
 public double Perimeter();
}
 
public Square : IQuadrilateral
{
 private double side;
 
 //...
 
 public double Area()
 {
   return side * side;
 }
 
 public double Perimeter()
 {
   return side * 4;
 }
}
 
public Rectangle : IQuadrilateral
{
 //...
}

This may sound a little confusing, so let’s take a look at A and B when using constructor dependency injection, rather than instantiating classes.

public class A
{
 private IBService bService;
 private IPostRepository postRepository;
 private IUserRepository userRepository;
 
 public A(IBService b, IPostRepository postRepo, IUserRepository userRepo)
 {
   bService = b;
   postRepository = postRepo;
   userRepository = userRepo;
 }
 
 // ...
}
 
public class B : IBService
{
 private ICommentRepository commentRepository;
 private IUserRepository userRepository;
 
 public B(ICommentRepository commentRepo, IUserRepository userRepo)
 {
   commentRepository = commentRepo;
   userRepository = userRepo;
 }
 
 // ...
}

Just like the previous example of A and B, both of these classes need dependencies but now have them coming as parameters in the constructor. At runtime, DI figures out what classes need to be injected based on the interface. This is also convenient since the UserRepository can be made once and passed along to both A and B.

As seen in the interface example as well, what if we have an interface with multiple implementations that needs to be injected? This makes interchanging classes easy, like if we have an IFood interface, we can inject a Sushi or CornDog class.

There is more to be learned about dependency injection, and these examples are just one way of implementing it. Check out some of these sources for more information and how to use it:

https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection
https://www.tutorialsteacher.com/ioc/dependency-injection

Tune in for Part 2 of Design Pattern Discussions (DPD) coming soon(ish)!

Read as TXT: /blog/599.txt