A Test-Driven Implementation of Huffman Data Compression — Part 9 (final)
I followed a strict test-driven approach: write a test, run the test and verify it fails, write the code, re-run the test and verify it passes, refactor the code if needed. Repeat.
Adopting TDD has a few benefits:
- you focus on what the code is supposed to do, not how to do it;
- you can stick to simple implementations and refactor them safely later;
- you get a whole suite of tests emerging at the same time as your code;
- your code naturally comes along as a set of loosely-coupled components;
- testing-first is more fun (at least for me);
A few final thoughts in random order:
London or Chicago?
There are different schools of thought on how to implement TDD, for example London vs. Chicago. Some experts are really strict on how it should be done. I am not really interested in adhering to any specific approach.
In my series, I’ve just presented my personal way of doing it which is probably a kind of the mix of the two:
- sketch your class: write a skeleton that represents its behaviour;
- write a test to exercise that behaviour;
- write the code;
- refactor the code if needed.
SOLID TDD all the way down
In my code, I adopted TDD “on the fly”, by writing classes in a TDD way when needed. That doesn’t mean that a code like that cannot be designed upfront. I prefer to sketch the behaviour of my classes assuming that there will be external collaborators dealing with specific responsibilities and I create those collaborators when those responsibilities arise. That way, I think my code is more respondent to the SOLID principles.
In my opinion, adding new functionalities to a class should always be done in a TDD way, as well as fixing any bugs.
Is TDD really the best way to write code?
There are no silver bullets in life, and TDD is no exception to that rule. In my experience, however, TDD is much better than a traditional test-after methodology.
TDD works very well when pair-programming, and especially when mob-programming: people collaborating on a problem are unlikely to jump straight into coding; first, they must collectively understand the criteria of satisfaction of the problem to solve.
TDD helps a lot in that sense.
The idea is that acceptance criteria drive tests and tests drive development.
Does TDD takes a long time?
Whoever has been brave enough to read my 8 articles might be wondering if TDD is time-consuming. In reality, it’s all a matter of practice: once you adopt a test-first approach, that will come natural and developing code in a TDD way will not take longer than traditional coding. Bear in mind that, with a non-TDD approach, writing the code is just half of the job; the rest is creating the tests, which are boring to write after and “biased” by the code already written. In other words, they will likely miss edge cases that were not considered in the code already written.
A class to represent a bit, really?
Yes, I wrote a class to represent a bit. When I started my career as a C developer, such thing would have made my skin crawl.
Of course I did it for “didactical” (so to say) purposes, but this is the problem with object-oriented languages like Java: everything is a class. You are forced to follow a single paradigm, which is not ideal.
Also, the proliferation of so many immutable objects in memory might not be ideal in terms of footprint and performance.
The code that I wrote in this series can be optimised to a great extent. For example, a Huffman tree is immutable, so a HuffmanCompressor does not need to traverse it after the first time: after obtaining a Huffman code for a symbol, it could just store it in a cache to be reused later.
That is just a change to the internal logic of that class and the test is already there to make sure the behaviour of the class doesn’t break. However, should you write any collaborators for that class for that specific purpose, they should be developed in a TDD way too.
The power is in the algorithm, not the tests
Tests, even when written before the code, serve the purpose of validating the code, but they cannot guarantee correctness. What is important is that our algorithms are correct. Having a comprehensive suite of tests, with lots of scenarios, may help a lot in that sense, but there may be edge cases that get ignored or are too difficult to explore. Comprehensive tests can be very time consuming to build and difficult to maintain, so developers need to be sensible and ensure that the algorithmic foundation of their code is solid.
End of my rambling. That’s all! Happy TDD-ing! :)
You find the code for the 8 parts of my series here: