Dependency Management in Rails

Ben and Joe dive into the Upcase exercises system, exploring inversion of control. We previously covered inversion of control, but now we tackle it with a new twist: a new dependency injection framework for Rails which makes this style of developm...
This is a companion discussion topic for the original entry at https://thoughtbot.com/upcase/videos/dependency-management-in-rails

It appears the github link to payload is not available

Actually, it appears that you havenā€™t accepted the invitation to join our GitHub team. Since you donā€™t have access, GitHub shows you a 404 page.

@benorenstein This looks cool, but how do you handle the ever growing config/dependencies.rb file? I guess we have to accept some complexity somewhere!

Also, you donā€™t seem to require the classes in that file either, do you just allow Rails autoloading to work? How does that effect test speed? Iā€™ve been trying to use a fast_spec_helper for my POROs as much as possible to speed things up.

Also, is it actually ok to try to use the payload gem in an app? It would have to be a public repo on GitHub to add it to a Gemfile. We add private repo Gems with a url including an x-oauth-basic key in the URL, so weā€™d need to know Thoughtbotā€™s key to use it.

Really interesting concept. I think if you are not on the SOLID path, this is going to be really confusing to grasp. Hereā€™s an explanation about it what it does - for myself and other object-orientated noobsā€¦

It appears the problem arises when you start using lots of small single responsibility objects and end up with an object that has many collaborators and is difficult to manage or use in your Controllers. For example, in your Controller you may have a NotificationPayment object that requires a Payment and a PaymentNotifier to be passed in or injected.

Rather than specify all your dependencies in your Controller (i.e build up a NotificationPayment with Payment and PaymentNotifier) you can use Payload to extract the setup or configuration for your dependencies to this new file called config/dependencies.rb so that in your Controller you can simply use a one liner (via the dependencies method) to instantiate whatever it is that you need.

What seemā€™s cool about this is assuming you have lots of different objects, you can ā€˜mix and matchā€™ your objects to compose other objects in this dependencies.rb file, and only have to worry about that in one place.

I know itā€™s not the same, but it kind of feels like using traits in factory girl, where you can configure your factory setup how you like and call them when you need. I really appreciate that feature in FactoryGirl.

I think thereā€™s an obvious pre-requisite to using Payload and thatā€™s having lots of collaborating objects interacting with one another via dependency injection (it is called a DI framework afterall). I find that as a noob beginning to use POROā€™s in my code - my objects are not necessarily injecting more than one other object/collaborator. Notwithstanding, Payload may encourage you to use more factories and decorators which might help make things smaller and easier to manage?

Overall Iā€™d say it looks very cool! Iā€™m not sure if I should being using it straight away but certainly if my objects become ā€˜composedā€™ enough then Iā€™ll start looking at adding it in, unless @jferris suggests otherwise :wink:

@aaronmcadam docs say you can group dependencies in a file like config/dependencies/payments.rb which may help in managing growing complexity?

1 Like

Aw geezā€¦ youā€™re right. Iā€™m sorry. I expected it would be open source though, would be awesome to try this in a production application

I think you meant to direct that at me :wink:

Yeah I think this gem will solve issues with having lots of smaller objects that can work together to form larger dependencies. Itā€™s cool that you can break up the dependency definitions into separate files.

I think a builders directory is probably the first place to put Builder/Factory objects extracted from a Controller. If you have a critical mass of builders/factories, maybe then an IoC container like payload is the right option. Looking at some of the definitions of the objects in upcase-exercises though, they look quite complicated, so Iā€™d think youā€™d have to make sure the benefits really outweigh the complexity. I think youā€™d have to be careful that you actually have the problem of needing to swap dependencies around and actually have the objects that are composable

This looks cool, but how do you handle the ever growing config/dependencies.rb file?

Thereā€™s some work in progress to create namespaces/modules for dependencies, so that you can create little boxes of dependencies instead of having a giant graph.

I guess we have to accept some complexity somewhere!

