2
votes

I have the following class where the properties are an Option[T]

class User extends IdBaseEntity[UUID] {
  var id: Option[UUID] = None
  var name: Option[String] = None
  var createdOn: Option[Date] = None
}

In some data access layer I need to assign these properties if they aren't set before the object is persisted to cassandra. Here are a few ways for the createdOn property. Are any of these the best approach or is there something better I should be doing?

Example 1

entity.createdOn = Some(entity.createdOn.map(identity).getOrElse(new Date()))

Example 2

entity.createdOn = entity.createdOn.orElse(Some(new Date()))

Example 3

entity.createdOn = entity.createdOn match {
  case None => Some(new Date())
  case _ => entity.createdOn
}

Example 4

entity.createdOn = entity.createdOn match {
  case None => Some(new Date())
  case Some(x) => Some(x)
}

Example 5

entity.createdOn match {
  case None => entity.createdOn = Some(new Date())
  case _ =>;
}
3
I would probably define a method on the case class that yields an instance with all incomplete fields filled with the specified defaults. (I'd do it using the tactic in (2), as Chris B suggests).Randall Schulz

3 Answers

4
votes

Matching on Option is not really idiomatic (IMHO). I prefer to get orElse or getOrElse. Personally I would go with example 2.

I'm not sure whether this will fit your use case, but it is more idiomatic to make User an immutable case class:

case class User(id: Option[UUID] = None, ...)

and copy it, rather than updating the fields in-place:

val updatedEntity = entity.copy(name = user.name.orElse(Some("Chris")))
1
votes

I'd consider changing your design a bit - for two reasons:

  • It looks like the User class should be read-only once initialized, so something like a case class or val instead of var would capture that requirement:

    case class User( id:UUID, name:String, createdOn:Date );

  • It looks like every User is required to have an id, name, and createdOn property set, so Option[] is not a good way to model that.

I often setup a Builder class along side read-only classes to simplify and decouple the object-construction process from what the object represents - something like this

object User {
    class Builder {
       var id:UUID = UUID.randomUUID()
       def id( v:UUID ):this.type = {id =v; this; }

       var name:String = id.toString
       def name( v:String ):this.type = { name=v; this; }

       var createdOn:Date = new Date()
       def createdOn( v:Date ):this.type = { createdOn = v; this; }

       def build():User = {
         assert( Seq(id,name,createdOn).find( _ == null ).isEmpty, "Must set all props" )
         User( user, name, createdOn )
       }
   }
}

Anyway - that's another way to do things ...

1
votes

Since the scenario is "get a property value and update it if some condition holds", I'd try to encapsulate access to properties. For example:

/**
 * Read+write access to property `P` of object `R`.
 */
case class Accessor[R,P](get: R => P, set: (R, P) => Unit) {
  /** Utility for updating values. */
  def update(record: R, modfn: P => P) =
    set(record, modfn(get(record)));
}

class User {
  var id: Option[Int] = None;
}
object User {
  // For each property of `User` we need to define an accessor,
  // but this is one time job:
  val idA: Accessor[User,Option[Int]] =
      Accessor((u: User) => u.id,
               (u: User, r: Option[Int]) => u.id = r);
}

object Test {
  import User._;

  // We can do a lot of stuff now with accessors, for example set
  // default values for `Option[...]` ones:
  def setDefault[A,B](record: A,
                      accessor: Accessor[A,Option[B]],
                      defPropVal: => B) =
    accessor.update(record, _.orElse(Some(defPropVal)));

  val user = new User();
  // Set user's id, if not defined:
  setDefault(user, idA, 42);
}

So instead of defining a specific method for each property for filling in default values, we define a generic accessor for each property. Then we can use them to implement all the other stuff generically.