Branching By Abstraction
Recently at a client we had a discussion as a team about our continuous integration practices. One of the goals of continuous integration is frequently pushing changes to the master branch, so that it can be integrated into the build.
But how do you do that if you need to make breaking changes? You can push to master, break everybody’s build, and earn the undying enmity of your entire team if you do this regularly. Or you can try a couple of different approaches, such as Branching by Abstraction.
I can’t claim that I thought of the concept. I read about it from Martin Fowler’s excellent article (with example) Branch By Abstraction.
The Idea
Branching by Abstraction is really a form of refactoring. One of the goals of refactoring is that you always keep your tests passing. If the change you need to make is a breaking change, you can wrap your old implementation into an interface which abstracts away the implementation details.
Once all of places in the code which used the old implementation are now using the new abstraction layer, you can change the implementation behind the abstracte interface. All of your old tests for system behavior should still pass, and you can test drive in your new features.
The Starting Point
I’ve created a simplified example of the practice using Conway’s Game of Life. This example isn’t contrived though: I was working through a complete example in C as a way of honing my skills, and realized that I had exposed my implementation for the board, and that I wanted a different implementation. I also didn’t want to break my existing tests, because it described behavior that I wanted to keep.
You can see that the initial implementation of the board is pretty simple and straight forward. There’s also a game engine for counting a cell’s neighbors. It accesses the storage of the board class directly.
While you would generally never do this in the java world (the native language of my client’s team), this is a perfectly reasonable implementation for a program written in C for an embedded microcontroller (like my original version).
There are also tests to ensure desired behavior, because I greatly prefer tested code to untested. Also, I’m lazy, so I don’t want to be performing manual tests.
The New Feature
In my original design, I was simply going to use a grid of LEDs to display the board, with cells either on or off. Storing the cell values as a boolean value makes good sense in that situation.
But at the suggestion of a colleague, I want to have a visual representation of the age of my cells. I’ll track the number of generations a cell has been alive, and represent that either via the brightness of the LED or the color, depending upon the actual implementation of the visualization. I now need to track how many generations a cell has been alive for, so an integer representation makes more sense.
The First Steps
The very first step is to define an interface which encapsulates the behavior that I care about.
In the process of writing the tests for the board, I learned that there were three behaviors that I needed:
clear()
to set all values of the board to false (or in the parlance of the game, dead.)at(x, y)
to retrieve the value at a specific position.set(x, y, value)
to set the value at a specific position. The values of our existing board are true and false, but I can use this interface to handle another board for tracking the number of neighbors a cell has, so the value parameter will be an integer rather than a boolean type.
I haven’t changed any implementations yet, and in fact no class currently implements this interface. That’s okay, I just want to get it out there into my master branch. Among other things, if another team member is implementing a board like class, I’d like this interface there to prod them to implement it rather than spinning something new.
Implement Interface
The next step is to actually implement the interface. You’ll notice that it gets its own test suite, because I don’t trust untested code. I’ve been writing code for close to 30 years, so I’m well aware that I’m an idiot and that without tests I’ll make a horrible mess of things.
I also renamed the board class to LifeBoard, partially to avoid problems (on Windows and OSX file systems Board.java and board.java are the same file), but also because I fully intend for there to be a NeighborBoard later which will track the number of neighbors each cell has.
All of the tests still pass so this gets pushed to master.
The Transition
I begin moving the users of the board to this new interface in a series of small commits. After each change I run tests and push to master.
- the board’s tests are done first.
- The Game Engine Tests is converted next. Note that I only converted the syntax to use the interface. I’m still relying on the fact that the board’s storage is static, because I haven’t converted the actual game engine yet.
- The Game Engine itself is finally converted. To remove dependence on the static storage of the board, I have also made the board into a parameter of the constructor for the game engine. The tests are modified to account for this fact.
Hiding the Implementation
Now that all of the code is accessing the LifeBoard through the defined interface, I can Hide the implementation of the board. Tests pass and I push to master.
Implement Aging
With my interface hidden, I can now implement aging of my cells.
Conclusion
This bit of code probably won’t gain me fame and fortune. But it will look pretty sharp hanging on my wall once it’s expressed in hardware.
Post Script
Now that I’ve run this exercise for teams, I’ve noticed that my code doesn’t actually implement the aging feature that I want. If you look at the commit for “implement aging” above, you’ll notice that the at() function only ever returns 1 or 0, not the age of a cell. You’ll also notice that there’s no test covering this functionality.
I could hide my shame and fix the code, but I think this serves as a great example of why I prefer to write my tests first. As I said earlier in the article, I’m an idiot and I make stupid mistakes. Following my own advice, if I’d written the test first I would have caught this error.