Suppose I have a (pseudo-code) function divideTwenty.
fn integer divideTwenty(int divisor) {
return 20/divisor;
}
This function is simple, but error prone. Specifically, what if divisor
is 0? What's the best way to deal with this? There are countless answers. But for now, well, we could add a guarding clause.
fn integer divideTwenty(int divisor) {
if(divisor == 0) {
return -1;
}
return 20/divisor;
}
This is still not great (can divisor be negative?), but let's just focus on the clause. There is an alternative approach, that accomplishes the same spirit. What if instead of passing int divisor
we passed NonZeroInteger divisor
?
That is,
class NonZeroInteger {
int value;
fn void init(int allegedNonZeroInteger) {
if(allegedNonZeroInteger == 0) { throw InvalidArgumentException("Number is zero"); }
self.value = allegedNonZeroInteger;
}
}
fn integer divideTwenty(NonZeroInteger divisor) {
return 20/divisor.value;
}
My question is very simple. Is there a specific name for this kind of design pattern? That is, where we create classes (or types) explicitly for the purposes of establishing constraints on the data being provided to functions. Is it just illustrative of the general use of typed programming, or OOP, encapsulation, or something else?
Thanks.
This isn't a troubleshooting problem. I'm just asking a question about code design concepts.
You are describing a design-concept that is summarized as "parse, don't validate". It is not exclusive to OOP and it works particularly well on immutable objects.
Your
NonZeroInteger
-class parses the int to be aNonZeroInteger
. Parsing, in this case, means to take an input with few constraints and return an output with the same meaning but more constraints that you can rely on. Parsing might fail if the unstructured input does not fulfill the constraints. This is not what you usually think of when you read something like "Json-parser", but it is consistent with this definition.Operations can now rely on the structure of
NonZeroInteger
. This is good, because the signature ofdivideTwenty(NonZeroInteger)
now describes to the programmer and the compiler/type-checker what kind of inputs it can take, while the olddivideTwenty(int)
method actually only works on a subset of its statically allowed inputs.Other formulations of this design-concept are "make invalid state unrepresentable" and the concept of preferring "complete functions" over "partial functions". You may look up "phantom types", they are a sophisticated implementation of this concept.
You can read an excellent and detailed article about it here, the examples are in haskell but you don't have to be a haskell-programmer to read it: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/