Deferred Quality: A Feature Pass on #buildingathing
So after the design pass, the thing that stands out as the "ugliest thing" isn't actually the design anymore. I have a few feature indicators in the UI. Things that I think I want but aren't built yet.
When the design is the most offensive thing again, I'll work on that, but right now, those missing features (from the indicators, which are mostly dead links and notes in the README) are what stand out as things that need to be addressed the most.
For this pass, which amounted to an evening of work, I needed the "saved jobs" list to be filterable and to have some edit in place functionality.
Here are the things that came to mind while working. These are not "principles of software development." They're fairly bad for any workflow with more than one person. And the first one especially doesn't apply when you have a designer, a spec, or some other form of requirements. They emerged as I was working, and seem notable at this stage.
- Bang for your buck
- Elegance later
- Manual testing
Bang for Your Buck
My "compass" while building is a completely workflow-based process. I try to use the thing. When I find myself wanting a feature, I build it. In most cases, I already had markers for the features (some combination of: dead links, a todo in the README, or a note to build something [here] directly in the UI). The first and last of those are luxuries of working on something unreleased, and one thing is worth calling out there.
A couple things worth mentioning. I don't have TODOs in a section of the code. My README notes are just 2 or 3 word descriptions of workflow features. The code is too ephemeral to put that high-level info there. The UI doesn't contain "TODOs" either. It has things that don't work, facades of features.
So while I'm in the workflow, I see signs for a feature, and it's missing. My implementation guesses as TODOs in the code would likely be bad, and would be more instructive (build this thing). In the UI, they are mostly links.
So I'm in the workflow. I see a few links. What do I do first? I make the feature of the link that I want to click on. If I end up not ever feeling the need to click on something, then I can just drop the anchor tag from the UI.
Could I have figured out what I wanted up front? Maybe. As for one example, I had two features in mind, "notes" and "todos" for a job listing. These were thought to be two separate features when I was planning. But as I was using the site, I got the impression that notes (adding words to a job posting) would be something I would use instead of an inline mini todo list pretty much all of the time.
So I'm going with my instinct there and not building the todos. Later, I might find out that everyone really wants that. I can deal with it then.
Put another way if I feel like the workflow is handled fine by some other feature, I don't need that duplication. It's not worth the time to build it, and it's not worth yet another thing to stuff into the UI.
Elegance Later
When I am on a team or working on an already complex project, I need more structure than I do on this project. The type of code "improvements" I advocate for and implement in a team project are normally one of the following:
- commit messages and sizes
- pushing deeper
- fixing duplication (db normalization as well)
Commit Messages and Sizes
One type of discipline that frequently comes into a shared codebase is in version control. Tiny commits, then later "squashed" without the noise for pull requests.
On a team, I believe strongly in the tiny commits and frequent push idea, as it's good for visibility. I'm less certain on the value of squashing as I've seen its advocacy orders of magnitude more than I've seen where its effects (searching through past commits) have or would have borne fruit.
But on this project, I have two different types of commits. I'm committing/pushing as frequently as I can (for backups sake), and very intentionally commit immediately after I solve a problem of configuration. For the first type, I don't care about the commit message. I add, commit (with a message like "update"), and push to the remote all in one command. When I actually solve something, finish a feature, or (especially) when I finish a feature that has a tricky (and likely to break later) config, that's when I get more detailed. This second case might be where you could use git tags. But for me, scanning a list of a few dozen commits labeled "update" with the actual milestones spelled out intermittently gives me enough info to check out past versions.
One thing to note here is that I commit everything that isn't git ignored. I don't add chunks, and if I don't want to push a file, I add it to gitignore. Having persistent (oh I don't check that in files) slows down the process, so anything generated is either gitignored or replaces old versions of itself.
Pushing Deeper
In an older codebase, supported by many people, the "deeper" the logic is, the better (one caveat here is some programmers, especially on the junior side, really hate chasing logic through the stack). Having a very thin UI means more backend code (proportionally). In rails's case, this means moving things from the UI (views), into controllers, then models, then concerns, scopes, ruby classes, and eventually libraries of their own.
As the logic moves back, it is easier to test pieces in isolation. Also, there is less duplication when you can abstract larger and larger pieces.
I am not doing that.
I want the code to be as flexible as possible at this stage.
Fixing Duplication
Another thing I'm not doing upfront is fixing duplication. If things are kind of the same but not quite, that is not a good reason to abstract. I have inline css styles that are repeated, I have a denormalized database (some tables have data that looks like it should be shared), and I have routing logic with multiple places that only support one tiny update.
All of this duplication could be centralized in respective "good" ways, but that is NOT a good path for a workflow-based building approach.
Story Testing
I forget who said it. I think it was Matz (Ruby creator), but anyways, the quote was something like:
Some good code reads like an equation. Some good code reads like a story.
Underpinning the whole #buildingathing process is that I am making workflows. As I'm conceiving of them now, those workflows are much more similar to stories than equations.
I am reading those stories in the code and in using the site. They are sometimes branching but fairly linear stories. The more abstraction I introduce, the less the code resembles a story and the more it looks like an equation.
Anything I can do to preserve the story will help me stay connected to the workflows.
Manual testing gets hard with a complex codebase not only because of the number of paths that exist, but also in how branching these paths are, and how varied the testing is for each layer of the stack.
By manually testing everything at the UI level, I stay connected to the stories of the code and the experience as well. Also, having independent stories in the code (the more similar code that exists without being unified and abstracted) not only makes for a more linear testing experience, but allows for more confidence that by changing something in one place, you're not breaking it in another.
Stories vs. Equations
It might seem like I'm advocating for every aspect of the software to be "bad," and I've certainly argued the opposite in the past.
The context here matters a lot. For one person, actively engaging in workflows and stories to drive development, equation-based code and abstraction have upfront costs. They shift focus away from the experience and can too-quickly optimize for unnecessary ways of completing tasks.
I do believe that in this project, a shift to more traditional "quality" will emerge. At some point, enough small variations pile up that I can't help but parameterize the stories. At a micro-level, this has already taken place a few times. What was html became an array and is now a handful of database records. The driver for that abstraction was the workflow itself. I needed to generate new and update old data from various places. I couldn't help but centralize that deep in the stack. (It's worth noting here that part of that process is actually admitting a broader scope to global variables, collected by the database).
That (support for various, but guaranteed workflows) is going to remain the primary driver for abstraction and other "quality" improvements.
In the future though, I can anticipate other drivers. Should I work with someone else on this, I'll need more tests and consistency. And when I become less connected (in time) to building the workflows, I don't think I'll want to inherit a story-based codebase. But until the workflows support the love letters, I need to stay connected to a story approach for the code as much as possible.