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.
Reply
You must be logged in to post a comment.