Skip to main content

Convention vs. Abstraction to Prevent Bugs

When I was working on Blogger, I caught the dependency injection bug and led a conversion of the frontend code to a new, DI-heavy pattern. This greatly helped our testability, as it let us move away from the service locator anti-pattern we had been relying on.

I got a bit carried away, though. One piece that I generalized out was the concept of a link to another part of the UI. All these links had to have a blogID query parameter to work, but exposing that to the page controller was a mixture of concerns, right? We could instead inject type-checked BlogUri representations of these URLs into the page controllers, and they could render them without ever having to know the right blogID to use.

This code enforced that you could never write bugs about linking to the wrong page, or using the wrong blogID parameter. Useful, right?

Nope.

Those are two bugs that people basically will never write. If you’re on a settings page, linking to a template page, there’s no circumstance where you’d mix up what ID value to use: there’s only one! And if you did typo the URL path, you’d immediately catch that when you went to exercise your code, fix it, and never have a reason to mess it up again.

I added a fair amount of complexity and cognitive overload to prevent two bugs that weren’t going to happen to begin with.


The reason this is an interesting topic is that I believe that anyone in a position of technical leadership, who’s choosing / setting up / developing frameworks on a project, has the responsibility to make bugs harder to write. One particularly wants to prevent bugs that lead to security vulnerabilities or are difficult to notice when they’re there. HTML escaping template values by default is a good example of preventing this sort of bug. The automated authorization check detection SamN and I wrote into Fabric’s GraphQL server is another.

This can be additionally important when sharing tech across teams or when developing with junior engineers in mind. No one can catch a bug that they don’t know to look for.

Noticing when these problems might come up takes some experience and guesswork, and, in the case of the Blogger code I wrote, there is a cost to getting it wrong. You can add complexity that doesn’t solve any real problem, making the codebase harder to use for other teams or others who aren’t intimately familiar with it.


Here’s a classification for thinking about how to prevent predictable bugs:

One strategy is convention. This is characterized by using familiar concepts (like query parameters) that have to be used in specific ways to avoid bugs (calling them blogID for example). The advantage here is typically a lower cognitive burden for the pieces involved, with the disadvantage that the convention itself needs to be taught, such as through documentation or copying existing examples.

On the other side, which is how I would characterize the injected BlogUri objects, is abstraction. By abstracting away mechanisms like query parameters we can keep developers from getting them wrong, but now they’re in unfamiliar territory in our codebase. It’s harder to get up to speed and navigate the existing code, and you don’t want to know how bad it gets when there’s a bug in the abstraction.

Abstractions can often avoid boilerplate that plagues conventions, but are prone to being leaky.

In many ways, the most preferable solution is something we can call a checked convention. In this case the developer uses the mechanisms and concepts familiar to them, but the system will detect and report when they’re used incorrectly. One great example of a checked convention would be an interface in Java: methods are the familiar mechanism, naming them correctly is the convention, and the static analysis the compiler does to ensure the interface is fulfilled is the check.

Checked conventions let you write in ways that you’re familiar with without learning unfamiliar abstractions. When written well, the checks only make themselves known when you violate them, so the cost to the developer is low. The GraphQL authorization checks we wrote for Fabric fall into this category: they just pop up to make sure you’ve followed the convention of specifying authorization, since it would be very easy to forget to.


I’m currently thinking through these issues in the realm of server-side rendering component-based apps. The gist of the problem is that on the client, an app can render a loading spinner, decide what data to fetch, wait for the data, and the re-render when it’s available. The app is in some sense only accountable to the user experience. On the server, however, all data fetching must happen before the initial render, even before the components are initialized. (Complicating this is that rendering itself is the sole source-of-truth for what components will be on the page.)

The challenge is to structure your app in a way that allows you to, on the server, fulfill the data requirements of components before they’re initialized and rendered, then on the client pick up right there without re-fetching everything the server already got.

If you get this wrong, you lose the benefits of server-side rendering or make your app less performant with a double fetch. Both failures can be hard to spot, however. The client will fill in missing server data when it starts up, and a redundant data fetch is easily overlooked. That makes these bugs absolutely candidates for things we should try to prevent people from writing.


On the convention side of things, there are ways of attaching fetch methods either to routes or page-level components. The server could access these through the router, call them, and proceed when their promises resolve. In this way we could stick to concepts that would be more familiar: AJAX requests, or perhaps Flux actions.

One checkable condition that React already handles is showing a warning if the initial client-side render didn’t match the server-side render. Perhaps this is sufficient. Other checks could be to error if the initial render seems to synchronously trigger actions or fetches, or if the fetchInitialData method is undefined on a route-matched component.

The abstraction solution for this is to adopt something like Relay. With Relay, components declare their data needs in terms of GraphQL queries, and the library abstracts away fetching and storing that data. An abstraction like Relay makes it feasible to handle server-side rendering transparently: components are already declaring what data they want, so we immediately know what’s needed for the initial render. And, we can hydrate the initial client-side data at the library layer, so components don’t need to handle their initial rendering any differently.

The obvious drawback to this is that developers need to make themselves familiar with Relay. Though it’s more likely to be easier to do this with a public library like Relay than a home-grown abstraction in your app, the burden is still there. My personal experience is that in the general case Relay is straightforward, but where it leaks it can be hard to manage. But, anecdotally, I’ve heard that it has the reputation for being overly complicated and folks are avoiding it for that reason, so my experience may not be typical.

Given my preference for GraphQL I may find myself doing more with Relay, but I’ll probably give a convention-based, redux / redux-pack / lokka (or apollo) solution a go just to see how it feels.

Comments

Popular posts from this blog

Dispatching multiple Redux actions

I was working in my Redux app today and was facing the question of how best to submit a “create” API call to the backend and then transition to a “confirm” page. I wired up my router to be sensitive to the Redux store, so a reducer could trigger the page change. I’m using redux-pack to handle async events, which I find quite pleasant so far (though I haven’t gotten deep into testing it, which is a bit awkward because it requires faking out some internals). One solution is to have the component that is handling the “submit” button dispatch the API call action and then use the Promise that redux-thunk provides as a return value to chain the router action. But, the Promise will always resolve when the action completes, even if the API call was not successful, leading to some awkwardness in detecting if the create actually happened without error before doing the redirect. I also have a fantasy of generically dispatching actions via POSTs on the backend (“isomorphic Redux,” if you w...

Separation of Concerns Leading to Implicit Dependencies

I’ve been building a toy app to feel out server-side React, Redux, GraphQL, &c . to explore ways to build my next webapp(s). One thing this has introduced me to is react-router . react-router has good support for SSR because it lets you statically determine the set of Component classes that match a route so that you can load their data before rendering them. Its route descriptions allow nesting, which translates interestingly to your rendered UI. Here’s my routes.js file: <Route path="/" > <IndexRedirect to="/rails/rails" /> <Route path=":ownerName/:repoName" component={App}> <IndexRoute component={IssueListPage} /> <Route path=":number" component={IssuePage} /> </Route> </Route> This is used to make pages that look something like these: (When in doubt, make a GitHub viewer.) The App component renders the header with the repo name, and react-router passes it a child component,...