Static Dependency Access

Jul 23, 2018
-
8
minute read

There has always been a lot of controversy over the right ways to access dependencies. Should they be injected? Located? Directly referenced? Magically resolved? Inverted? Context objects? Everyone tends to speak very dogmatically about how they think you should or shouldn’t access your dependencies. Statically accessing your dependencies is a very powerful technique. You should absolutely use it, but you must avoid its many perils.

Recently, there was a heated discussion over when global variables should or shouldn’t be used. There are certainly dangers with global variables, global state, and global access, but they are also very powerful tools when used correctly. A good engineer knows when and where to use a tool. In order to illustrate my point, let’s walk through a short development cycle together, and watch a frontend-oriented codebase evolve.

The Development Cycle

Suppose you have an application which plays different background music in each scene.

The conventional wisdom says that you should do all of these things:

Create an abstract interface for the MusicPlayer, to support different implementations

Register an instance of the MusicPlayer in your composition root

Explicitly inject the instance of MusicPlayer into each object who needs it

The goal of the conventional wisdom is this:

Use clean abstractions to ease testing,

Use abstractions to ease changing behavior (like not playing music when debugging)

Make dependency usage clear

Make the composition root clearly declare the configuration of your application

This feels good at first. You write all the code. You inject the MusicPlayer into each scene when you instantiate him. You get a nice dopamine hit in your brain as you know that this code follows the best practices. It’s elegant. It’s perfect.

So, you keep developing. The next feature you are working on has some visuals. Each scene needs to have a different background image. That sounds simple enough. Let’s pull in a SpriteBatch and start drawing images.

You update all of your scenes, make their constructors bigger, store the SpriteBatch in a field, register him in the composition root, and inject him into every single scene. It took a bit more work than you expected, but hey, everyone knows that sometimes writing code the right way takes a bit of work. “This is just the price of doing things the right way”, you tell yourself. However, doubt starts to grow at the back of your brain. Too small to acknowledge, but just enough to make you think, “Should it be this hard to add something that I already know I need just about everywhere?”

The next ticket is waiting for you in Up Next, you quickly assign it to yourself and mark it In Progress. It’s time to add navigation to all your scene. Users will want to click buttons or perform actions that will take them from one scene to another. No problem.

You update all of your scenes, make their constructors bigger, store the navigation in a field, register your Navigation instance in the composition root, and inject him into every single scene. Now, the work isn’t particularly hard, but it is extremely verbose, tedious, and repetitive. The small doubt becomes a full-fledged thought, “Is there a better way to do this?”

A Better Way

Once the game is all wired up, the rest of everything happens in the scenes. They all need generally the same sorts of dependencies. The usage flow of the application looks like this:

1) Init Game Application2) Navigate User to Main Menu3) User Initializes a Game Instance (New Game/Load/Continue)4) User Plays Game

As long as the application only actually uses one instance of MusicPlayer, SpriteBatch, and Navigation, and as long as those are wired up before the User reaches the Main Menu, the application will function correctly, in all scenes. Realistically, it doesn’t make a lot of sense to change the way one plays sounds (use the OS Audio Devices) or display visuals (use the OS Video Devices). Some things will change (resolution, FPS, windowed/fullscreen, volume, sound/music balance, subtitles, etc), and some things just won’t.

There is a better way than following the conventional wisdom of constructor injection. Access your general dependencies statically. The codebase looks like this instead.

This is a far more ergonomic design. Now, creating a new scene won’t involve all of the boilerplate of bringing in the MusicPlayer to play the background music. Having scenes do more things doesn’t require massive changes rippling through the whole codebase to wire in the new dependency.

It also still accomplishes all of the goals of following the conventional constructor injection wisdom.

Use clean abstractions to ease testing

Use abstractions to ease changing behavior

Make dependency usage clear

Make the composition root clearly declare the configuration of your application

What Pitfalls Should Be Avoided With Static Dependencies?

Don’t make a dependency statically available until enough classes need it

Don’t allow a dependency to be changed after application wireup

Ergonomics is a critical API trait. Your software can be well-designed and follow the “right” principles, while still being cumbersome and verbose. Learning where and when to adapt and make concessions for syntax and usability is a key skill to develop. Sometimes providing static accessors for instance dependencies is the best choice. Look for patterns of duplication and verbosity in your code and let them guide you to designs that are more ergonomic. Don’t be afraid to adapt things and make them more usable for your team and your software project.