Managing Dependencies

10 Min. Read
Aug 22, 2019

Object-oriented programming languages contend that they are efficient and effective because of the way they model reality. Objects reflect qualities of a real-world problem and the interactions between those objects provide solution. A single object cannot know everything, so inevitably it will have to talk to another object.

If you could peer into a busy application and watch the messages as they pass, the traffic might seem overwhelming. There’s a lot going on. However, if you stand back and take a global view, a pattern becomes obvious. Each message is initiated by an object to invoke some bit of behavior. All of the behavior is dispersed among the objects. Therefore, for any desired behavior, an object either knows it personally, inherits it, or knows another object who knows it.

Because well designed objects have a single responsibility, their very nature requires that they collaborate to accomplish complex tasks. This collaboration is powerful and perilous. To collaborate, an object must know something know about others. Knowing creates a dependency. If not managed carefully, these dependencies will strangle your application.

Understanding Dependencies

An object depends on another object if, when one object changes, the other might be forced to change in turn. A dependency is some external object that is relied upon.

For instance, here’s a version of the Gear class, where Gear is initialized with four arguments. The gear_inches method uses two of them, rim and tire , to create a new instance of Wheel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Gear
  attr_reader :chainring, :cog, :rim, :tire

  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @rim = rim
    @tire = tire
  end

  def gear_inches
    ratio * Wheel.new(rim, tire).diameter
  end

  def ratio
    chainring / cog.to_f
  end
end

class Wheel
  attr_reader :rim, :tire
  def initialize(rim, tire)
    @rim = rim
    @tire = tire
  end

  def diameter
    rim + (tire * 2)
  end
end

Gear.new(52, 11, 26, 1.5).gear_inches

Try examining the code above and make a list of the situations in which Gear would be forced to change because of a change to Wheel. This code seems innocent but it’s sneakily complex. Gear has at least four dependencies on Wheel ,enumerated below. Most of the dependencies are unnecessary; they are a side effect of the coding style. Gear does not need them to do its job. Their very existence weakens Gear and makes it harder to change.

Listing out the dependencies

In the above example, gear is dependent on wheel because:

  • The name of another class: Gear expects a class named Wheel to exist.
  • The name of a message that it intends to send to someone other than self: Gear expects a Wheel instance to respond to diameter.
  • The arguments that a message requires: Gear knows that Wheel.new requires a rim and a tire.
  • The order of those arguments: Gear knows the first argument to Wheel.new should be rim, the second tire.

The Consequence

Each of these dependencies creates a chance that Gear will be forced to change because of a change to Wheel . Some degree of dependency between these two classes is inevitable, after all, they must collaborate, but most of the dependencies listed above are unnecessary. These unnecessary dependencies make the code less reasonable. Because they increase the chance that Gear will be forced to change, these dependencies turn minor code tweaks into major undertakings where small changes cascade through the application, forcing many changes.

Coupling Between Objects (CBO) : These dependencies couple Gear to Wheel. Alternatively, you could say that each coupling creates a dependency. The more Gear knows about Wheel, the more tightly coupled they are. The more tightly coupled two objects are, the more they behave like a single entity.

If you make a change to Wheel you may find it necessary to make a change to Gear. If you want to reuse Gear, Wheel comes along for the ride. When you test Gear , you’ll be testing Wheel too.

What kind of other dependencies are there?

One especially destructive kind of dependency occurs where an object knows another who knows another who knows something; that is, where many messages are chained together to reach behavior that lives in a distant object. This is the “knowing the name of a message you plan to send to someone other than self ” dependency, only magnified. Message chaining creates a dependency between the original object and every object and message along the way to its ultimate target. These additional couplings greatly increase the chance that the first object will be forced to change because a change to any of the intermediate objects might affect it.

Another entire class of dependencies is that of tests on code. The natural tendency of “new-to-testing” programmers is to write tests that are too tightly coupled to code. This tight coupling leads to incredible frustration; the tests break every time the code is refactored, even when the fundamental behavior of the code does not change. Tests begin to seem costly relative to their value. Test-to-code over-coupling has the same consequence as code-to-code over-coupling. These couplings are dependencies that cause changes to the code to cascade into the tests, forcing them to change in turn.

Tight Coupling V/S Loose Coupling

Essentially, coupling is how much a given object or set of object relies on another object or another set of objects in order to accomplish its task. As the name suggests, loose coupling means reducing dependencies of a class that use a different class directly. Loose coupling promotes greater reusability, easier maintainability. On the other hand, tight coupled classes and objects are dependent on one another. I must say that, tight coupling is usually bad because it reduces flexibility and re-usability of code and we are not able to achieve complete object originated programming features.

Tight Coupling

As par above definition a Tightly Coupled Object is an object that needs to know about other objects and are usually highly dependent on each other’s interfaces. When we change one object in a tightly coupled application often it requires changes to a number of other objects. There is no problem in a small application we can easily identify the change. But in the case of a large applications these inter-dependencies are not always known by every consumer or other developers or there is many chance of future changes.

Think of a car. In order for the engine to start, a key must be inserted into the ignition, turned, gasoline must be present, a spark must occur, pistons must fire, and the engine must come alive. You could say that a car engine is tightly coupled to several other objects.

Loose Coupling

Loose coupling is simply writing software in such a way that all your classes can work independently without relying on each other. It is a design strategy which allows us to reduce the inter-dependencies between components of a system with the goal of reducing the risk that changes in one component will require changes in any other component. It’s all about thinking a problem in generic manner and which intended to increase the flexibility of a system, make it more maintainable, and makes the entire framework more stable.

Writing Loosely Coupled Code

