DEV Community

Nicola Todorov
Nicola Todorov

Posted on • Edited on

Designing with Opaque types

Previously in Designing with types for the working programmer

We have imbued meaning in Email type

record Email(String value) { }
Enter fullscreen mode Exit fullscreen mode

But are all possible String values a valid email address?
Can we enforce validation so that we do not end up with defensive programming?

Sure. Constructor can enforce it.

record Email(String value) {
  Email {
    if(!Pattern.compile( "^(.+)@(\\S+)$").matcher(value).matches()) 
      throw new IllegalArgumentException("Not a valid email");
  }
}
Enter fullscreen mode Exit fullscreen mode

Are we really going to throw exceptions like it is new year?

I summon smart constructor.

class Email {
    private final String email;

    private Email(String email) { this.email = email; }

    static Optional<Email> parse(String email) {
        if (Pattern.compile( "^(.+)@(\\S+)$").matcher(email).matches())
            return Optional.of(new Email(email));

        return Optional.empty();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the constructor is private. The only way to get valid Email is to go through parse - the smart constructor.
As a consequence the instance of Email is like a boarding pass. It is a proof in itself that the email is valid format. Correct by construction.
This is what they call opaque types and they ...

Make illegal states unrepresentable

This phrase was coined by Yaron Minsky and captures a bit more than we are going experience in this article. Nevertheless opaque types are maybe the simplest incarnation of this idea.
Look!

class Password {
  private final String password;
  private Password(String password) { this.password = passowrd; }

  public static Optional<Password> parse(String password) {
    if(password == null || password.length < 6) return Optional.empty();

    return Optional.of(new Password(password));
  }
}
Enter fullscreen mode Exit fullscreen mode

We can now encode/enforce domain rules like

  • Password has to be at least 7 chars long
  • PositiveAmount guarantees non-negative values
  • DiscountPercentage is within predefined range (0, 10]

Just think about it. With smart constructors we can now encode domain events into our types like VerifiedUser - guarantee identity is confirmed.

In each case the type itself is a proof that something was done.

Conclusion

Wrapper type + Smart constructor = Opaque type
Simple as that.

We traded extra lines of code for:

  • correctness by construction - illegal states can not be created
  • business rules encoded in types - guaranteed by the compiler
  • values with meaning - values communicate intent and what has been done already
  • no defensive coding - validation as earliest as possible

If you are hungry for more and do not mind some Haskell, you can go ahead and read Parse, don’t validate.
For other resources you can google around for "Make illegal states unrepresentable" or better yet watch the original Effective ML by Yaron Minsky

Top comments (0)