It's a case class, so why aren't you doing this with pattern matching?
def validateConfig(config: Config): Try[Config] = config match {
case Config(None, _, _, _) => Failure(new IllegalArgumentException("Email Address")
case Config(_, None, _, _) => Failure(new IllegalArgumentException("First Name")
case Config(_, _, None, _) => Failure(new IllegalArgumentException("Last Name")
case Config(_, _, _, None) => Failure(new IllegalArgumentException("Password")
case _ => Success(config)
}
In your simple example, my priority would be to forget monads and chaining, just get rid of that nasty if...else
smell.
However, while a case class works perfectly well for a short list, for a large number of configuration options, this becomes tedious and the risk of error increases. In this case, I would consider something like this:
- Add a method that returns a key->value map of the configuration options, using the option names as the keys.
- Have the Validate method check if any value in the map is
None
- If no such value, return success.
- If at least one value matches, return that value name with the error.
So assuming that somewhere is defined
type OptionMap = scala.collection.immutable.Map[String, Option[Any]]
and the Config
class has a method like this:
def optionMap: OptionMap = ...
then I would write Config.validate
like this:
def validate: Either[List[String], OptionMap] = {
val badOptions = optionMap collect { case (s, None) => s }
if (badOptions.size > 0)
Left(badOptions)
else
Right(optionMap)
}
So now Config.validate
returns either a Left
containing the name of all the bad options or a Right
containing the full map of options and their values. Frankly, it probably doesn't matter what you put in the Right
.
Now, anything that wants to validate a Config
just calls Config.validate
and examines the result. If it's a Left
, it can throw an IllegalArgumentException
containing one or more of the names of bad options. If it's a Right
, it can do whatever it wanted to do, knowing the Config
is valid.
So we could rewrite your validateConfig
function as
def validateConfig(config: Config): Try[Config] = config.validate match {
case Left(l) => Failure(new IllegalArgumentException(l.toString))
case _ => Success(config)
}
Can you see how much more functional and OO this is getting?
- No imperative chain of
if...else
- The
Config
object validates itself
- The consequences of a
Config
object being invalid are left to the larger program.
I think a real example would be more complex yet, though. You are validating options by saying "Does it contain Option[String]
or None
?" but not checking the validity of the string itself. Really, I think your Config
class should contain a map of options where the name maps to the value and to an anonymous function that validates the string. I could describe how to extend the above logic to work with that model, but I think I'll leave that as an exercise for you. I will give you a hint: you might want to return not just the list of failed options, but also the reason for failure in each case.
Oh, by the way... I hope none of the above implies that I think you should actually store the options and their values as an optionMap
inside the object. I think it's useful to be able to retrieve them like that, but I wouldn't ever encourage such exposure of the actual internal representation ;)