A practical example for dependency injection

In today’s article I am sharing with our team an example I found while reading parts of the code in one of our platforms where we’ve created a form of ‘cyclic’ references with cancancan merges in Rails Controllers. I wondered for a while how we’ve managed to create it, how to avoid creating them in the future and I share this as an example with our team, but I hope the article could be useful for the whole community.

I see this as a perfect example for the use of Dependency Injection.

The example

CourseSectionsAbilities is merged with ContentsAbility which is merged with ContentRefsAbilities which is then merged with CourseSectionAbilities. This means that CourseSectionsAbilities is merged twice and the second merge overrides the first merge.

class CourseSectionsController < CommonController

  def current_ability
    # This is the first merge 
    # CoruseSectionsAbility is merged with ContentsAbility.
    @current_ability ||= Abilities::CourseSectionsAbility.new(current_user)
                           .merge(Abilities::ContentsAbility.new(current_user))
  end

end

module Abilities
  class ContentsAbility
    include CanCan::Ability

    def initialize user
      ...
      # This is the second merge
      # ContentsAbility is merged with ContentRefs ability
      merge ContentRefsAbility.new user
    end
  end
end

module Abilities

  class ContentRefsAbility
    include CanCan::Ability

    def initialize user
      # This is the third merge
      # ContentRefsAbility is merged with CourseSectionsAbilily.
      # This overrides the first merge.
      merge CourseSectionsAbility.new(user)
      ...
    end
  end
end

How did it happen?

It think it is the classic grow of the code where you solve the problem in the easiest way possible. Initially we had:

class CourseSectionsController < CommonController

  def current_ability
    # This is the first merge 
    # CoruseSectionsAbility is merged with ContentsAbility.
    @current_ability ||= Abilities::CourseSectionsAbility.new(current_user)
                           .merge(Abilities::ContentsAbility.new(current_user))
  end

end

But then a new requirement about an year after that has come and there is a commit that adds the second merge:

module Abilities

  class ContentRefsAbility
    include CanCan::Ability

    def initialize user
      # This is the third merge
      # ContentRefsAbility is merged with CourseSectionsAbilily.
      # This overrides the first merge.
      merge CourseSectionsAbility.new(user)
      ...
    end
  end
end

What is the problem?

The problem is that there is a class called ContentRefsAbility that can dependent on everything it wants. It can merge anything inside it with any consideration on what was already merged. It can set it’s own dependencies. This couples the ContentRefsAbility with the places it is used. Because we must take into consideration every place where ContentRefsAbility is used before changing it’s implementation.

How Dependency Injection solves this?

We pass the ability in the constructor

module Abilities

  class ContentRefsAbility
    include CanCan::Ability

    def initialize user, outside_ability
      # This is the third merge
      # ContentRefsAbility is merged with CourseSectionsAbilily.
      # This overrides the first merge.
      outside_ability.method1 # we call the method of the outside_ability
      ...
    end
  end
end

Instead of creating the ability in the class we pass the dependency from the outside. In this way we can control the dependency and choose different dependencies in different conditions.

The ContentRefsAbility no longer depends on the specific implementation of outside ability, but it depends on the behavior we inject from the outside.