Abstracting Method Argument Validation
Introduction
I just finished Mark Seemann's book Code That Fits in Your Head and a certain solution I expected never surfaced. The solution is the abstraction of API controller argument validation. Though his examples deal with C# .NET controllers, the idea is more general and applicable in all languages. Abstracting method argument validation is what I'm ultimately interested in for this article. Still, I'll use his examples as concrete starting points.
Improvement Opportunity
Why did I expect this solution to surface? Well, because the below concepts were highly emphasized throughout the book and the solution aligns with all of them:
- Fractal architecture
- Cyclomatic complexity
- Abstraction
Let's dig into each before getting to my proposal.
The first two are likely less known (at least they were to me) where the latter is standard in software.
Fractal Architecture
For fractal architecture, Mark argues that code architecture should be informed by the constraint of human short-term memory (~7 active units of information). I think this is a great idea and heuristic to use. What this means in practice is that reading a method or execution context should contain no more than ~7 states or conditions to reason about. The fractal quality surfaces naturally when you zoom-in (Go to Definition) to a given method, object, or class. After the zoom-in—if the codebase is properly a fractal architecture—you'll again be reasoning about no more than ~7 states or conditions. This repeats. Unlike a true fractal however, it has a natural stopping point.
Cyclomatic Complexity
Cyclomatic complexity is a simple metric (automatable in fact) that can be used to aid fractal architecture decisions. In practice, this metric is calculated as the number of code paths in a given method or execution context. As such, one is the minimum where each conditional expression or statement adds one.
Abstraction
As a reminder, abstraction manifests as a simple view or model (not in the MVC sense) that intentionally hides information. In practice, a well designed API is a prime example. This is the case as it amplifies the essential while eliminating the irrelevant.
With these ideas in mind, here is the example from the book that triggered this article:
public bool WillAccept(DateTime now, IEnumerable existingReservations, Reservation candidate)
{
if (existingReservations is null)
throw new ArumentNullException(nameof(existingReservations));
if (candidate is null)
throw new ArgumentNullException(nameof(candidate));
if (candidate.At < now)
return false;
if (IsOutsideOfOpeningHours(candidate))
return false;
var seating = new Seating(SeatingDuration, candidate);
var relevantReservations = existingReservations.Where(seating.Overlaps);
var availableTables = Allocate(relevantReservations);
return availableTables.Any(t => t.Fits(candidate.Quantity));
}
Do you see the improvement opportunity?
Proposal
If my set up isn't obvious, the argument validation can be abstracted (encapsulated), thus further reducing cyclomatic complexity while better adhering to a fractal architecture.
The exact shape of my proposal below may change in the future, but I think it's a natural and improved step in the right direction. Additionally, since argument validation is so common in applications, I can't help but think a Validator
aggregator pattern emerges.
Here is the above example refactored:
public bool WillAccept(DateTime now, IEnumerable existingReservations, Reservation candidate)
{
var validation = Validator.WillAccept(now, existingReservations, candidate);
if (validation.Failed) return validation.ReturnValue;
var seating = new Seating(SeatingDuration, candidate);
var relevantReservations = existingReservations.Where(seating.Overlaps);
var availableTables = Allocate(relevantReservations);
return availableTables.Any(t => t.Fits(candidate.Quantity));
}
Though the exact implementation of the first two lines could take a few shapes, the heart of the improvement is that the validation itself should be hidden information. What really matters in the context of the method (and virtually all methods) is:
- If the arguments are valid or not
- If invalid, return the correct early exit value
That's it.
I'll be experimenting with this implementation idea in the future.