Guard clauses, on the surface, sound like a great idea. They can reduce conditional complexity by exiting a method or function early. However, I find guard clauses used in the real world to be of little value. Often polluting application-level code for trivial preconditions. I will refactor some code to push those preconditions forcing the edge of your application so your domain focuses on real business concerns.
Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.
The most common case of guard clauses is doing null checks. To illustrate this, I looked at the eShopOnWeb sample application and will use it as an example.
In the example above, two guard clauses do null checks on the anonymousId and userName. While this is incredibly common, I’d rather not have to deal with these preconditions mainly because this method is in the BasketService and a part of the core application code.
Often these types of preconditions are very inconsistent. Since the userName is likely passed around through various layers, does each method that accepts the username have this guard clause? Likely not.
As an example, the above code creates a new instance of the Basket passing the userName to the constrictor. Here’s the constructor.
Sure the TransferBasketAsync method was doing the null check, but does this Basket class get created anywhere else? Is it doing a null check? As you can imagine, if you did this everywhere, you’d have a ton of repetitive trivial code for these null check preconditions.
Forcing Valid Values
I don’t want to litter my core application code with trivial guard clauses, such as null checks, as my example. Instead, I want my core code always to accept valid values so that it doesn’t need to concern itself with doing these guard clauses.
In doing so, you’re pushing the responsibility to the outer edge of your application to produce valid values. If you think about a web application, there is some translation from an HTTP request into your application code. You want to force that translation at the edge, which is your web endpoint, into valid domain values.
One way to accomplish this, as in my example with the userName is to define a type (a record struct) that, during creation, forces the value not to be null.
Then we can use this type wherever we accept the userName as a parameter. Instead of accepting a string for the anonymousId and username in the TransferBasketAsync method, we can move this to the Username type.
To call this TransferBasketAsync in the BasketService, you must construct a Username type. In this example, this is done on a Razor page.
In the above example, the userName comes from the HTTP request, which will be a string. We then construct a Username type passing in that string value. We’re pushing the validation to be at the edge of the application.
Our core application defines the Username type, but its usage/creation is at the edge. Nothing can get into the core application without being in a valid state.
Because of guard clauses, such as null checks, there tend to be tests associated with them. This was the case with this eShopOnWeb sample, where there were tests to confirm those null checks were done. But since we’ve moved to a type that forces it to be valid, these tests still passed because it was throwing. However, these tests are useless now, and we can remove them. We can now remove any tests that were related to doing a null check against the string username.
Instead, we have a few tests to confirm the behavior of our new Username type.
As some others commented on the YouTube video, you might be thinking that I’ve introduced a value object to combat primitive obsession. While true, that’s not the seeing the root cause. The root cause isn’t primitive obsession. The issue is allowing your domain to accept invalid arguments that you need to guard against. This can apply to primitives that are null, as my example illustrates, but it can also be explicit invariants for generic examples, Money, or Date/times with Timezones. However, this can include very domain-specific values.
I’m not suggesting guard clauses are not helpful. They are. Exiting early within a method when preconditions aren’t met simplifies logic. However, littering trivial guard clauses all over your codebase is not helpful. Force the outer edge of your application to construct valid values that are passed into your core domain code.
Developer-level members of my YouTube channel or Patreon get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.