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
This code enforced that you could never write bugs about linking to the wrong page, or using the wrong
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
On the other side, which is how I would characterize the injected
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
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.
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
Post a Comment