Sometimes, the coding task in front of you can be overwhelmingly complex. Despite refining the user story, numerous unknowns remain. In such situations, you have two options: continuously refine the story, hoping for an epiphany, or start with what you already know.
Recently, I chose the latter approach: diving right in. Armed with a few assumptions about how things should work, I embarked on developing an application with a graphical UI and a backend. The goal was to compare two object graphs and present them in a way a user could selectively merge them.
My initial step was to identify the differences between the two object graphs. Nodes could be added or removed, and relationships between nodes might have been updated. This resulted in a collection of "change" objects. To tackle each operation, I started with a small test, following the principles of Test-Driven Development (TDD). If the tests became too complex, I knew something was missing.
As an aside, TDD is an iterative workflow that involves describing a specific aspect you want to solve in terms of the programming language you're using. This description takes the form of a test. Then, you implement a portion of the code that fulfills the test's requirements. Clean up the code, and repeat.
Implementing this first part not only provided me with the confidence that we could effectively compare two graphs but also paved the way for the next step: the front end. The aim was to display the changes in a tree view, enabling me to group related modifications. For example, when a node is added, it often involves adding a few related relationships. To construct the hierarchy, I did not require the GUI just yet. This was convenient as it allowed me to maintain a TDD flow without external dependencies.
However, the first attempts at this simple approach quickly revealed its limitations. Users and even I found it overwhelming to make sense of the changes. Clearly, the simple approach was too simplistic.
While refining the idea of how to present the changes to the user, the tests I had created along the way proved invaluable as guardrails. I could modify something and observe if the behavior changed accordingly. Sometimes, this meant the tests should pass, while other times, I expected them to fail.
By describing the expected behavior of the system as tests, I established a straightforward contract: a function call that, given an input, would yield the correct output. The tests didn't make assumptions about the inner workings of the code. Throughout the process, I had to fine-tune the initial comparison code. However, since I had built the code around the tests, the modifications mostly entailed adding new tests, and ensuring the existing behavior was still working.
The end result was three independent modules: one for calculating the changes, one for grouping them, and one for presenting the changes in the UI. The design emerged from writing tests upfront. Truly, TDD saved the day!