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.

3

There are 3 answers

3
julaine On BEST ANSWER

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 a NonZeroInteger. 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 of divideTwenty(NonZeroInteger) now describes to the programmer and the compiler/type-checker what kind of inputs it can take, while the old divideTwenty(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/

1
AJ Snow On

The answer, currently, seems to be Value Objects. Value objects are an object meant to represent an immutable set of data with arbitrary constraints that can (and probably should be) enforced at construction time. You can find a video here that does a solid job of illustrating a motivation for value objects. Additionally, they use the result type to handle validation control, which is another separate problem that my question brought up.

Edit: Bonus, Traits seem to flirt with this concept. I also learned today that after v20 C++ has a new feature called constraints (and concepts) which, for typed languages, gets remarkably close to what I'm envisioning. In this talk in 2022 around 42 minutes, you can see an interesting example of how this might look in practice in the function isFirstElemTheSame.

Credit and thanks to @R.Abbasi for the initial answer that prompted me to investigate value objects more, and Milan Jovanović; the youtuber cited in the hyperlinks. Also Alex Dathskovsky from the conference link.

1
R.Abbasi On

This is called encapsulation. Using OOP to encapsulate the data and behavior (validations and logic) in the objects. It is encouraged to use it whenever it makes sense to encapsulate behavior in objects. By using this approach:

  • You comply with SRP
  • It results in a high-cohesion design
  • You achieve higher reusability

For example, one of the domain-driven design tactical patterns building blocks is Value Object. It is used to encapsulate behavior and reuse it to reduce the complexity of the domain by dividing the responsibilities into multiple coherent classes.