HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Reducing boilerplate when validating parameters and using a tuple for the parameter list of a case class

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
casetheclassreducingvalidatingtupleusingforwhenand

Problem

I had designed a simple case class that looks like this:

case class StreetSecondary(designator: String, value: Option[String])


I needed to add validation (to prevent invalid instances from ever being constructed). So, I then modified the class to look like this:

case class StreetSecondary(designator: String, value: Option[String]) {
  require(
      designator.forall(char => ((char == '#') || char.isLetter))
    , s"designator [$designator] must only contain characters from [#,A-Za-z]"
  )
  require(
      value.isEmpty || (value.get.head != ' ')
    , s"if defined, value [${value.get}] must not start with a space"
  )
}


However, this means that client code using this case class ends up having to deal with any failures as exceptions (bad FP code smell). Additionally, there is no way to know if more than one test failed as the constructor aborts at the first failed condition. It is preferable to return a list of failures as opposed to having to repeatedly hitting the constructor to slowly sift through all of the possible validation exceptions.

So, to produce this list of failed validations, I decided to define an explicit companion object to the case class. This has the unfortunate consequence of blowing away the scala compiler provided implicit companion object (please see this post for more details). So, here is what the implementation now looks like:

```
object StreetSecondary extends ((String, Option[String]) => StreetSecondary) {
override def toString = getClass.getName.split("""\$""").reverse.dropWhile(!_.take(1).head.isLetter).head
def validate(designator: String, value: Option[String]): List[RuntimeException] = {
val exceptions: List[Option[RuntimeException]] =
List(
if (!designator.forall(char => ((char == '#') || char.isLetter)))
Some(new IllegalStateException(s"designator [$designator] must only contain characters from [#,A-Za-z]"))
else None
, if (value.isDefined && (value

Solution

Minor point: designator.forall(char => ((char == '#') || char.isLetter)) would look more normal as a regex.

I don't think you should throw exceptions from the constructor with require. Exceptions should be for exceptional circumstances and a failed validation is not an exceptional circumstance. You can make the case class constructor private and only rely on the contructor methods from the object. You don't want the users to be able to create an unvalidated instance.

I tried to find ways to shorten/generalize your code, but I could not think of anything. Tuples are not monads and there is not much to do with them.

EDIT as reply to comment:

This does run:

case class A private (i: Int)
  object A {
    def create(i: Int) = A(i)
  }
  println(A.create(3))


See also this stackoverflow post.

I guess "advanced" is relative to experience:

val nameRegex = """\A[#a-zA-Z]*\Z""".r
def isValidName(word: String): Boolean = nameRegex.findFirstIn(word).isDefined


I did have to mess around a bit to get it right. Note that it accepts the empty string, as did your original implementation. If you don't want to accept the empty string, just change * to + in the regexp.

Code Snippets

case class A private (i: Int)
  object A {
    def create(i: Int) = A(i)
  }
  println(A.create(3))
val nameRegex = """\A[#a-zA-Z]*\Z""".r
def isValidName(word: String): Boolean = nameRegex.findFirstIn(word).isDefined

Context

StackExchange Code Review Q#60645, answer score: 2

Revisions (0)

No revisions yet.