DEV Community

Jill
Jill

Posted on

Deep Impact with Dependency Injections

If you've ever filled out a tax return, the concept of Dependency Injection should be fairly easy to comprehend, even if the semantics are vague.

On US tax forms, there's a section to fill out for "dependents", people (usually children) that rely upon the tax payer for their own livelihood. This is an excellent example of how dependency injection works and why it's so important to use in our applications.

Dependency Injection (DI) is simply when one object supplies the dependencies of another object, and a dependency is just an object that can be utilized throughout an app.

Alt Text

CLASSES

There are three very important parts classes in the DI pattern. The service object, to be used, the client object that depends on the service, and the injector, which is responsible for constructing the service and injecting it into the client.

By creating objects directly within the class that requires the objects, our code becomes tightly-coupled and inflexible. Because the class is dependent on specific objects, it becomes impossible to change any instantiation later without having to refactor the entire class.

The class then cannot be reused if other objects are required, finally making it hard to test because of it's heavy reliance on real objects that cannot be mocked.

DI provides a solution to these many issues by creating objects, understanding which classes require those objects, and then providing those classes with the objects.

Wikipedia provides a helpful analogy:

"Service - an electric, gas, hybrid, or diesel car
Client - a driver who uses the car the same way regardless of the engine
Injector - the parent who bought the kid the car and decided which kind"

Why DI

Stay DRY

By following the principles of DI we effectively can reduce boilerplate code since all work to structure and initialize the dependencies is handled in a single component.

Separation of Concerns

One of the most important pros of using DI is the separation of concerns when building components. The biggest trade off here would be that DI can make code difficult to trace because it separates client behavior from construction. However, with good variable and file names, module co-ordination will be far easier to navigate than one file with hundreds to thousands of lines of code.

Parallel Programming & Unit Testing

When components create their own dependencies, there is no way to effective way to test their functionality. In the example below, the Coupe object depends on both the Engine and Wheels Object, while the Engine object is dependent on the Pistons object. This is called "Tightly Coupled Code", and can become very problematic.

function Wheels() {
  this.action = () => log("We're rollin' now!");
}

function Pistons() {
  this.action = () => log("Pew! Pew! Pew!");
}

function Engine() {
  this.pistons = new Pistons();
  this.action = () => {
    this.pistons.action();
  };
  log("Hummmmmmmm.");
}

function Coupe() {
  this.wheels = new Wheels();
  this.engine = new Engine();
  this.action = () => {
    this.wheels.action();
    this.engine.action();
    log("Nice car lady!");
  };
  log("Fresh off the lot.");
}
Enter fullscreen mode Exit fullscreen mode

Should any of Pistons object happen to fail, both the Engine and the Coupe would fall apart, this can lead to spaghetti code and hard to find bugs.

The solution is to loosen the couplings of the code and pass the dependency as a parameter to the client object.

function Engine(pistons) {
  this.pistons = pistons;
  this.action = () => {
    this.pistons.action();
    log("Hummmmmmm.");
  };
  log("Fresh off the lot.");
}

function Car(wheels, engine) {
  this.wheels = wheels;
  this.engine = engine;
  this.action = () => {
    this.wheels.action();
    this.engine.action();
    log("Nice car lady!");
  }
  log("Fresh off the lot.");
Enter fullscreen mode Exit fullscreen mode

While the code above is a better method, it can still lead to coupling and scope issues. To truly separate our concerns, the best way is to isolate them completely, and keep our clients from knowing anything about eachother, which can be achieved with Inversion of Control.

Inversion of Control (IoC)

DI is a broad technique used to achieve IoC, which a concept that operates on the belief that an object should not configure its dependencies statically, but should be configured by some other class externally.

To make it relatable, let's say you're going on vacation and plan to stay at a nice hotel or resort. It's not your responsibility to bring towels, sheets, the phone in the room etc.

Alt Text

The hotel provides the amenities so you can do the one thing you came to do: enjoy your vacation. IoC follows this same principle to decouple our code, by isolating the dependency and then passing it to client objects on demand. This method maintains those important connections between service and client objects, and effectively extend the app's functionality.

Containers

The use of a service within a DI or 'unity' container is popular in many frameworks, and Angular JS utilizes this method extensively. This method effectively implements IoC by separating the construction concerns and use concerns of this video player app by creating a service, and then passing it to the instance, which uses it throughout the app as props.

angular.module('video-player')
  //create a new service to act as an DI container
  .service('youTube', function ($http) {
    this.search = function (query, cb) {
      //utilize the $http object
      return $http({
        //specify the request method
        method: 'GET',
        //request the proper endpoint
        url: 'https://www.googleapis.com/youtube/v3/search',
        //params required for angularjs https requests
        params: {
          key: `${YOUTUBE_API_KEY}`,
          q: `${query}`,
          part: 'snippet',
          maxResults: 5,
          type: 'video',
          videoEmbeddable: true,
        }
      }).then(function successCB(response) {
        console.log(response, 'here's that video you asked for!');
        cb(response.data.items);
      }).catch(function errorCB(response) {
        console.error(response, 'sorry, couldn't get that video for you');
      });
    };
  });
Enter fullscreen mode Exit fullscreen mode

DI Containers are so useful because dependencies are defined in a single place instead of throughout your app, and the components that depend on them can easily request data from them without knowledge of any other dependents.

angular.module('video-player')
  .component('app', {
    //pass the youTube service to the app controller to pass down it's response
    controller: function (youTube) {
      //mock videos 
      this.videos = window.exampleVideoData;
      //this selects the first video from the response
      this.currentVideo = window.exampleVideoData[0];
      //onSearched will call the youtube search function on a queried string
      this.onSearched = (query) => {
        //the youtube search takes a query and a callback
        youTube.search(query, this.afterSearch);
        //console.log('video queried!');
      };
      //helper function to re-render the video player and list to the updated queried item's top 5 results
      this.afterSearch = (videos) => {
        //reassign the queried video results to the videos attribute
        this.videos = videos;
        //reassign the first video to the current video in the player
        this.currentVideo = videos[0];
      };
      //select video will grab the index from the array and render selected video to the main player
      this.selectVideo = (index) => {
        //updates the current video to the plucked index video;
        this.currentVideo = this.videos[index];
      };
    },
    templateUrl: 'src/templates/app.html'
  });
Enter fullscreen mode Exit fullscreen mode

In Conclusion

allow us to stay DRY, separate our concerns, extends and uncouples our code while still effectively passing data to our client objects that need them. Use the DI principle whenever you want to implement parallel programming in your code and look forward to better and more successful unit testing in your next application!

Alt Text

Thanks for reading!

Top comments (0)