Every dependency is like a little dot of glue that causes your class to stick to the things it touches. A few dots are necessary, but apply too much glue and your application will harden into a solid block. Reducing dependencies means recognizing and removing the ones you don’t need. Following are some of the methods that prevents an application from strangling itself with tight coupled codes

1) Inject Dependency

Referring to another class by its name creates a major sticky spot. In the code demonstrated before version of Gear we’ve been discussing (repeated below), the gear_inches method contains an explicit reference to class Wheel:

The immediate, obvious consequence of this reference is that if the name of the Wheel class changes, Gear’s gear_inches method must also change. On the face of it this dependency seems innocuous. After all, if a Gear needs to talk to a Wheel, something, somewhere, must create a new instance of the Wheel class. If Gear itself knows the name of the Wheel class, the code in Gear must be altered if Wheel’s name changes.

When Gear hard-codes a reference to Wheel deep inside its gear_inches method, it is explicitly declaring that it is only willing to calculate gear_inches for instances of Wheel. Gear refuses to collaborate with any other kind of object, even if that object has a diameter and uses gears.

If your application expands to include objects such as disks or cylinders and you need to know the gear_inches of gears which use them, you cannot. Despite the fact that disks and cylinders naturally have a diameter you can never calculate their gear inches because Gear is stuck to Wheel.

The code above exposes an unjustified attachment to static types. It is not the class of the object that’s important, it’s the message you plan to send to it.

The class Gear does not care and should not know about the class of that object. It is not necessary for Gear to know about the existence of the Wheel class in order to calculate gear_inches . It doesn’t need to know that Wheel expects to be initialized with a rim and then a tire; it just needs an object that knows diameter. Hanging these unnecessary dependencies on Gear simultaneously reduces Gear’s reusability and increases its susceptibility to being forced to change unnecessarily. Gear becomes less useful when it knows too much about other objects; if it knew less it could do more.

Instead of being glued to Wheel, this next version of Gear expects to be initialized with an object that can respond to diameter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Gear
  attr_reader :chainring, :cog, :wheel

  def initialize(chainring, cog, wheel)
    @chainring = chainring
    @cog = cog
    @wheel = wheel
  end

  def gear_inches
    ratio * wheel.diameter
  end
# ...
end

Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches

Gear now uses the @wheel variable to hold, and the wheel method to access this object, but don’t be fooled, Gear doesn’t know or care that the object might be an instance of class Wheel. Gear only knows that it holds an object that responds to diameter. This change is so small it is almost invisible, but coding in this style has huge benefits. Moving the creation of the new Wheel instance outside of Gear decouples the two classes. Gear can now collaborate with any object that implements diameter. As an extra bonus, this benefit was free. Not one additional line of code was written; the decoupling was achieved by rearranging existing code.

This technique is known as dependency injection. Despite its fearsome reputation, dependency injection truly is this simple. Gear previously had explicit dependencies on the Wheel class and on the type and order of its initialization arguments, but through injection these dependencies have been reduced to a single dependency on the diameter method. Gear is now smarter because it knows less.

2) Isolate Dependencies

It’s best to break all unnecessary dependencies but, unfortunately, while this is always technically possible it may not be actually possible. When working on an existing application you may find yourself under severe constraints about how much you can actually change. If prevented from achieving perfection, your goals should switch to improving the overall situation by leaving the code better than you found it.

Therefore, if you cannot remove unnecessary dependencies, you should isolate them within your class.

Isolate Instance Creation

If you are so constrained that you cannot change the code to inject a Wheel into a Gear, you should isolate the creation of a new Wheel inside the Gear class. The intent is to explicitly expose the dependency while reducing its reach into your class. The next two examples illustrate this idea.

In the first, creation of the new instance of Wheel has been moved from Gear’s gear_inches method to Gear’s initialization method. This cleans up the gear_inches method and publicly exposes the dependency in the initialize method. Notice that this technique unconditionally creates a new Wheel each time a new Gear is created.

1
2
3
4
5
6
7
8
9
10
11
12
class Gear
  attr_reader :chainring, :cog, :rim, :tire

  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @wheel = Wheel.new(rim, tire)
  end
  def gear_inches
    ratio * wheel.diameter
  end
end

The next alternative isolates creation of a new Wheel in its own explicitly defined wheel method. This new method lazily creates a new instance of Wheel , using Ruby’s ||= operator. In this case, creation of a new instance of Wheel is deferred until gear_inches invokes the new wheel method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Gear

  attr_reader :chainring, :cog, :rim, :tire

  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @rim = rim
    @tire = tire
  end

  def gear_inches
    ratio * wheel.diameter
  end

  def wheel
    @wheel ||= Wheel.new(rim, tire)
  end
end

In both of these examples Gear still knows far too much; it still takes rim and tire as initialization arguments and it still creates its own new instance of Wheel. Gear is still stuck to Wheel; it can calculate the gear_inches of no other kind of object. However, an improvement has been made. These coding styles reduce the number of dependencies in gear_inches while publicly exposing Gear’s dependency on Wheel. They reveal dependencies instead of concealing them, lowering the barriers to reuse and making the code easier to refactor when circumstances allow. This change makes the code more agile; it can more easily adapt to the unknown future.

The way you manage dependencies on external class names has profound effects on your application. If you are mindful of dependencies and develop a habit of routinely injecting them, your classes will naturally be loosely coupled. If you ignore this issue and let the class references fall where they may, your application will be more like a big woven mat than a set of independent objects. An application whose classes are sprinkled with entangled and obscure class name references is unwieldy and inflexible, while one whose class name dependencies are concise, explicit, and isolated can easily adapt to new requirements.