Throwing exceptions at construction time is generally seen as not a very polite thing to do, but I'm not sure how to avoid that when trying to enforce constraints on case class values.
Say for example that I need to represent a range, and that both boundaries must be positive. An immediate implementation would be:
case class Range(from: Int, to: Int)
This, however, does not make sure that both from
and to
are positive, nor that to
is greater than from
.
My first instinct is to implement it as follows:
case class Range(from: Int, to: Int) {
require(from >= 0)
require(to >= 0)
require(to >= from)
}
This, however, makes Range
's constructor unsafe.
Is there a common pattern to keep the ease of use of case classes, enforce value constraints, and avoid throwing exceptions?
This is pretty subjective, but I'll try.
It depends on how your class is constructed. In the case of a simple utility range class used casually in internal code, any programmer using it should probably be aware that feeding a range wrong values might lead to weird results. You can assume that sane uses of the classes really don't encounter this problem, and throwing an exception in these rare cases might not be a bad practice to signal somebody really screwed up. If it is obvious from context what values should be fed to the class or not, I don't think it is particularly in bad taste to throw exceptions if people fail to meet these expectations of sanity.
Compare this to the behaviour of NullPointerException
s or ArithmethicException
s which still happen within Scala sometimes. Sometimes it just isn't reasonable anymore to program defensively when faced with enough 'insanity'.
On the other hand, if your class is filled by values you have less control over, i.e. they are the direct consequence of user input, you have to simply take greater control over your construction. Create a companion object with a function that returns an Option
or an Either
:
object Range {
def createSafe(from: Int, to: Int): Option[Range] = {
if(from >= 0 && to >= 0 && to >= from)
Some(Range(from, to))
else
None
}
}
You can then even reuse the validation logic in the instantiation of your case class by overriding apply
as the other answer suggested.
object Range {
def createSafe ...
def apply(from: Int, to: Int): Range = {
createSafe(from, to).getOrElse(throw new IllegalArgumentException(...))
}
}
You can overload the apply
operator in the companion object to achieve what you'd like to do:
object Range{
def apply(from: Int, to: Int) ={
val _from = Math.max(0, from)
val _to = Math.max(0, to)
if(_from < _to) new Range(_from, _to)
else new Range(_to, _from)
}
}
Do note that using to
as a variable name can lead to some "interesting" results.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With