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:

  1. When one class knows the name of another class.
  2. When you know the message you need to send someone other than ‘self’.
  3. When you know the parameters you are required to send.
  4. 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:

  1. Isolate all creation of instances - that way if something changes then you only need to make one single change as opposed to 100+.

  2. 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.

Written on May 27, 2017