Every developer has heard terms like loose or tight coupling yet still a lot have problems maintaining coupling in their codebase. Let’s take a look into some code and try to identify where it is tightly coupled and refactor making it more maintainable and testable.
Facing a code
So here is a class:
A purpose of this class is simple - it should be able to list relative or absolute paths to all files that are stored in a USB drive and are of appropriate, supported file type. Now pause for a moment and take a look onto that class and try to identify all places where it is tightly coupled.
Problems are rising
The easiest way to identify coupling is finding all references to other classes. Each time you’ve encounter reference to other class you should ask yourself: is this class really needs to know all that about that other class?.
Our example references following classes:
Let’s try answering above question for each of them:
Dir- it is a class from a Ruby’s standard library. There is in fact nothing wrong referencing classes from the std-lib and this kind of coupling can be safely left and refactored only if there is a good reason behind that.
Document- that class provides us a list of supported files. But do we really need to know that this list is in a
UsbKey- this class is used to get a path to a directory where USB is mounted. But do we really need to instantiate that class to just invoke one method on it?
So we can say that coupling to classes
UsbKey is tight.
But there is one more subtle kind of coupling in this class.
Huston now we have a real problem
You can argue that referencing to
UsbKey classes is
a big problem. Maybe, but let me ask you something. How you would
write a test for that class? Think about it for a while. How you would
test if this class correctly lists file paths?
Current implementation is not only tightly coupled with
It is tightly coupled with a USB itself. You will need to have an USB
stick plugged into your machine to make tests passing! Of course you
could try mocking and it will work but let’s check how it might look:
It will work. But just because it works it doesn’t mean it is good. Firstly this amount of mocking for such a simple class should be already suspicious. Secondly what this code is really doing is mocking class internals and you shouldn’t care about class internals. Burn it into your head: never, ever mock object internals. Never. Just don’t do it. Everybody will be more happy. By internals you should consider all private methods, state and all collaborators which are not injected into that class. If you ever need to mock one of these in order to make code testable it means that your design is wrong.
If you are more interested in why you shouldn’t mock object’s internal state (also called implementation detail) check Ian Cooper’s great presentation.
Decoupling for the win
So lets try to refactor that class this time doing it right. Let’s start with writing some tests:
We are expecting a class
File::FileList to provide 2 methods
one to return absolute second to return relative paths. Both should
include only paths to supported files however I’ve skipped that
for simplicity and in fact current specs will cover that. In production we
could add appropriate examples for documentation purposes.
We need to setup some directory and files structure as a fixture:
spec/fixtures/lib/file/file_list_spec/ subdir/ file2.rb file3.py ignore2.exe file1.rb ignore1.exe
Now we can imlement example for
Time to move some code to our new class:
I’ve introduced a list of supported files via optional hash parameter (in ruby 2.x we would use keyword arguments) that should be tested in separate context, but I’m not gonna to do that in this post.
Given code passes the specs so we can implement example for
Now we can move remaining code to a new class:
And we’re done!
Let’s summarize work we’ve done in the refactoring:
- We’ve removed coupling to
Documentclass by providing a list of supported files in a constructor.
- We’ve removed coupling to
UsbKeyclass by providing a path in a constructor.
- We’ve removed coupling to USB mount location making a new class more generic and potentially useful in other cases.
But most importantly by making a class decoupled we made it perfectly testable without a need to mock any of its internals. Not only the class is more useful but the code is more maintainable now. And thats the real profit of making objects loosely coupled.
Each time you are instantiating a class within some other class think do you really need to know that object. If there is only one particular information you need from it or you want to call some method but you are not interested on object’s state, probably you can achieve the same by injecting that object and decoupling things. This way you could test classes with ease. Limiting collaborators is a great and simple technique to achieve testable and maintainable code without much hussle.