Aight. I been away from programming for a couple of months, but there was a reason I started talking the other day about the kontentment project: I’m wanting mucho change in it.
For a talk I’m giving, I want the ability to draw human arcs, with the same ease with which I can draw human lines. So I set out today to get that in.
Human straight lines start with a line segment AB. Pick two random locations on that line, so we got 4 points. Now jiggle all four points a little — that’s official terminology — and make them the four points of a cubic bezier: start. handle1, handle2, end.
I "invented" this algorithm by going to a live JS site that lets me play with cubics visually and, uhhh, goofing around with them. At that time, I didn’t know how to interpolate cubics, but learning that was part of it, too.
The upshot: I can cause lines that look as if a human had drawn them with a marker to appear as if the drawing were happening live.
I further added optional arrowheads at either end, which was also something I had to learn how to do. (I am not a math/geometry head, I had to slog through a lot of articles I couldn’t understand to find ones I could make sense of.)
Two geepaw-isms: GAK means "Geek At Keyboard", and GAK testing means testing your code by running the app over and over and seeing what it does. RORA means "Runs Once, Run Away", and refers to the practice of leaving bad code that passes GAK tests, moving on even tho it’s awful.
So this code was GAK-tested RORA. I have all of the normal excuses for this, but the key one: damned if I could figure out how to test most of this app at that point. More correctly, how I could profitably test most of this app at that point.
There were/are various schemes one could use to create tests, but all of them would be basically automating the whole damned UI and comparing old and new snapshots looking for differences. That kind of test is a) slow, b) large, c) uninformative, and d) unlikely to find anything.
The JavaFx graphics engine is multi-threaded, double-based, auto-scaling, anti-aliasing, and has no cheap "headless" mode. Tests that can only run on my exact hardware are not our friends.
And besides, I had the thing running, I used it for dozens of presentations with hundreds of human lines, and it unquestionably worked. Not bloody likely I was gonna change all that, ya know?
Setting aside tests and testability, the code is a work of extraordinary crystalline genius, or, I mean, it would be, if I’d written it very well. But I didn’t. And working so testlessly, refactoring to get it there seemed like an infinitely delayable goal. MORE FEATURES!!
Anyway, there I was, nekkid as a jaybird, and I’m wanting these arcs, and I’m wanting them to be "just like" the straight lines. There’ll be minor differences, but not so much. Let’s re-use the code! Let’s refactor the code so we can re-use the code! Yay, team!!!
So I go study the code — it’s been well over two years since I RORA ‘d it. And my goodness, what do you think I found?
Uh-huh.
Exactly what you’re guessing I found.
The factoring is awful, with the code in the wrong place both "objectfully" and "temporally". Though the two most heavily used component classes are well-done and thoroughly tested, the stuff the hoops they’re jumped through are largely opaque. It’s not good, kids, it’s not good.
The algorithm I described above is correct and accurate. It is by no means expressed in the code as simply as I expressed it up there.
Now consider doing "just like" that algorithm, only with an arc. Take line segment AB and a height. Make a cubic that pulls the line up so that at its maximum distortion from AB it’s height pixels away. I did that first thing this morning just to confirm the concept for myself.
I mean, take a look, if you want:
But it looks exactly like a computer drew it, and there are no arrowheads, and that’s not "just like" the straight connectors.
Arrowheads first: the existing code computes them at a bizarre time, and is deeply committed to the original line segment’s starting point for its orientation. But that won’t work here: they should be roughly oriented back to the "height" value.
Then humanness. In the straight line code we "cubic-ize" a line segment then jiggle the control handles. This inhuman arc isn’t a line segment, it’s already a cubic.
Okay, fine. We’ll pick two random points on this starting cubic and split it at those two points, yielding three total cubics. Then we’ll jiggle the control handles on all three of them, and sit back and wait for the stock options to mature.
So that’s the plan, but in the interim, I finally figured out what I needed to do to raise my tests and testability in all this crap, so I want to do all that, too.
Two straightforward resolutions to the testing issues, and both of them I’ve applied a thousand times in my TDD career, but what can I tell you, somehow they didn’t feel doable back when I first RORA’d this. They’re both easily doable.
- JavaFx works, at least as well as any library that is built-in to one’s language. Don’t test it. Put the *geometry* and *logic* in non-JavaFx classes, and test it there.
- Geometry works. Don’t test it. Test that your implementation of geometry works in all the known readily calculable cases you can think of, up until the day your GAK work reveals you haven’t thought of them all, then add some more.
These two notions, as I say, are just basic chapter 3 TDD, and I can’t think why I didn’t see them both before. Or, I spoze I can: solo work, too few advisors who insist on microtests, MORE FEATURES, stress of being both geek & product for myself, all the usual suspects, really.
So there ya go. I started. I wrote a tested ArrowHead class from scratch, essentially copy-paste-heavily-edit the stuff in the existing Connector. This time I made it not assume orientation-point. I tested it using the four 90 degree cases.
Next step, refactor the existing Connector code so that it can drop its own implementation and use the actually-tested single-responsibility constructor-only API new class instead of its old in situ hack.
Once I’ve done that, I can GAK test older scripts and make sure they still render arrowheads "correctly". (A founding premise of kontentment is that actual pixel-for-pixel accuracy is almost entirely irrelevant to my purposes in having it, so some variation isn’t an issue.)
And that’s the state of play. Once I’m happy with the extracted and testing ArrowHead, I will tackle the humanness issue. I have only a broad notion of how that will go.
I’m not here to offer morals to this story. I have some — for myself — but instead, I just wanted to share more of what "my actual coding" looks like.
Too much of our pedagogy is about how perfect humans write perfect code by making perfect decisions, and that’s just not what actual making is like.
I’ve a rep as a "TDD expert". I don’t want to spend that by spewing psuedo-algorithmic psuedo-axiomatic TDD bullet points.
I don’t think that works. I don’t know if these detailed stories will work better, but one of my mottos is "Try Different, Not Harder".
So I am.
Thanks!
GeePaw Hill
GeePaw’s Camerata is a community of software developers leading in changing the industry. Becoming a member also gives you exclusive access and discounts on the site.The GeePaw Podcast
If you love the GeePaw Podcast, show your support with a monthly donation to help keep the content flowing. Support GeePaw Here. You can also show your support by sending in voice messages to be included in the podcasts. These can be questions, comments, etc. Submit Voice Message Here.