SIDENOTE:
Again, while I'd prefer to not do so, I am adding a third answer (as opposed to editing one of the two existing ones above). Per the meta site, they specify a very strong preference to avoid my editing an existing answer if there are any comments already attached to it.
I decided to revisit this case class pattern recently, when I decided to add caching (via ScalaCache). I started from scratch using Scala 2.11.7 and reimplemented the entire pattern hoping to even further reduce boilerplate. I discovered several ways to simplify things. My intention is to eventually turn it into a Scala macro. Here's what the new code looks like:
object StreetSecondary extends ((String, Option[String]) => StreetSecondary) {
type Parameters = (String, Option[String])
val cache =
CacheBuilder
.newBuilder()
.maximumSize(10000L)
.expireAfterAccess(30, TimeUnit.SECONDS)
.build[String, Object]
implicit val scalaCache: ScalaCache = ScalaCache(GuavaCacheWrapper(cache))
def apply(designator: String, value: Option[String] = None): StreetSecondary =
validate(designator, value) match {
case Some(runtimeExceptionsException) =>
throw runtimeExceptionsException
case None =>
create(designator, value)
}
def apply(parameters: Parameters): StreetSecondary =
StreetSecondary(parameters._1, parameters._2)
def tryValidatedFactory(designator: String, value: Option[String] = None): Try[() => StreetSecondary] =
validate(designator, value) match {
case Some(runtimeExceptionsException) =>
Failure(runtimeExceptionsException)
case None =>
Success(
new (() => StreetSecondary) {
def apply(): StreetSecondary =
create(designator, value)
}
)
}
def tryValidatedFactory(parameters: Parameters): Try[() => StreetSecondary] =
tryValidatedFactory(parameters._1, parameters._2)
def validate(designator: String, value: Option[String] = None): Option[RuntimeExceptionsException] =
RuntimeExceptionsException(clientProvidedValidateImpl(designator, value))
def validate(parameters: Parameters): Option[RuntimeExceptionsException] =
validate(parameters._1, parameters._2)
def clientProvidedValidateImpl(designator: String, value: Option[String]): List[RuntimeException] = {
def validateInRange(name: String, upperBound: Double, value: Double): Option[IllegalArgumentException] =
if (!(-upperBound <= value))
Some(new IllegalArgumentException(s"$name [$value] must be greater than or equal to ${-upperBound}"))
else
if (!(value < upperBound))
Some(new IllegalArgumentException(s"$name [$value] must be less than $upperBound"))
else
None
List(
if (!designator.forall(char => ((char == '#') || char.isLetter)))
Some(new IllegalStateException(s"designator [$designator] must only contain characters from [#,A-Za-z]"))
else
None
, value.flatMap(
valueGet =>
if (valueGet.head == ' ')
Some(new IllegalStateException(s"if defined, value [$valueGet] must not start with a space"))
else
None
)
).flatten
}
private def create(designator: String, value: Option[String]): StreetSecondary = memoize {
new StreetSecondary(designator, value) {
private def readResolve(): Object =
StreetSecondary(designator, value)
def copy(designator: String = designator, value: Option[String] = value): StreetSecondary =
StreetSecondary(designator, value)
val tuple: StreetSecondary.Parameters =
(designator, value)
}
}
}
abstract case class StreetSecondary private[StreetSecondary] (designator: String, value: Option[String]) { //example J
def copy(designator: String = designator, value: Option[String] = value): StreetSecondary
def tuple: StreetSecondary.Parameters
}
Here are the test cases (intended for use with Scala WorkSheet; just copy and paste):
val tryDesignator1 = Try(StreetSecondary("Upper"))
val tryDesignator2 = Try(StreetSecondary("Upper", None))
val tryBoth1 = Try(StreetSecondary("Upper", Some("331")))
val tryInvalidDesignator = Try(StreetSecondary("Upper!", None))
val tryInvalidBoth1 = Try(StreetSecondary("Upper!", Some("331")))
val tryInvalidBoth2 = Try(StreetSecondary("Upper", Some(" 331")))
val tryInvalidBoth3 = Try(StreetSecondary("Upper!", Some(" 331")))
val tryBoth2 = Try(StreetSecondary("Ste", Some("100")))
//
//Note: these won't compile as the constructor is private - desired effect
//val tryNewDesignator = Try(new StreetSecondary("Upper", None) { /* implementation intentionally left blank */})
//val tryNewBoth = Try(new StreetSecondary("Upper", Some("331")) { /* implementation intentionally left blank */})
//val tryNewInvalidDesignator = Try(new StreetSecondary("Upper!", None) { /* implementation intentionally left blank */})
//val tryNewInvalidBoth1 = Try(new StreetSecondary("Upper!", Some("331")) { /* implementation intentionally left blank */})
//val tryNewInvalidBoth2 = Try(new StreetSecondary("Upper!", Some(" 331")) { /* implementation intentionally left blank */})
//val tryNewInvalidBOth3 = Try(new StreetSecondary("Upper", Some(" 331")) { /* implementation intentionally left blank */})
//
val tryCopyDesignator = Try(tryBoth2.get.copy(designator = "Upper"))
val tryCopyValue = Try(tryBoth2.get.copy(value = Some("331")))
val tryCopyBoth1 = Try(tryBoth2.get.copy(designator = "Upper", value = None))
val tryCopyBoth2 = Try(tryBoth2.get.copy(designator = "Upper", value = Some("331")))
val tryCopyInvalidDesignator = Try(tryBoth2.get.copy(designator = "Upper!", value = None))
val tryCopyInvalidBoth1 = Try(tryBoth2.get.copy(designator = "Upper!", value = Some("331")))
val tryCopyInvalidBoth2 = Try(tryBoth2.get.copy(designator = "Upper", value = Some(" 331")))
val tryCopyInvalidBoth3 = Try(tryBoth2.get.copy(designator = "Upper!", value = Some(" 331")))
//
val validateDesignator1 = StreetSecondary.validate("Upper")
val validateDesignator2 = StreetSecondary.validate("Upper", None)
val validateBoth1 = StreetSecondary.validate("Upper", Some("331"))
val validateInvalidDesignator = StreetSecondary.validate("Upper!", None)
val validateInvalidBoth1 = StreetSecondary.validate("Upper!", Some("331"))
val validateInvalidBoth2 = StreetSecondary.validate("Upper", Some(" 331"))
val validateInvalidBoth3 = StreetSecondary.validate("Upper!", Some(" 331"))
val validateBoth2 = StreetSecondary.validate("Ste", Some("100"))
//
val tryValidatedFactoryDesignator1 = StreetSecondary.tryValidatedFactory("Upper").map(_.apply())
val tryValidatedFactoryDesignator2 = StreetSecondary.tryValidatedFactory("Upper", None).map(_.apply())
val tryValidatedFactoryBoth1 = StreetSecondary.tryValidatedFactory("Upper", Some("331")).map(_.apply())
val tryValidatedFactoryInvalidDesignator = StreetSecondary.tryValidatedFactory("Upper!", None).map(_.apply())
val tryValidatedFactoryInvalidBoth1 = StreetSecondary.tryValidatedFactory("Upper!", Some("331")).map(_.apply())
val tryValidatedFactoryInvalidBoth2 = StreetSecondary.tryValidatedFactory("Upper", Some(" 331")).map(_.apply())
val tryValidatedFactoryInvalidBoth3 = StreetSecondary.tryValidatedFactory("Upper!", Some(" 331")).map(_.apply())
val tryValidatedFactoryBoth2 = StreetSecondary.tryValidatedFactory("Ste", Some("100")).map(_.apply())
//
val tryTupledDesignator2 = Try(StreetSecondary.tupled(("Upper", None)))
val tryTupledBoth1 = Try(StreetSecondary.tupled(("Upper", Some("331"))))
val tryTupledInvalidDesignator = Try(StreetSecondary.tupled(("Upper!", None)))
val tryTupledInvalidBoth1 = Try(StreetSecondary.tupled(("Upper!", Some("331"))))
val tryTupledInvalidBoth2 = Try(StreetSecondary.tupled(("Upper", Some(" 331"))))
val tryTupledInvalidBoth3 = Try(StreetSecondary.tupled(("Upper!", Some(" 331"))))
val tryTupledBoth2 = Try(StreetSecondary.tupled(("Ste", Some("100"))))
//
val tryApplyTupleDesignator2 = Try(StreetSecondary(("Upper", None)))
val tryApplyTupleBoth1 = Try(StreetSecondary(("Upper", Some("331"))))
val tryApplyTupleInvalidDesignator = Try(StreetSecondary(("Upper!", None)))
val tryApplyTupleInvalidBoth1 = Try(StreetSecondary(("Upper!", Some("331"))))
val tryApplyTupleInvalidBoth2 = Try(StreetSecondary(("Upper", Some(" 331"))))
val tryApplyTupleInvalidBoth3 = Try(StreetSecondary(("Upper!", Some(" 331"))))
val tryApplyTupleBoth2 = Try(StreetSecondary(("Ste", Some("100"))))
//
val tryValidatedFactoryTupleDesignator2 = StreetSecondary.tryValidatedFactory(("Upper", None)).map(_.apply())
val tryValidatedFactoryTupleBoth1 = StreetSecondary.tryValidatedFactory(("Upper", Some("331"))).map(_.apply())
val tryValidatedFactoryTupleInvalidDesignator = StreetSecondary.tryValidatedFactory(("Upper!", None)).map(_.apply())
val tryValidatedFactoryTupleInvalidBoth1 = StreetSecondary.tryValidatedFactory(("Upper!", Some("331"))).map(_.apply())
val tryValidatedFactoryTupleInvalidBoth2 = StreetSecondary.tryValidatedFactory(("Upper", Some(" 331"))).map(_.apply())
val tryValidatedFactoryTupleInvalidBoth3 = StreetSecondary.tryValidatedFactory(("Upper!", Some(" 331"))).map(_.apply())
val tryValidatedFactoryTupleBoth2 = StreetSecondary.tryValidatedFactory(("Ste", Some("100"))).map(_.apply())
//
val validateTupleDesignator2 = StreetSecondary.validate(("Upper", None))
val validateTupleBoth1 = StreetSecondary.validate(("Upper", Some("331")))
val validateTupleInvalidDesignator = StreetSecondary.validate(("Upper!", None))
val validateTupleInvalidBoth1 = StreetSecondary.validate(("Upper!", Some("331")))
val validateTupleInvalidBoth2 = StreetSecondary.validate(("Upper", Some(" 331")))
val validateTupleInvalidBoth3 = StreetSecondary.validate(("Upper!", Some(" 331")))
val validateTupleBoth2 = StreetSecondary.validate(("Ste", Some("100")))
To examine the contents of the cache after running the test cases, use this:
val cacheContents = StreetSecondary.cache.asMap
I reached this solution via a sequence of very carefully designed changes. I started with the simplest case class:
case class StreetSecondary (designator: String, value: Option[String]) //example A
And then I kept refactoring and enhancing the case class and its companion object until I got to the end solution (presented above, which is example J). I captured each progression in the evolution and turned it into an article (in a Google Drive document).
Here's the complete source code for the Eclipse ScalaIDE WorkSheet:
package org.public_domain
import scala.util.{Failure, Success, Try}
import java.util.concurrent._
import com.google.common.cache.CacheBuilder
import scalacache._
import scalacache.guava.{GuavaCache => GuavaCacheWrapper}
import scalacache.memoization._
object WorkSheetStreetSecondaryJ {
println("required by ScalaIDE or a silent/unexplained execution failure occurs")
object StreetSecondary extends ((String, Option[String]) => StreetSecondary) {
type Parameters = (String, Option[String])
val cache =
CacheBuilder
.newBuilder()
.maximumSize(10000L)
.expireAfterAccess(30, TimeUnit.SECONDS)
.build[String, Object]
implicit val scalaCache: ScalaCache = ScalaCache(GuavaCacheWrapper(cache))
def apply(designator: String, value: Option[String] = None): StreetSecondary =
validate(designator, value) match {
case Some(runtimeExceptionsException) =>
throw runtimeExceptionsException
case None =>
create(designator, value)
}
def apply(parameters: Parameters): StreetSecondary =
StreetSecondary(parameters._1, parameters._2)
def tryValidatedFactory(designator: String, value: Option[String] = None): Try[() => StreetSecondary] =
validate(designator, value) match {
case Some(runtimeExceptionsException) =>
Failure(runtimeExceptionsException)
case None =>
Success(
new (() => StreetSecondary) {
def apply(): StreetSecondary =
create(designator, value)
}
)
}
def tryValidatedFactory(parameters: Parameters): Try[() => StreetSecondary] =
tryValidatedFactory(parameters._1, parameters._2)
def validate(designator: String, value: Option[String] = None): Option[RuntimeExceptionsException] =
RuntimeExceptionsException(clientProvidedValidateImpl(designator, value))
def validate(parameters: Parameters): Option[RuntimeExceptionsException] =
validate(parameters._1, parameters._2)
def clientProvidedValidateImpl(designator: String, value: Option[String]): List[RuntimeException] = {
def validateInRange(name: String, upperBound: Double, value: Double): Option[IllegalArgumentException] =
if (!(-upperBound <= value))
Some(new IllegalArgumentException(s"$name [$value] must be greater than or equal to ${-upperBound}"))
else
if (!(value < upperBound))
Some(new IllegalArgumentException(s"$name [$value] must be less than $upperBound"))
else
None
List(
if (!designator.forall(char => ((char == '#') || char.isLetter)))
Some(new IllegalStateException(s"designator [$designator] must only contain characters from [#,A-Za-z]"))
else
None
, value.flatMap(
valueGet =>
if (valueGet.head == ' ')
Some(new IllegalStateException(s"if defined, value [$valueGet] must not start with a space"))
else
None
)
).flatten
}
private def create(designator: String, value: Option[String]): StreetSecondary = memoize {
new StreetSecondary(designator, value) {
private def readResolve(): Object =
StreetSecondary(designator, value)
def copy(designator: String = designator, value: Option[String] = value): StreetSecondary =
StreetSecondary(designator, value)
val tuple: StreetSecondary.Parameters =
(designator, value)
}
}
}
abstract case class StreetSecondary private[StreetSecondary] (designator: String, value: Option[String]) { //example J
def copy(designator: String = designator, value: Option[String] = value): StreetSecondary
def tuple: StreetSecondary.Parameters
}
//
val tryDesignator1 = Try(StreetSecondary("Upper"))
val tryDesignator2 = Try(StreetSecondary("Upper", None))
val tryBoth1 = Try(StreetSecondary("Upper", Some("331")))
val tryInvalidDesignator = Try(StreetSecondary("Upper!", None))
val tryInvalidBoth1 = Try(StreetSecondary("Upper!", Some("331")))
val tryInvalidBoth2 = Try(StreetSecondary("Upper", Some(" 331")))
val tryInvalidBoth3 = Try(StreetSecondary("Upper!", Some(" 331")))
val tryBoth2 = Try(StreetSecondary("Ste", Some("100")))
//
//Note: these won't compile as the constructor is private - desired effect
//val tryNewDesignator = Try(new StreetSecondary("Upper", None) { /* implementation intentionally left blank */})
//val tryNewBoth = Try(new StreetSecondary("Upper", Some("331")) { /* implementation intentionally left blank */})
//val tryNewInvalidDesignator = Try(new StreetSecondary("Upper!", None) { /* implementation intentionally left blank */})
//val tryNewInvalidBoth1 = Try(new StreetSecondary("Upper!", Some("331")) { /* implementation intentionally left blank */})
//val tryNewInvalidBoth2 = Try(new StreetSecondary("Upper!", Some(" 331")) { /* implementation intentionally left blank */})
//val tryNewInvalidBOth3 = Try(new StreetSecondary("Upper", Some(" 331")) { /* implementation intentionally left blank */})
//
val tryCopyDesignator = Try(tryBoth2.get.copy(designator = "Upper"))
val tryCopyValue = Try(tryBoth2.get.copy(value = Some("331")))
val tryCopyBoth1 = Try(tryBoth2.get.copy(designator = "Upper", value = None))
val tryCopyBoth2 = Try(tryBoth2.get.copy(designator = "Upper", value = Some("331")))
val tryCopyInvalidDesignator = Try(tryBoth2.get.copy(designator = "Upper!", value = None))
val tryCopyInvalidBoth1 = Try(tryBoth2.get.copy(designator = "Upper!", value = Some("331")))
val tryCopyInvalidBoth2 = Try(tryBoth2.get.copy(designator = "Upper", value = Some(" 331")))
val tryCopyInvalidBoth3 = Try(tryBoth2.get.copy(designator = "Upper!", value = Some(" 331")))
//
val validateDesignator1 = StreetSecondary.validate("Upper")
val validateDesignator2 = StreetSecondary.validate("Upper", None)
val validateBoth1 = StreetSecondary.validate("Upper", Some("331"))
val validateInvalidDesignator = StreetSecondary.validate("Upper!", None)
val validateInvalidBoth1 = StreetSecondary.validate("Upper!", Some("331"))
val validateInvalidBoth2 = StreetSecondary.validate("Upper", Some(" 331"))
val validateInvalidBoth3 = StreetSecondary.validate("Upper!", Some(" 331"))
val validateBoth2 = StreetSecondary.validate("Ste", Some("100"))
//
val tryValidatedFactoryDesignator1 = StreetSecondary.tryValidatedFactory("Upper").map(_.apply())
val tryValidatedFactoryDesignator2 = StreetSecondary.tryValidatedFactory("Upper", None).map(_.apply())
val tryValidatedFactoryBoth1 = StreetSecondary.tryValidatedFactory("Upper", Some("331")).map(_.apply())
val tryValidatedFactoryInvalidDesignator = StreetSecondary.tryValidatedFactory("Upper!", None).map(_.apply())
val tryValidatedFactoryInvalidBoth1 = StreetSecondary.tryValidatedFactory("Upper!", Some("331")).map(_.apply())
val tryValidatedFactoryInvalidBoth2 = StreetSecondary.tryValidatedFactory("Upper", Some(" 331")).map(_.apply())
val tryValidatedFactoryInvalidBoth3 = StreetSecondary.tryValidatedFactory("Upper!", Some(" 331")).map(_.apply())
val tryValidatedFactoryBoth2 = StreetSecondary.tryValidatedFactory("Ste", Some("100")).map(_.apply())
//
val tryTupledDesignator2 = Try(StreetSecondary.tupled(("Upper", None)))
val tryTupledBoth1 = Try(StreetSecondary.tupled(("Upper", Some("331"))))
val tryTupledInvalidDesignator = Try(StreetSecondary.tupled(("Upper!", None)))
val tryTupledInvalidBoth1 = Try(StreetSecondary.tupled(("Upper!", Some("331"))))
val tryTupledInvalidBoth2 = Try(StreetSecondary.tupled(("Upper", Some(" 331"))))
val tryTupledInvalidBoth3 = Try(StreetSecondary.tupled(("Upper!", Some(" 331"))))
val tryTupledBoth2 = Try(StreetSecondary.tupled(("Ste", Some("100"))))
//
val tryApplyTupleDesignator2 = Try(StreetSecondary(("Upper", None)))
val tryApplyTupleBoth1 = Try(StreetSecondary(("Upper", Some("331"))))
val tryApplyTupleInvalidDesignator = Try(StreetSecondary(("Upper!", None)))
val tryApplyTupleInvalidBoth1 = Try(StreetSecondary(("Upper!", Some("331"))))
val tryApplyTupleInvalidBoth2 = Try(StreetSecondary(("Upper", Some(" 331"))))
val tryApplyTupleInvalidBoth3 = Try(StreetSecondary(("Upper!", Some(" 331"))))
val tryApplyTupleBoth2 = Try(StreetSecondary(("Ste", Some("100"))))
//
val tryValidatedFactoryTupleDesignator2 = StreetSecondary.tryValidatedFactory(("Upper", None)).map(_.apply())
val tryValidatedFactoryTupleBoth1 = StreetSecondary.tryValidatedFactory(("Upper", Some("331"))).map(_.apply())
val tryValidatedFactoryTupleInvalidDesignator = StreetSecondary.tryValidatedFactory(("Upper!", None)).map(_.apply())
val tryValidatedFactoryTupleInvalidBoth1 = StreetSecondary.tryValidatedFactory(("Upper!", Some("331"))).map(_.apply())
val tryValidatedFactoryTupleInvalidBoth2 = StreetSecondary.tryValidatedFactory(("Upper", Some(" 331"))).map(_.apply())
val tryValidatedFactoryTupleInvalidBoth3 = StreetSecondary.tryValidatedFactory(("Upper!", Some(" 331"))).map(_.apply())
val tryValidatedFactoryTupleBoth2 = StreetSecondary.tryValidatedFactory(("Ste", Some("100"))).map(_.apply())
//
val validateTupleDesignator2 = StreetSecondary.validate(("Upper", None))
val validateTupleBoth1 = StreetSecondary.validate(("Upper", Some("331")))
val validateTupleInvalidDesignator = StreetSecondary.validate(("Upper!", None))
val validateTupleInvalidBoth1 = StreetSecondary.validate(("Upper!", Some("331")))
val validateTupleInvalidBoth2 = StreetSecondary.validate(("Upper", Some(" 331")))
val validateTupleInvalidBoth3 = StreetSecondary.validate(("Upper!", Some(" 331")))
val validateTupleBoth2 = StreetSecondary.validate(("Ste", Some("100")))
//
val cacheContents = StreetSecondary.cache.asMap
}