03. Enforcer Pattern: Composable Guard Clauses // JavaScript Codecasts

How can you cut down small if-else statements
that recur across several functions? Let’s cover another pattern for nuking if-else
statements on today’s episode of TL;DR, the JavaScript codecast series that teaches
working web developers to craft exceptional software in 5 minutes a week. Over the past few episodes, we’ve been covering
design patterns to help cut down the size and depth of if-else statements. If you’re new to this vendetta against if-else
statements, hop back to the episode on nested ternaries to get up to speed. Nested ternaries and the Router design pattern
have helped us reduce the size and depth of cascading if-else statements, but we haven’t
dealt with terse, non-cascading if-else statements that get copy-pasted across functions. These if-else statements often appear at the
beginning of the function as a guard clause. They’re innocent and short, but like a weed
they reproduce with each new feature, and the duplication is tricky to eradicate. Today we’re continuing to work on a chatbot
that helps outdoor enthusiasts find great trails to hike. This chatbot can respond to simple text commands,
like “list hikes”, “add hike” and “delete hike”. If it doesn’t understand the command, it
replies with a fallback message. The code is a few steps forward from what
we had last time: the responder function still follows the Router pattern, but we lifted
the individual routes into functions to make the list of responses easier to read. The responder function searches through the
list of responses for a command that matches the chat message, then invokes the corresponding
response function. Today, we want to enforce that the “add
hike” and “delete hike” commands are executed with the word “sudo” to prevent
any accidental changes. Only some commands need sudo, and if the user
forgets sudo, we want to provide feedback. So we can’t just add the word “sudo”
directly to the regular expressions. We can make the regular expressions a little
more lenient so the command is at least recognized, but how should we enforce the use of sudo
for these admin commands? One tempting way to support a new, shared
behavior like this is to add a new property to each response object: we’ll call it “adminOnly”. Then in the responder, we’ll add a guard
clause that checks if the route requires “sudo”, and if the word is missing, we’ll respond
with “Not allowed.” When faced with this kind of feature request
— that is, supporting a new behavior that can be generalized for related functions — many
developers would probably do what we did and insert that behavior logic into the responder
function. It’s quick, keeps the code DRY, and it just
feels nice. But it’s also a premature abstraction that
conflates responsibilities: the responder function has become responsible for routing
and authorization logic. Every time a feature requires a new qualifier,
the responder will be edited. It won’t be long before there are several
short if-else statements in the responder — which is precisely what the Router pattern
was intended to help us demolish. From a testing perspective, we can’t unit
test the authorization logic for individual chat commands without going through the responder. We can only write integration tests for authorization. Whenever you’re tempted to alter terse,
single responsibility functions to incorporate a new behavior, take a step back and identify
the most naive solution that still satisfies the single responsibility principle. For example, what if we added this admin enforcement
logic directly to the addHike() and deleteHike() response functions instead of the responder? Let’s undo our changes. For the response functions to determine if
sudo was used, we need to pass the full chat message. In addHike(), we can add a guard clause that
checks if the message starts with “sudo” and returns “Not allowed” if it doesn’t. We can copy-paste this guard clause to deleteHike(). This naive solution is feature complete and
leaves the responder function focused on one responsibility. But now one if-else statement has multiplied
into two in our response functions. So how are we any better off? Well, by letting the naive solution play out,
we’re equipped to build an abstraction that solves a concrete problem: the duplicated
guard clause. This guard clause represents a behavior, which
we could call “adminOnly”. When you hear the word “behavior” or “trait”,
we’re referring to a cross-cutting concern that can be shared across several functions,
even if they do completely different things. The addHike() and deleteHike() response functions
have different jobs, but they share a similar behavior. A great way to share behavior in a language
that supports functional programming is through function composition. Suppose we had a function, called adminOnly(),
that receives an unprotected function like addHike(), and returns a new version of addHike()
that enforces the use of the “sudo” keyword. adminOnly() is easy to code up once you get
the parameter signature right. If the message contains the word “sudo”,
it invokes the route it received as an argument. Otherwise, it returns the failure message. I like to call this kind of behavior function
an enforcer: it’s a Higher-Order Function with a guard clause that enforces some authorization
rule, like requiring the word “sudo” or checking if the current user is an admin. The “add hike” and “delete hike” commands
behave exactly as they did in our first solution. But this time, we didn’t have to edit existing
functions to support the new behavior: we only added new functions and composed them. It’s as though we’re writing immutable
code, and like immutable data structures, this style of coding has great design benefits
and prevents regressions. None of our existing unit tests will change,
and the new code already follows the single responsibility principle. We can even add new enforcement behaviors. Suppose we want to enforce that the “list
hikes” command include the word “please” with a new behavior called askNicely(). All we need to do is duplicate the adminOnly()
behavior, then change the keyword and failure message. And because these enforcers are built through
function composition, they layer without additional work. To make the “delete hike” command require
“sudo” and “please”, we just compose the behaviors. But what about the duplication between these
behaviors? Other than a different keyword and failure
message, they look exactly the same. We can DRY them up into an enforcer factory
called requireKeyword() that returns a new behavior based on a customizable keyword and
failure message. Now the adminOnly() and askNicely() behaviors
can be replaced with partial invocations of the requireKeyword() enforcer factory! We’ve landed on a solution that satisfies
the single responsibility principle, didn’t change existing functions, and produces descriptive
code. The enforcer pattern pops up in other places,
like guarding authenticated pages in a React web app, rendering a loading indicator while
an API request finishes, or protecting backend routes based on the current user. But we wouldn’t have discovered this pattern
without writing the naive copy-paste solution first and letting the repetition guide the
refactor. So don’t try to prevent copy-paste prematurely:
instead, let the code be duplicated, then DRY up the duplication through function composition. The naive copy-paste solution will lead you
to a resilient abstraction that won’t be outgrown by the next feature. Today, look for short, repeated if-else statements
near the beginning of the function that guard the rest of the function, and try extracting
them into an enforcer function. That’s it for today, you can get a transcript
of today’s episode and catch up on other ways to craft exceptional code at JonathanLeeMartin.com/TLDR,
and if you want to keep leveling up your craft, don’t forget to subscribe to the channel
for more rapid codecasts on design patterns, refactoring and development approaches.


Add a Comment

Your email address will not be published. Required fields are marked *