Dependency Injection, Simply

Published Apr 18, 2018
Dependency Injection, Simply

I remember when I first started working on non-trivial software after graduating from college. In my ignorance, I couldn't comprehend why people were using some of these patterns, such as: dependency injection, inversion of control, MVVM, etc. I thought, 'Why are you adding so many layers? Those are just more points of failure!'

Sometimes the best teacher is experience. The need for abstraction began to make sense as I moved from larger project to larger project, and working with larger and larger teams. One of my favorite patterns has been dependency injection/inversion of control, and many other designs built on this concept.

You have just arrived at your first day of work in the IT department of a large fast food conglomerate, and your task is to build a recipe catalog service. This catalog service will be used by the customer facing nutrition information page, and it will be used in the kitchen to keep track of changes to the recipes.

New recipes will be added via this service, but we will have several different sources of data that we can pull from as well. Ten years ago somebody put together a spreadsheet, and twenty years ago, somebody put together a folder of text files.

That sounds like a lot of work!

It is, and we will not be looking at all of it in this post. We will simply look at how we may aggregate these different data sources.

A beginner might just be tempted to do something like this.

public class Recipes{
  public List<Recipie> SearchRecipies(string searchString){
    var matches = new List<Recipie>();
    
    var spreadSheetReader = new SpreadSheetReader("recipies.xls");
    foreach(var row in spreadSheetReader.GetRows()){
      if(row.contains(searchString)){
      	matches.Add(ExtractRecipieFromSpreadSheetRow(row);
      }
    }
    
    var textFileReader = new TextFileReader("recipies/*.txt");
    foreach(var file in textFileReader.GetFiles()){
     if(file.contains(searchString)){
      	matches.Add(ExtractRecipieFromSpreadSheetRow(row);
     }
    }
    
    var dbReader = new TableReader("companyDB","dbo.Recipies");
    foreach(var row in dbReader.GetFiles()){
     if(row.contains(searchString)){
      	matches.Add(ExtractRecipieFromDataRow(row);
     }
    }
    return matches;
  }
}

There is nothing wrong with the code above. Writing something like this absolutely makes sense in certain scenarios. However this way doesn't give us a lot of flexibility. There are several things we could do to enhance this code.

Seperate the instantiation from the execution

In the above example, we create the spreadsheet reader, text file reader, and db reader all inside the method we use to do the searching. That limits us to one search at a time. What if the text file reader created a file lock?

Lets try this a different way.

public class Recipes{
 private SpreadSheetReader _spreadSheetReader;
 private TextFileReader _textFileReader;
 private TableReader _tableReader;
 
 public Recipes(){
    _spreadSheetReader = new SpreadSheetReader("recipies.xls");
    _textFileReader = new TextFileReader("recipies/*.txt");
    _dbReader = new TableReader("companyDB","dbo.Recipies");
 }
 
 public List<Recipie> SearchRecipies(string searchString){
    var matches = new List<Recipie>();
    
    foreach(var row in _spreadSheetReader.GetRows()){
      if(row.contains(searchString)){
      	matches.Add(ExtractRecipieFromSpreadSheetRow(row);
      }
    }
    
    foreach(var file in _textFileReader.GetFiles()){
     if(file.contains(searchString)){
      	matches.Add(ExtractRecipieFromSpreadSheetRow(row);
     }
    }
    
    foreach(var row in _dbReader.GetFiles()){
     if(row.contains(searchString)){
      	matches.Add(ExtractRecipieFromDataRow(row);
     }
    }
  }
 
}

What does this get us, exactly?

The biggest thing this gets us is that now the service owns the resources instead of method call. If there was a cost associated to activating these resources, this is now only done once, instead of every time.

This is better, but it could be better still. It isn't very flexible. What if we had a new requirement added, now we need to read recipes in from emails. I think at some point, this poor class is doing too much.

Let the seperation of concerns begin!

// IRecipeSearcher.cs
public interface IRecipeSearcher{
  List<Recipe> SearchRecipes(string searchString);
}

// SpreadsheetRecipeSearcher.cs
public class SpreadsheetRecipeSearcher: IRecipeSearcher{
  private SpreadSheetReader _spreadSheetReader;
  public SpreadsheetRecipeSearcher(SpreadSheetReader reader){
    _spreadSheetReader = reader;
  }
  
  public List<Recipe> SearchRecipes(string searchString){
    var matches = new List<Recipie>();
    
    foreach(var row in _spreadSheetReader.GetRows()){
      if(row.contains(searchString)){
      	matches.Add(ExtractRecipieFromSpreadSheetRow(row);
      }
    }
    return matches;
  }
}

// TextFileRecipeSearcher.cs
public class TextFileRecipeSearcher: IRecipeSearcher{
  private TextFileReader _textFileReader;
  public TextFileRecipeSearcher(TextFileReader reader){
    _textFileReader = reader;
  }
  
  public List<Recipe> SearchRecipes(string searchString){ ... }
}

// DbRecipeSearcher.cs
public class DbRecipeSearcher: IRecipeSearcher{
  ...
}

Yikes! That's a lot of code. That's way more code than the first two examples, how is this better?

Consider the Recipes class. What will that look like now?

//Recipes.cs
public class Recipes{
  private List<IRecipeSearcher> _searchers;
  public Recipes(List<IRecipeSearcher> searchers){
    _searchers = searchers;
  }
  public List<Recipie> SearchRecipies(string searchString){
    var matches = new List<Recipie>();
    foreach(var searcher in _searchers){
      matches.AddRange(searcher.SearchRecipes(searchString);
    }
    
    return matches;
  }
}

//Somewhere in Program.cs
var recipeService = new Recipes(new List<IRecipeSearcher>{
 new SpreadsheetRecipeSearcher(...),
 new TextFileRecipeSearcher(...),
 new DbRecipeSearcher(...)
});

return recipeService.SearchRecipies("french toast");

Drastically smaller, isn't it? We could go further, but I think this proves the point.

  • We now have greater separation of concern. Each searcher is now separated into its own file. It's concerned with reading the resource in and returning the data.

  • The Recipes class is now smaller, and will not change no matter how many new searchers are added. In fact, the searchers are... wait for it... injected into the service.

  • Another topic for another time, but unit testing is now much easier. If you wanted to test the first way we tried to write this, you would need to construct a test spreadsheet file, a test folder with test recipes in it, and a test recipe database! Now, you can create a mock IRecipeSearcher and test against that.

Thanks for reading! I hope that was a useful demonstration of dependency injection. I look forward to writing many more articles.

Discover and read more posts from Joel Longanecker
get started