I’m continuously trying to find ways to make my code better. I experiment with a lot of tools for code correctness. I play with constructs to make my factoring better, increase code mobility, and decrease complexity. I like these because I can find things that work and distill them into rules. Maybe I have rules on what references I allow in headers, or maybe I bias towards functional code over OOP to minimize coding by side effects. Whatever.
It is a bit more difficult to evaluate things like processes or methodologies. Take Test Driven Development (TDD) as an example. It’s difficult to make a solid case against writing test code (though occasionally I see a persuasive argument). The arguments tend to be self reinforcing – you want your code to stay correct over time with changes, you want consistency in QA, you want reduced sustained engineering costs, etc. TDD makes the suggestion that test code should be written before production code. The argument is that writing test code first puts a soft upper bound on the complexity of production code, since you cannot write the test code without understanding the production code behavior.
I have this code that I wrote a very long time ago. It changes the position and size of a dialog’s child controls when the dialog changes size. It’s not particularly complex, but it has some math that most would just gloss over.
int xPos = m_original_listview_rect.left;
int yPos = m_original_listview_rect.top;
int cx = new_client_width - m_original_client_rect.right +
m_original_listview_rect.right;
int cy = new_client_height - m_original_client_rect.bottom +
m_original_listview_rect.bottom;
HWND hWndDlgItem = GetDlgItem(hWnd, IDC_LIST1);
MoveWindow(hWndDlgItem, xPos, yPos, cx, cy, FALSE);
xPos = new_client_width - (m_original_client_rect.right -
m_original_button_rect.left);
yPos = new_client_height - (m_original_client_rect.bottom -
m_original_button_rect.top);
cx = m_original_button_rect.right;
cy = m_original_button_rect.bottom;
hWndDlgItem = GetDlgItem(hWnd, IDC_BUTTON1);
MoveWindow(hWndDlgItem, xPos, yPos, cx, cy, FALSE);
I didn’t write this with TDD, as TDD wasn’t in vogue in ~2000 when I wrote it. The code isn’t particularly complex, and isn’t particularly great. It looks testable, and probably could have used TDD. But what isn’t obvious to me is how TDD might have made the factoring better.
When refactoring some code for GitHub recently, I came across this and decided it was a candidate to make better-er. I pushed and pulled at the code. I messed with the math. I played with the units (should a rectangle be defined as two points, or a point and a size?). I attempted tracking the minimal state needed to recalculate everything. I ran it through a debugger and watched the numbers actually behave. I played with redundant sub-expressions. In the end, I came up with something like this (the controls are a bit different, but the idea stands):
void reposition_window_by_offset(
_In_ HWND window,
_In_ int control_id,
const RECT& original_control_rect,
const POINT& position_offset,
const SIZE& size_offset)
{
HWND control_window = ::GetDlgItem(window, control_id);
::MoveWindow(control_window,
original_control_rect.left + position_offset.x,
original_control_rect.top + position_offset.y,
original_control_rect.right -
original_control_rect.left +
size_offset.cx,
original_control_rect.bottom -
original_control_rect.top +
size_offset.cy,
FALSE);
}
int offset_width = new_client_width -
m_original_client_rect.right;
int offset_height = new_client_height -
m_original_client_rect.bottom;
POINT position_offset = {};
SIZE size_offset = { offset_width, offset_height };
reposition_window_by_offset(window, IDC_LISTVIEW1,
m_original_clientspace_listview_rect,
position_offset,
size_offset);
position_offset.x = 0;
position_offset.y = offset_height;
size_offset.cx = 0;
size_offset.cy = 0;
reposition_window_by_offset(window, IDC_MYLABEL,
m_original_clientspace_label_rect,
position_offset,
size_offset);
I like this code. Its a bit longer (in terms of LOC), but its immediately obvious what is changing on each of the controls. The calculation code that nobody reads is separate from the code that states what needs to change. If I started with test code, I have no idea how I would have ended with the second implementation. Even for simple things, often times you need to mold the code. This is where languages like Python and Lisp, which have a REPL loop, really excel, as they provide a playground for this type of experimentation.
Arguably what I’ve done here is the refactoring step of TDD, so I could have just made a test against expected outputs. However, I wasn’t even sure of the format of my inputs, including what shared state there was. I wasn’t even sure of the number of functions I’d end up with. What I’m making a case for is exploratory programming as a tool for requirements gathering. And if writing code is used for gathering requirements, perhaps the idea of writing test code first should be reexamined.
This is hardly damning for TDD, but it’s hard to know where TDD might be applicable if I didn’t see how it would work for a toy example. I worry that TDD might be focusing on the wrong metric. In my world, the problems I deal with aren’t on the function level, or often even on the module level. They’re race conditions. They’re scalability issues. They’re issues with information leakage or security. I don’t know how TDD might catch those issues. I am concerned for the developer that uses TDD as a crutch to avoid thinking through code especially if they are the same devs that need to worry about threads, scalability, or security.
I’ll keep an eye on this space. I still have a nagging feeling this is useful somewhere, but I can’t quite put my finger on it.