I am trying to learn GRASP and I found this explained (here on page 3) about Low Coupling and I was very surprised when I found this:
Consider the method
addTrack
for anAlbum
class, two possible methods are:
addTrack( Track t )
and
addTrack( int no, String title, double duration )
Which method reduces coupling?
The second one does, since the class using the Album class does not have to know a Track class.
In general, parameters to methods should use base types (int, char …) and classes from the java.* packages.
I tend to diasgree with this; I believe addTrack(Track t)
is better than addTrack(int no, String title, double duration)
due to various reasons:
-
It is always better for a method to as fewer parameters as possible (according to Uncle Bob’s Clean Code none or one preferably, 2 in some cases and 3 in special cases; more than 3 needs refactoring – these are of course recommendations not holly rules).
-
If
addTrack
is a method of an interface, and the requirements need that aTrack
should have more information (say year or genre) then the interface needs to be changed and so that the method should supports another parameter. -
Encapsulation is broke; if
addTrack
is in an interface, then it should not know the internals of theTrack
. -
It is actually more coupled in the second way, with many parameters. Suppose the
no
parameter needs to be changed fromint
tolong
because there are more thanMAX_INT
tracks (or for whatever reason); then both theTrack
and the method need to be changed while if the method would beaddTrack(Track track)
only theTrack
would be changed.
All the 4 arguments are actually connected with each other, and some of them are consequences from others.
Which approach is better?
8
Well, your first three points are actually about other principles than coupling. You always have to strike a balance between oft-conflicting design principles.
Your fourth point is about coupling, and I strongly agree with you. Coupling is about the flow of data between modules. The type of the container that data flows in is largely immaterial. Passing a duration as a double instead of as a field of a Track
doesn’t obviate the need to pass it. The modules still need to share the same amount of data, and still have the same amount of coupling.
He is also failing to consider all the coupling in the system as an aggregate. While introducing a Track
class admittedly adds another dependency between two individual modules, it can significantly reduce the coupling of the system, which is the important measure here.
For example, consider an “Add to Playlist” button and a Playlist
object. Introducing a Track
object could be considered to increase coupling if you only consider those two objects. You now have three interdependent classes instead of two. However, that is not the entirety of your system. You also need to import the track, play the track, display the track, etc. Adding one more class to that mix is negligible.
Now consider needing to add support for playing tracks over the network instead of just locally. You just need to create a NetworkTrack
object that conforms to the same interface. Without the Track
object, you would have to create functions everywhere like:
addNetworkTrack(int no, string title, double duration, URL location)
That effectively doubles your coupling, requiring even modules that don’t care about the network-specific stuff to nevertheless still keep track of it, in order to be able to pass it on.
Your ripple effect test is a good one to determine your true amount of coupling. What we are concerned with is limiting the places a change affects.
4
My recommendation is:
Use
addTrack( ITrack t )
but be sure that ITrack
is an interface and not a concrete class.
Album doesn’t know the internals of ITrack
implementors. It’s only coupled to the contract defined by the ITrack
.
I think this is the solution that generates the least amount of coupling.
5
I would argue that the second example method most likely increases coupling, since it most likely is instantiating a Track object and storing it in the current Album object. (As suggested in my comment above, I would assume it to be inherent that an Album class would have the concept of a Track class somewhere inside it.)
The first example method assumes that a Track is getting instantiated outside the Album class, so at the very least, we can assume that the instantiation of the Track class is not coupled to the Album class.
If best practices suggested that we never have one class reference a second class, the entirety of object-oriented programming would be thrown out the window.
2
Coupling is just one of many aspects to try to obtain in your code. By reducing coupling, you’re not necessarily improving your program. In general, this is a best practice, but in this particular instance, why shouldn’t Track
be known?
By using a Track
class to be passed to Album
, you are making your code easier to read, but more importantly, as you mentioned, you’re turning a static list of parameters into a dynamic object. That ultimately makes your interface far more dynamic.
You mention that encapsulation is broke, but it is not. Album
must know the internals of Track
, and if you did not use an object, Album
would have to know each and every piece of information passed to it before it could make use of it all the same. The caller must know the internals of Track
as well, since it must construct a Track
object, but the caller must know this information all the same if it were passed directly to the method. In other words, if the advantage of encapsulation is not knowing an object’s contents, it could not possibly be used in this case since Album
must make use of Track
‘s information just the same.
Where you wouldn’t want to use Track
is if Track
contains internal logic that you wouldn’t want the caller to have access to. In other words, if Album
were a class that a programmer using your library were to use, you wouldn’t want him to use Track
if you use it to say, call a method to persist it on the database. The true problem with this lies in the fact that the interface is entangled with the model.
To fix the problem, you would need to separate Track
into its interface components and its logic components, creating two separate classes. To the caller, Track
becomes a light class which is meant to hold information and offer minor optimizations (calculated data and/or default values). Inside Album
, you would use a class named TrackDAO
to perform the heavy lifting associated with saving the information from Track
to the database.
Of course, this is just an example. I’m sure this is not your case at all, and so feel free to use Track
guilt-free. Just remember to keep your caller in mind when you’re constructing classes and to create interfaces when required.
0
Both are correct
addTrack( Track t )
is better (as you already argumented) while
addTrack( int no, String title, double duration )
is less coupled because the code that uses addTrack
does not need to know that there is a Track
class. Track can be renamed for example without the need to update the calling code.
While you are talking about more readable/maintainable code the article is talking about coupling.
Less coupled code is not necessarily easier to implement and to understand.
1
Low Coupling doesn’t mean No Coupling. Something, somewhere, has to know about objects elsewhere in the codebase, and the more you reduce dependence on “custom” objects, the more reasons you give for code to change. What the author you cite is promoting with the second function is less coupled, but also less object-oriented, which is contrary to the entire idea of GRASP as being an object-oriented design methodology. The whole point is how to design the system as a collection of objects and their interactions; avoiding them is like teaching you how to drive a car by saying you should ride a bike instead.
Instead, the proper avenue is to reduce dependence on concrete objects, which is the theory of “loose coupling”. The fewer definite concrete types a method has to have knowledge of, the better. Just by that statement, the first option is actually less coupled, because the second method taking the simpler types must know about all of those simpler types. Sure they’re built-in, and the code inside the method may have to care, but the signature of the method and the method’s callers most definitely do not. Changing one of these parameters relating to a conceptual audio track is going to require more changes when they’re separate versus when they’re contained in a Track object (which is the point of objects; encapsulation).
Going one step further, if Track were expected to be replaced with something that did the same job better, perhaps an interface defining the requisite functionality would be in order, an ITrack. That could allow for differing implementations such as “AnalogTrack”, “CdTrack” and “Mp3Track” that provided additional information more specific to those formats, while still providing the basic data exposure of ITrack that conceptually represents a “track”; a finite sub-piece of audio. Track could similarly be an abstract base class, but this requires you to always want to use the implementation inherent in Track; reimplement it as BetterTrack and now you have to change out the expected parameters.
Thus the golden rule; programs and their code components will always have reasons to change. You can’t write a program that will never require editing code you’ve already written in order to add something new or modify its behavior. Your goal, in any methodology (GRASP, SOLID, any other acronym or buzzword you can think of) is simply to identify the things that will have to change over time, and design the system so that those changes are as easy to make as possible (translated; touching as few lines of code and affecting as few other areas of the system beyond the scope of your intended change as possible). Case in point, what’s most likely to change is that a Track will gain more data members that addTrack() may or may not care about, not that Track will be replaced with BetterTrack.