18 March 2021

Pragmatic refactoring with 99 Bottles of OOP

This article is sponsored by retest, a gem that helps you refactor ruby projects on the fly just like the methodology described in 99 Bottles of OOP. Don't forget to smash like, subscribe and leave a comment…. I mean… Try retest, star the repository and leave an issue, now back to the article.

demo Refactor code one change at a time with retest.

A great refactoring book

Last year, I read the amazing 99 Bottles of OOP by Sandi Metz, Katrina Owen & TJ Stankus. The book explores OOP concepts and how to refactor code while being one cmd + z away from green tests. It teaches 'practical techniques for getting things done that lead, naturally and inevitably, to beautiful code', by changing one line at a time. That is right you read that properly. For every single line of code that changes your tests should remain green. If they fail, then undo the change and try again.

How is this possible? 99 Bottles of OOP explains the technique thoroughly. It is a bit tricky at the beginning but just like any technique, it becomes simpler over time.

The gif above gives an example of how this is possible although this particular example can be considered cheating.

The methodology

The book is an ode to 'preparatory refactoring' by introducing a new feature for the 99 Bottles song. How can we replace any references of 6 bottles to 1 six-pack in the song? Simple to understand yet not trivial.

The authors go through the change following the open/close principle: Code is open to a new requirement when you can meet that new requirement without changing existing code. It is best expressed with this quote from Kent Beck.

For each desired change, make the change easy (warning: this may be hard), then make the easy change. Kent Beck

Make the change easy

The first step to great refactoring is a good test coverage to increase confidence that new changes are preserving the code functionality.

Good test coverage doesn't mean loads of tests and the book is a good example of that. 99 Bottles of OOP handles the entire refactoring with one simple yet reliable integration test: the code must be able to print out the full song after every new code change. One assertion on a single hardcoded string of 100 lines with no dependencies on the code being tested.

Developers tend to write a lot of coupled unit tests while shying away from slow integration tests or feature tests. I find it fascinating that the code in the book can be refactored with only this specific integration test. During the refactoring, the authors create new classes but no new tests. New classes are considered private and don't deserve any tests (just yet). Mind-blowing.

Keep calm and carry on

The authors tell you to trust the process even if the code seems far from being open to change. Keep calm and continue refactoring, ultimately the code will reach a state where the new feature can be introduced. This refactoring phase is more of a mechanical process as it doesn't require a vision or sparks of ingenuity. It looks like this:

  1. Change one line
  2. Run the tests
  3. If the tests fail, undo the change
  4. Go back to step 1

The authors teach simple effective rules to help identify areas requiring refactoring like the Flocking rule. Then you repeat simple known atomic changes from Martin Folwer's book refactoring and your code will become ready for change. Always.

Make the easy change

Only once the code is open do you go back to boring TDD:

Make it easy to understand

It's only at that point that the authors write unit tests to cover most, but not all, newly introduced classes. That is right, every class doesn't need unit tests when they can still be considered too small or private. This section of the book uses insightful methods to write tests that document and teach future devs how to use the new classes effectively. As tests are written, classes are still being refactored. Amazing chapter.

The infamous test

Finally comes my only point of disagreement with the authors of this great book. They decide after this journey to delete the integration test. They cover the entire refactoring with one single test only to… remove it without an ounce of remorse. Not even a thank you, Marie Kondo would be ashamed.

While not being a unit test, it should be kept. It was testing the only class already in use in the fictive codebase. The test makes sure that printing the 99 Bottles of beer song still works which is supposedly an important part of the existing business rules. RIP integration test.

Conclusion

The book is in its 2nd edition. Reading it feels like drinking water from a water hose. You can expect to spill most of it but will catch refreshing bits here and there.

It's one of those books, like POODR, which needs to be read a few times to start internalising the concepts properly. Just like any skill, theory is not enough. Read the book, practise, read the book again, practise again… Trust the process and keep looping. Every time you'll have new AHA! moments.

I recommend this book to developers with any level of experience and especially to senior developers who haven't refreshed their OOP basics for at least two years. This book will challenge your deeply ingrained practices and this exercise alone will make you a better developer. Who knows, you might even fall in love with factories again.

The book covers those topics well:

Retest gem

This book gave me the incentive to create a gem that facilitates this type of extreme refactoring: retest.

Retest is a small command-line tool to help you refactor code and works with every Ruby project (ruby, rspec, rails, rake). Designed to be dev-centric and project independent, it can be used on the fly. No Gemfile updates, no commits to a repo, no configuration files (like Guard) required to start refactoring. The gif at the start shows how to install and use the gem.

$ gem install retest
$ retest --auto

# Start refactoring by making a change and save the file

If you're interested in reading 99 Bottles of OOP while going through the changes yourself this gem is for you. It will rerun the infamous integration test after every change you make.