scalacoffee

Strongly-typed Java disaster prevention in Scala

For today’s entry on the weblog we take a short excursion down null- and validation-checking certain-disaster lane and emerge out the other side victorious.

I was writing some Scala code yesterday. Unfortunately it interfaces with Java code (ew! yeck! boo! hiss!) so almost immediately I became mired in null-checking everything constantly. I also had to check for valid values since some of my particular variables didn’t make sense unless they were in a specific range.

Since I shouldn’t show you that code, let’s just pretend like this is the code:

object InputValidator {

  def validateInput(customerComments: String, customerEmail: String, starRating: Int) = {
    require(customerComments != null && customerComments.length > 0 && customerComments.length < 1000)
    require(customerEmail != null && customerEmail.length > 0 && isValidEmail(customerEmail))
    require(starRating != null && starRating >= 1 && starRating <= 5)

    //add'l code elided
  }
  
  def processCustomerComments(customerComments: String) = {
    require(customerComments != null && customerComments.length > 0 && customerComments.length < 1000)
    //add'l code elided for example
  }
  
  def sendThankYouEmail(customerEmail: String) = {
    require(customerEmail != null && customerEmail.length > 0 && isValidEmail(customerEmail))
    //add'l code elided for example
  }
  
  // so on and so forth ... ad infinitum
  
}

There were many more methods than these. They were called from various places in the client/calling code. And this was simplifying and cleaning up the calling code quite a bit. But, since all the values were bare types like String and int, I was putting these require statements at the top of every method to make sure no accidental bullshit like a NullPointerException would happen.

Since that choice made my Scala code obviously suck hard, I next tried making all method parameters of type Option, but I was bothered by the conviction that I should not semantically convey to my clients that any of these were optional when all fields were required. It also didn’t solve the problem of the further valid length and value checking that needed to happen. Another approach I briefly considered was the PartialFunction one.

Unfortunately, neither solution solved another issue, which is that the code still would not be DRY. Any change in the accepted-valid input, like changing star rating to accept the values 1 to 10, would create a situation where I had to go back and change validation logic everywhere it appeared.

Then I had a new idea: tiny types!

Tiny types to the rescue!

So here’s the story of how I solved this problem.

First, I started with a little refactor. I added some classes that could only be created with valid input. For the sake of example, I am only going to show how I handled the star rating:


  case class StarRating(rating: Int) = {
    require(rating != null && rating >= 1 && rating <= 5)

    val value = rating
  }

Doing this means I accepted the overhead of calling ".value" when I was actually doing anything meaningful with the values:


if(starRating.value == 5) {
  sendPleaseTweetEmail(customerEmail)
}

Being forced to call ".value" seems minor considering the amount of validation code it cleared out.

With classes like this available, I updated my method signatures to force calling code to create an instance of StarRating (or EmailAddress, or CustomerComments, or whatever...) instead of just passing an int across. The method signatures turned into:

object InputValidator {

  def validateInput(customerComments: Comments, customerEmail: EmailAddress, starRating: StarRating)
  
  def processCustomerComments(customerComments: Comments)
  
  def sendThankYouEmail(customerEmail: EmailAddress)

  // so on and so forth ...
  
}

This put validity checking of the different values into one and only one place, and made it the responsibility of the caller! Instead of every method having to repeatedly check for validity, we know the values are valid already, when we receive them, because they are strongly-typed.

Instead of every method having to repeatedly check for validity, we know the values are valid because they are strongly-typed.

And, furthermore, I never have to worry that I or someone else maintaining or adding to this code later will accidentally forget to check for something. NullPointerExceptions will never be seen in this code, and no funky behavior (like a customer comment greater than the database-alotted 1000 character length) will ever occur!

And thus I saved my Scala code from certain disaster . . . with strong types.

One thought on “Strongly-typed Java disaster prevention in Scala

Leave a Reply

Your email address will not be published. Required fields are marked *