Managing Dependencies
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 namedWheel
to exist. - The name of a message that it intends to send to someone other than self:
Gear
expects aWheel
instance to respond todiameter
. - The arguments that a message requires:
Gear
knows thatWheel.new
requires arim
and atire
. - The order of those arguments:
Gear
knows the first argument toWheel.new
should berim
, the secondtire
.
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.