Managing Dependencies - Part 1 (OOP)
Managing Dependencies
Let’s suppose you’re coding away. Let’s take a look at this for example:
class Car
def initialize
end
def drive
bens_phone = new Iphone()
# we like to play music while we drive
# "Don't stop me now...I'm having such a good time..."
bens_phone.play_music
end
end
What do you notice in the above code? If you can answer, please do so before proceeding, because it will help you learn and retain information in the future.
It is here that we notice the concept of the ‘external dependency’.
What exactly is an external dependency? Let’s put it simply: one object is dependent upon another, such that if one is forced to change, the other might be forced to change in turn.
Now there are four types of dependencies we need to be aware of. Let us consider each in turn:
- When one class knows the name of another class.
- When you know the message you need to send someone other than ‘self’.
- When you know the parameters you are required to send.
- When you know the order of the parameters you are required to send.
(Source: Practical Object Oriented Design in Ruby)
Example of Dependency
*Pop Quiz:** Consider class New England Patriots and class Brady. Which of the above rules applies to the below code?
class NewEnglandPatriots
def initialize
end
def play_game
qb = new TomBrady()
qb.throw_touchdown_passes
qb.win_super_bowl
end
end
Answer: To me, the NewEnglandPatriots knows the name of another class: TomBrady. Secondly, the NewEnglandPatriots knows the messages that are required to be sent to TomBrady. Consequently rules 1 and 2 are broken.
Further Explanation to Elucidate Dependencies
Things are going well at the Patriots. But they are highly dependent upon the Tom Brady class. If Tom decides that he wants to retire or if he gets injured then he won’t be able to throw winning touch downs any more. Let’s suppose that it actually happens:
Somebody gets a support request from an irate fan: give somebody else a turn at being the lead QB. The unwitting developer listens to the request and decides to get rid of the throw_winning_passes method in the TomBrady class. Everything runs smoothly and all is happy….till the Patriots play their next game.
Suddenly, when Belichick calls on Brady to do his winning thing, Brady throws a method not found exception rather than a winning touch down pass.
This is a perfect example of an dependency: because the TomBrady class changed, then the New England class also needs to make a change in order to continue working.
So what’s the big deal?
The big deal is when something changes. And we all know that change is inevitable in software development. So we gotta try and make it easy for ourselves to manage change.
We can do so immediately by “de-coupling” as much as we can. In other words, we want to make sure that when one thing changes, we minimise the changes that need to be made.
Have you ever made a small change, which then trickled down to another change, when they snow-balled into an avalanche of further changes - until you find yourself rewriting entire applications? Yeah, you want to avoid that. You can do that by de-coupling.
Here are some tried and tested strategies to write loosely coupled code:
#1 Inject Dependencies
Consider this code:
def play_game
qb = TomBrady.new()
qb.throw_touchdown_passes
qb.win_super_bowl
end
What do you notice about it? The Patriots are forced to depend on Brady and only Brady. They can’t substitute another QB at all. Tut-tut-tut: that’s a no-no.
You want to inject dependencies where possible. This allows you to substitute another quarter back, without making many other changes.
All that matters is whether the quarter_back knows how to throw_touchdown_passes and win_super_bowl.
Here:
class NewEnglandPatriots
attr_reader :qb
def initialize(qb)
@qb = qb
end
def play_game
@qb.throw_touchdown_passes
@qb.win_super_bowl
end
end
Now if you want to substitute another player for Tom Brady then you can easily do so.
Inject dependencies where possible. This allows you to easily substitute similar objects if and when the times comes for you to do that.
What if you can’t inject dependencies - isolate the offenders
Consider the following and note any problems which may arise:
class NewEnglandPatriots
def initialize()
end
def pass
qb = TomBrady.new()
qb.pass
end
def run_ball
qb = TomBrady.new()
qb.run_ball
end
def kick
qb = TomBrady.new()
qb.pass_to_kicker
end
end
What if you had to change the name of the TomBrady class? What if you want to substitute Brady for another QB? That requires a lot of changing. You want to minimize your effort and to keep things as DRY as possible. Stop and consider this as a very important exercise: how you would improve the above code to handle the aforementioned issue? Consider writing it out yourself to assist in your learning.
class NewEnglandPatriots
attr_accessor :qb
def initialize()
end
def pass
get_qb.pass
end
def run_ball
get_qb.run_ball
end
def kick
get_qb.pass_to_kicker
end
def get_qb
@qb ||= TomBrady.new()
end
end
The above represents a vast improvement. Now if we want to substitute another QB, say Farve for instance, we can do so without too much trouble. Just change the get_qb method. DRY it up - makes things easier to change.
- Question to think about: why should we bother to isloate these dependencies?
What about isolating messages
Now let’s consider a different situation. Consider the code below:
class NewEnglandPatriots
attr_accessor :qb
def initialize()
end
def run_ball
get_qb.pass
get_qb.run_ball
end
def kick
get_qb.pass
get_qb.pass_to_kicker
end
def get_qb
@qb ||= TomBrady.new()
end
end
What are the risks inherent here?
What if person who wrote the QuarterBack class decides to change the message “pass” to “throw”? Imagine if you had a million ruby "get_qb.pass"
methods sprinkled throughout your code. Changing that would be a nightmare. You ought to DRY that out, to a simple wrapping method:
class NewEnglandPatriots
attr_accessor :qb
def initialize()
end
def pass
get_qb.pass
end
def run_ball
pass
only_run_ball
end
def kick
pass
only_pass_to_kicker
end
def only_run_ball
get_qb.run_ball
end
def only_pass_to_kicker
get_qb.pass_to_kicker
end
def get_qb
@qb ||= TomBrady.new()
end
end
Now the only messages that are not sent to self are confined in the ruby only_pass_to_kicker
and ruby only_run_ball
methods. So that if the dependent class does change its message, then you aren’t all that vulnerable.
Ok let’s summarize so far:
-
Isolate all creation of instances - that way if something changes then you only need to make one single change as opposed to 100+.
-
Isolate cases where you are sending messages which might change. If you are sending the message “pass” and it is sprinkled a million times throughout your app, what are you going to do if the owner of that method decides to change “pass” to “pass_ball”? You’re screwed. You should isolate that to a wrapping method - DRY so that if the message changes in any way, you can handle it in one place, and only one place.
Further things to Isolate
- Note that a lot of methods require parameters. We have to known the order with which to enter those parameters. You can eliminate the need to know this by passing in a hash instead.
Imagine you are using a class you didn’t write: How would you handle that?
You would handle that in the same way. Create a wrapper and pass a hash to that wrapper. Let the wrapper take care of putting all the parameters/arguments in the right order and passing it to an external libraries.
In the next few posts we will have to think seriously about dependency direction. But before that, I’m gonna run you through some exercises. So you’ll be forced to practice and reinforce your knowledge.