Have questions about buying, selling or renting during COVID-19? Learn more

Zillow Tech Hub

The Benefits of Dependency Injection in Dynamic Languages

Introduction

This post will focus on a specific way of implementing one of the SOLID principles, Dependency Inversion. First, we should define Dependency Inversion. There are two major parts:

  1. High-level code should not depend on lower level code, both should depend upon abstractions.
  2. Abstractions should not depend on details, instead, details should depend on abstractions

A lot of people might be familiar on how to implement this in a statically-typed environment. You define an interface, and require that interface as an argument (inject it) to the class/method where you have a dependency you’re attempting to invert. Take the following code as an example:

Hard Dependency in UserSaver Interactor (Java):

Inverted Dependency in UserSaver Interactor:

Here are some benefits of this approach:

  1. Our code depends on an abstraction (easier to change implementation details without messing around with unrelated code).
  2. Our code can be injected with mocked dependencies in unit tests (we’ll still test that the layers communicate properly in our integration tests). This will allow for a major speed increase in our tests (no network or DB calls!)

Dynamic Languages

“Great”, you might say, “but I code in a dynamically typed language, I have no explicit interfaces. How am I supposed to do this?!”.

The answer: duck typing.

When a method/class in a statically typed language requires an object, it defines its type at compile time. This allows us to invert our dependencies at compile time; we can require abstractions instead of implementations to our methods (via type annotations pointing to interfaces instead of concrete classes).

In a language like Ruby, though, this happens automagically! When we define a new dependency to be injected into a class, that dependency is an abstraction, it has no concrete type until runtime, and it can be any type as long as it responds to the methods that are called on it in that scope (an implicitly defined interface).

This means that when you define a new method argument, that really is like defining an interface that requires all the methods/properties that are called on it in that scope.

As an example, take a look at what looks to be a pretty standard Ruby method:

This method can be rewritten in a more generic/reusable way:

And an equivalent Java interface would be defined and used as such:

The point here is that while interfaces don’t exist in dynamically typed languages, we get very similar functionality via non-type annotated method arguments.

This all means that by injecting our dependencies into our code, instead of calling concrete implementations directly, our code goes from depending on an implementation, to depending on an abstraction. In other words, by utilizing Dependency Injection, we achieve Dependency Inversion for free!

Rails Example

Here’s a super simple Rails example of Dependency Injection and its benefit.

A normal AR object:

and an Interactor to fetch an AR object from the DB:

Here’s this cleaned up with some Dependency Injection:

Here’s us injecting a mocked User in a unit test:

You need to mock out all methods that are used in the UserFetcher on the passed in user_datastore_class (practically just adhere to the interface).

There’s the magic! Your unit tests are now truly just testing the logic in that class, and as an added benefit, your tests will run a lot quicker (not hitting the DB anymore)!

Conclusion

A quick note, in Ruby, and most dynamic languages, you can set a default to these arguments. You still get the benefit of quicker tests and decoupling, while also getting the benefit of not having to manually pass in the arguments to every method call.

A major downside to utilizing default arguments though, is it does break the Dependency Inversion Principle, “Abstractions should not depend on details, instead, details should depend on abstractions”. Setting a default state for an abstraction is causing said abstraction to depend on a detail, so I would stay away from default arguments for injected dependencies.

In conclusion, Dependency Inversion is just as important to adhere to in a dynamically typed environment, and is very easy to achieve by utilizing Dependency Injection. It requires less code than in a statically typed environment, as is normally the case in a dynamically typed environment, but can lead to the same benefits in testing and code reusability.

 

The Benefits of Dependency Injection in Dynamic Languages