- Understanding Dependencies
- Writing Loosely Coupled Code
- Managing Dependency Direction
- Summary
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.
The following examples illustrate coding techniques that reduce dependencies by decoupling code.
Inject Dependencies
Referring to another class by its name creates a major sticky spot. In the version of Gear we’ve been discussing (repeated below), the gear_inches method contains an explicit reference to class Wheel:
1
class
Gear
2
attr_reader:chainring
,:cog
,:rim
,:tire
3
def
initialize
(chainring, cog, rim, tire)4
@chainring
= chainring5
@cog
= cog6
@rim
= rim7
@tire
= tire8
end
9
10
def
gear_inches
11
ratio *Wheel
.new(rim, tire).diameter12
end
13
# ...
14
end
15
16
Gear
.new(52
,11
,26
, 1.5).gear_inche
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.
In truth, dealing with the name change is a relatively minor issue. You likely have a tool that allows you to do a global find/replace within a project. If Wheel’s name changes to Wheely, finding and fixing all of the references isn’t that hard. However, the fact that line 11 above must change if the name of the Wheel class changes is the least of the problems with this code. A deeper problem exists that is far less visible but significantly more destructive.
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. Gear needs access to an object that can respond to diameter; a duck type, if you will (see Chapter 5, Reducing Costs with Duck Typing). 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
class
Gear
2
attr_reader:chainring
,:cog
,:wheel
3
def
initialize
(chainring, cog, wheel)4
@chainring
= chainring5
@cog
= cog6
@wheel
= wheel7
end
8
9
def
gear_inches
10
ratio * wheel.diameter11
end
12
# ...
13
end
14
15
# Gear expects a 'Duck' that knows 'diameter'
16
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.
Using dependency injection to shape code relies on your ability to recognize that the responsibility for knowing the name of a class and the responsibility for knowing the name of a message to send to that class may belong in different objects. Just because Gear needs to send diameter somewhere does not mean that Gear should know about Wheel.
This leaves the question of where the responsibility for knowing about the actual Wheel class lies; the example above conveniently sidesteps this issue, but it is examined in more detail later in this chapter. For now, it’s enough to understand that this knowledge does not belong in Gear.
Isolate Dependencies
It’s best to break all unnecessary dependences 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. In Chapter 2, Designing Classes with a Single Responsibility, you isolated extraneous responsibilities so that they would be easy to recognize and remove when the right impetus came; here you should isolate unnecessary dependences so that they are easy to spot and reduce when circumstances permit.
Think of every dependency as an alien bacterium that’s trying to infect your class. Give your class a vigorous immune system; quarantine each dependency. Dependencies are foreign invaders that represent vulnerabilities, and they should be concise, explicit, and isolated.
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
class
Gear
2
attr_reader:chainring
,:cog
,:rim
,:tire
3
def
initialize
(chainring, cog, rim, tire)4
@chainring
= chainring5
@cog
= cog6
@wheel
=Wheel
.new(rim, tire)7
end
8
9
def
gear_inches
10
ratio * wheel.diameter11
end
12
# ...
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
class
Gear
2
attr_reader:chainring
,:cog
,:rim
,:tire
3
def
initialize
(chainring, cog, rim, tire)4
@chainring
= chainring5
@cog
= cog6
@rim
= rim7
@tire
= tire8
end
9
10
def
gear_inches
11
ratio * wheel.diameter12
end
13
14
def
wheel
15
@wheel
||=Wheel
.new(rim, tire)16
end
17
# ...
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.
Isolate Vulnerable External Messages
Now that you’ve isolated references to external class names it’s time to turn your attention to external messages, that is, messages that are “sent to someone other than self.” For example, the gear_inches method below sends ratio and wheel to self, but sends diameter to wheel:
1
def
gear_inches
2
ratio * wheel.diameter3
end
This is a simple method and it contains Gear's only reference to wheel.diameter. In this case the code is fine, but the situation could be more complex. Imagine that calculating gear_inches required far more math and that the method looked something like this:
1
def
gear_inches
2
#... a few lines of scary math
3
foo = some_intermediate_result * wheel.diameter4
#... more lines of scary math
5
end
Now wheel.diameter is embedded deeply inside a complex method. This complex method depends on Gear responding to wheel and on wheel responding to diameter. Embedding this external dependency inside the gear_inches method is unnecessary and increases its vulnerability.
Any time you change anything you stand the chance of breaking it; gear_inches is now a complex method and that makes it both more likely to need changing and more susceptible to being damaged when it does. You can reduce your chance of being forced to make a change to gear_inches by removing the external dependency and encapsulating it in a method of its own, as in this next example:
1
def
gear_inches
2
#... a few lines of scary math
3
foo = some_intermediate_result * diameter4
#... more lines of scary math
5
end
6
7
def
diameter
8
wheel.diameter9
end
The new diameter method is exactly the method that you would have written if you had many references to wheel.diameter sprinkled throughout Gear and you wanted to DRY them out. The difference here is one of timing; it would normally be defensible to defer creation of the diameter method until you had a need to DRY out code; however, in this case the method is created preemptively to remove the dependency from gear_inches.
In the original code, gear_inches knew that wheel had a diameter. This knowledge is a dangerous dependency that couples gear_inches to an external object and one of its methods. After this change, gear_inches is more abstract. Gear now isolates wheel.diameter in a separate method and gear_inches can depend on a message sent to self.
If Wheel changes the name or signature of its implementation of diameter, the side effects to Gear will be confined to this one simple wrapping method.
This technique becomes necessary when a class contains embedded references to a message that is likely to change. Isolating the reference provides some insurance against being affected by that change. Although not every external method is a candidate for this preemptive isolation, it’s worth examining your code, looking for and wrapping the most vulnerable dependencies.
An alternative way to eliminate these side effects is to avoid the problem from the very beginning by reversing the direction of the dependency. This idea will be addressed soon but first there’s one more coding technique to cover.
Remove Argument-Order Dependencies
When you send a message that requires arguments, you, as the sender, cannot avoid having knowledge of those arguments. This dependency is unavoidable. However, passing arguments often involves a second, more subtle, dependency. Many method signatures not only require arguments, but they also require that those arguments be passed in a specific, fixed order.
In the following example, Gear’s initialize method takes three arguments: chainring, cog, and wheel. It provides no defaults; each of these arguments is required. In lines 11–14, when a new instance of Gear is created, the three arguments must be passed and they must be passed in the correct order.
1
class
Gear
2
attr_reader:chainring
,:cog
,:wheel
3
def
initialize
(chainring, cog, wheel)4
@chainring
= chainring5
@cog
= cog6
@wheel
= wheel7
end
8
...9
end
10
11
Gear
.new(12
52
,13
11
,14
Wheel
.new(26
, 1.5)).gear_inches
Senders of new depend on the order of the arguments as they are specified in Gear’s initialize method. If that order changes, all the senders will be forced to change.
Unfortunately, it’s quite common to tinker with initialization arguments. Especially early on, when the design is not quite nailed down, you may go through several cycles of adding and removing arguments and defaults. If you use fixed-order arguments each of these cycles may force changes to many dependents. Even worse, you may find yourself avoiding making changes to the arguments, even when your design calls for them because you can’t bear to change all the dependents yet again.
Use Hashes for Initialization Arguments
There’s a simple way to avoid depending on fixed-order arguments. If you have control over the Gear initialize method, change the code to take a hash of options instead of a fixed list of parameters.
The next example shows a simple version of this technique. The initialize method now takes just one argument, args, a hash that contains all of the inputs. The method has been changed to extract its arguments from this hash. The hash itself is created in lines 11–14.
1
class
Gear
2
attr_reader:chainring
,:cog
,:wheel
3
def
initialize
(args)4
@chainring
= args[:chainring
]5
@cog
= args[:cog]
6
@wheel
= args[:wheel
]7
end
8
...9
end
10
11
Gear
.new(12
:chainring
=>52
,13
:cog
=>11
,14
:wheel
=>Wheel
.new(26
, 1.5)).gear_inches
The above technique has several advantages. The first and most obvious is that it removes every dependency on argument order. Gear is now free to add or remove initialization arguments and defaults, secure in the knowledge that no change will have side effects in other code.
This technique adds verbosity. In many situations verbosity is a detriment, but in this case it has value. The verbosity exists at the intersection between the needs of the present and the uncertainty of the future. Using fixed-order arguments requires less code today but you pay for this decrease in volume of code with an increase in the risk that changes will cascade into dependents later.
When the code in line 11 changed to use a hash, it lost its dependency on argument order but it gained a dependency on the names of the keys in the argument hash. This change is healthy. The new dependency is more stable than the old, and thus this code faces less risk of being forced to change. Additionally, and perhaps unexpectedly, the hash provides one new, secondary benefit: The key names in the hash furnish explicit documentation about the arguments. This is a byproduct of using a hash but the fact that it is unintentional makes it no less useful. Future maintainers of this code will be grateful for the information.
The benefits you achieve by using this technique vary, as always, based on your personal situation. If you are working on a method whose parameter list is lengthy and wildly unstable, in a framework that is intended to be used by others, it will likely lower overall costs if you specify arguments in a hash. However, if you are writing a method for your own use that multiplies two numbers, it’s far simpler and perhaps ultimately cheaper to merely pass the arguments and accept the dependency on order. Between these two extremes lies a common case, that of the method that requires a few very stable arguments and optionally permits a number of less stable ones. In this case, the most cost-effective strategy may be to use both techniques; that is, to take a few fixed-order arguments, followed by an options hash.
Explicitly Define Defaults
There are many techniques for adding defaults. Simple non-boolean defaults can be specified using Ruby’s || method, as in this next example:
1
# specifying defaults using ||
2
def
initialize
(args)3
@chainring
= args[:chainring
] ||40
4
@cog
= args[:cog
] ||18
5
@wheel
= args[:wheel
]6
end
This is a common technique but one you should use with caution; there are situations in which it might not do what you want. The || method acts as an or condition; it first evaluates the left-hand expression and then, if the expression returns false or nil, proceeds to evaluate and return the result of the right-hand expression. The use of || above therefore, relies on the fact that the [] method of Hash returns nil for missing keys.
In the case where args contains a :boolean_thing key that defaults to true, use of || in this way makes it impossible for the caller to ever explicitly set the final variable to false or nil. For example, the following expression sets @bool to true when :boolean_thing is missing and also when it is present but set to false or nil:
@bool
= args[:boolean_thing
] ||true
This quality of || means that if you take boolean values as arguments, or take arguments where you need to distinguish between false and nil, it’s better to use the fetch method to set defaults. The fetch method expects the key you’re fetching to be in the hash and supplies several options for explicitly handling missing keys. Its advantage over || is that it does not automatically return nil when it fails to find your key.
In the example below, line 3 uses fetch to set @chainring to the default, 40, only if the :chainring key is not in the args hash. Setting the defaults in this way means that callers can actually cause @chainring to get set to false or nil, something that is not possible when using the || technique.
1
# specifying defaults using fetch
2
def
initialize
(args)3
@chainring
= args.fetch(:chainring
,40
)4
@cog
= args.fetch(:cog
,18
)5
@wheel
= args[:wheel
]6
end
You can also completely remove the defaults from initialize and isolate them inside of a separate wrapping method. The defaults method below defines a second hash that is merged into the options hash during initialization. In this case, merge has the same effect as fetch; the defaults will get merged only if their keys are not in the hash.
1
# specifying defaults by merging a defaults hash
2
def
initialize
(args)3
args = defaults.merge(args)4
@chainring
= args[:chainring
]5
# ...
6
end
7
8
def
defaults
9
{:chainring
=>40
,:cog
=>18
}10
end
This isolation technique is perfectly reasonable for the case above but it’s especially useful when the defaults are more complicated. If your defaults are more than simple numbers or strings, implement a defaults method.
Isolate Multiparameter Initialization
So far all of the examples of removing argument order dependencies have been for situations where you control the signature of the method that needs to change. You will not always have this luxury; sometimes you will be forced to depend on a method that requires fixed-order arguments where you do not own and thus cannot change the method itself.
Imagine that Gear is part of a framework and that its initialization method requires fixed-order arguments. Imagine also that your code has many places where you must create a new instance of Gear. Gear’s initialize method is external to your application; it is part of an external interface over which you have no control.
As dire as this situation appears, you are not doomed to accept the dependencies. Just as you would DRY out repetitive code inside of a class, DRY out the creation of new Gear instances by creating a single method to wrap the external interface. The classes in your application should depend on code that you own; use a wrapping method to isolate external dependencies.
In this example, the SomeFramework::Gear class is not owned by your application; it is part of an external framework. Its initialization method requires fixed-order arguments. The GearWrapper module was created to avoid having multiple dependencies on the order of those arguments. GearWrapper isolates all knowledge of the external interface in one place and, equally importantly, it provides an improved interface for your application.
As you can see in line 24, GearWrapper allows your application to create a new instance of Gear using an options hash.
1
# When Gear is part of an external interface
2
module
SomeFramework
3
class
Gear
4
attr_reader:chainring
,:cog
,:wheel
5
def
initialize
(chainring, cog, wheel)6
@chainring
= chainring7
@cog
= cog8
@wheel
= wheel9
end
10
# ...
11
end
12
end
13
14
# wrap the interface to protect yourself from changes
15
module
GearWrapper
16
def
self
.gear
(args)17
SomeFramework
::Gear
.new(args[:chainring
],18
args[:cog
],19
args[:wheel
])20
end
21
end
22
23
# Now you can create a new Gear using an arguments hash.
24
GearWrapper
.gear(25
:chainring
=>52,
26
:cog
=>11
,27
:wheel
=>Wheel
.new(26
, 1.5)).gear_inches
There are two things to note about GearWrapper. First, it is a Ruby module instead of a class (line 15). GearWrapper is responsible for creating new instances of SomeFramework::Gear. Using a module here lets you define a separate and distinct object to which you can send the gear message (line 24) while simultaneously conveying the idea that you don’t expect to have instances of GearWrapper. You may already have experience with including modules into classes; in the example above GearWrapper is not meant to be included in another class, it’s meant to directly respond to the gear message.
The other interesting thing about GearWrapper is that its sole purpose is to create instances of some other class. Object-oriented designers have a word for objects like this; they call them factories. In some circles the term factory has acquired a negative connotation, but the term as used here is devoid of baggage. An object whose purpose is to create other objects is a factory; the word factory implies nothing more, and use of it is the most expedient way to communicate this idea.
The above technique for substituting an options hash for a list of fixed-order arguments is perfect for cases where you are forced to depend on external interfaces that you cannot change. Do not allow these kinds of external dependencies to permeate your code; protect yourself by wrapping each in a method that is owned by your own application.