Definitely. Some complexity is removed when factories can reduce the number of hops you need a dependency for, but mostly the goal is to break up complexity rather than remove it. The idea is to focus on dependency management in one place and let Ruby classes not worry about building instances of other classes.

Also, you donā€™t seem to require the classes in that file either, do you just allow Rails autoloading to work? How does that effect test speed? Iā€™ve been trying to use a fast_spec_helper for my POROs as much as possible to speed things up.

Weā€™re using Rails autoloading, yes. We use Spring, so it doesnā€™t affect our test speed at all. However, Payload doesnā€™t reference classes until you actually try to use a dependency, so you can still require things by hand if you prefer, in your specs or elsewhere. Iā€™ve considered adding a facility to Payload to handle file loading, but it hasnā€™t been a pain point for us so far.

Also, is it actually ok to try to use the payload gem in an app? It would have to be a public repo on GitHub to add it to a Gemfile. We add private repo Gems with a url including an x-oauth-basic key in the URL, so weā€™d need to know Thoughtbotā€™s key to use it.

You can actually just add payload to your Gemfile, because itā€™s on rubygems.org. Weā€™re not announcing or releasing it outside of Upcase yet, because weā€™re still trying to confirm whether or not this approach is viable and whether or not we want to maintain it. We decided to provide early access to Upcase subscribers, particularly since we talk so much about SOLID on the Weekly Iteration. That being said, we may decide not to continue pursuing this approach, so Payload may disappear some day.

Really interesting concept. I think if you are not on the SOLID path, this is going to be really confusing to grasp.

We havenā€™t found that to be the case so far when bringing on apprentice-level developers. Thereā€™s some ramp up time, but Iā€™d say the difficulty is less than learning things like the asset pipeline, Mongodb, or Capybara. Learning to manage dependencies well is certainly difficult, but that difficulty exists whether you use small classes, large classes, dependeny management, or anything at all.

Aw geezā€¦ youā€™re right. Iā€™m sorry. I expected it would be open source though, would be awesome to try this in a production application

We try not to open source things until we feel comfortable letting people depend on them. Itā€™s always difficult to decide when that is. As mentioned above, though, youā€™re welcome to try it out if youā€™d like.

1 Like

Great insights, thanks @jferris! One thing Iā€™m grappling with at the moment is how to test my dependency injector. Iā€™ve been looking in the exercises repo, but canā€™t seem to find any coverage. Do you let your feature specs catch that correct dependencies have been passed?

Hereā€™s an example Iā€™ve been working on, Iā€™m not sure whether itā€™s worth testing that the correct dependencies are passed to the constructor, it feels like it does add some value, otherwise anything could end up being passed as dependencies.

Thereā€™s a Builders::ProfilePage object that builds a ProfilePage instance: builders_profile_page_spec.rb Ā· GitHub

Yeah, we assume the feature specs catch problems with dependencies not lining up. We try to keep as little business logic as possible in the dependency configuration. So far, this has not caused any problems.

Awesome episode, thanks @jferris and @benorenstein . We watched this at an interesting time - we just got done extracting some functionality to a dependency injection container. Iā€™m curious about your opinion on the given solution vs what weā€™re trying out at ControlledVocabularyManager/vocabularies_controller.rb at master Ā· OregonDigital/ControlledVocabularyManager Ā· GitHub and ControlledVocabularyManager/vocabulary_injector.rb at master Ā· OregonDigital/ControlledVocabularyManager Ā· GitHub

I think one important to keep in mind is Inversion of Control. In the VocabulariesController example, control is still not inverted, because VocabulariesController instantiates VocabularyInjector and then delegates to it. This is using composition, which is a good way to break up large components or achieve reuse between several components, but it doesnā€™t invert control.

If you want to remove the concern of dependency management from your controllers, youā€™ll have to find a way to remove the reference to the VocabularyInjector class.

This is a good point - at this point weā€™ve declared our controllers as the entry point - largely because we havenā€™t found a good place to inject that concern. The goal is to invert control below that point - weā€™re not there yet, but this has been a good route there.

Thank you for the insight.