MORCHARD
MORCHARD

Reputation: 263

Scala Generic Trait Factory

In my project I have many events that are very similar. Here's a shortened example:

object Events {
  final case class UpdatedCount(id: Int, prevValue: Double, newValue: Double) 
      extends PropertyEvent[Double]
  final case class UpdatedName(id: Int, prevValue: String, newValue: String) 
      extends PropertyEvent[String]
}

The trait looks like this:

trait PropertyEvent[A] {
  val id: Int
  val prevValue: A
  val newValue: A
}

There is a factory that is used to get the appropriate event at runtime. This gets called by another generic method that uses partial functions to get the preValue and newValue:

object PropertyEventFactory{
  def getEvent[A, B <: PropertyEvent[A]](id: Int, preValue: A, newValue: A, prop: B): PropertyEvent[A]= prop match{
    case UpdatedCount(_,_,_) => UpdatedCount(id, preValue, newValue)
    case UpdatedName(_,_,_) => UpdatedName(id, preValue, newValue)
  }
}

IntelliJ's intelliSense complains about the preValue and newValue, but the compiler is able to figure it out and build successfully.

Here is a basic spec to show how this might get called:

"Passing UpdatedCount to the factory" should "result in UpdatedCount" in {
    val a = PropertyEventFactory.getEvent(0, 1d,2d, UpdatedCount(0,0,0))
    assert(a.id == 0)
    assert(a.prevValue == 1)
    assert(a.newValue == 2)
}

Is there a way to achieve this by passing UpdatedCount as a type instead of an object? Creating a temporary version of UpdatedCount just to get the actual UpdatedCount Event has code smell to me. I've tried many ways but end up with other issues. Any ideas?

Edit 1: Added the getEvent calling function and some additional supporting code to help demonstrate the pattern of use.

Here is the basic entity that is being updated. Forgive the use of vars in the case class as it makes the examples much simpler.

final case class BoxContent(id: Int, var name: String, var count: Double, var stringProp2: String, var intProp: Int){}

The command used to request an update:

object Commands {
  final case class BoxContentUpdateRequest(requestId: Long, entity: BoxContent, fields: Seq[String])
}

Here is a persistent actor that receives request to update a BoxContent in a Box. The method that calls the factory is in here in the editContentProp function:

class Box extends PersistentActor{

  override def persistenceId: String = "example"

  val contentMap: BoxContentMap = new BoxContentMap()

  val receiveCommand: Receive = {
    case request: BoxContentUpdateRequest =>
      val item = request.entity
      request.fields.foreach{
        case "name" => editContentProp(item.id, item.name, contentMap.getNameProp, contentMap.editNameProp, UpdatedName.apply(0,"",""))
        case "count" => editContentProp(item.id, item.count, contentMap.getCountProp, contentMap.editCountProp, UpdatedCount.apply(0,0,0))
        case "stringProp2" => /*Similar to above*/
        case "intProp" => /*Similar to above*/
        /*Many more similar cases*/
      }
  }

  val receiveRecover: Receive = {case _ => /*reload and persist content info here*/}


  private def editContentProp[A](key: Int, newValue: A, prevGet: Int => A,
                             editFunc: (Int, A) => Unit, propEvent: PropertyEvent[A]) = {
    val prevValue = prevGet(key)
    persist(PropertyEventFactory.getEvent(key, prevValue, newValue, propEvent)) { evt =>
      editFunc(key, newValue)
      context.system.eventStream.publish(evt)
    }
  }
}

Edit2: The suggestion made in the comments to expose a factory method for each event and then pass the factory method seems to be best approach.

Here is the modified Box class:

class Box extends PersistentActor{

  override def persistenceId: String = "example"

  val contentMap: BoxContentMap = new BoxContentMap()

  val receiveCommand: Receive = {
    case request: BoxContentUpdateRequest =>
      val item = request.entity
      request.fields.foreach{
        case "name" => editContentProp(item.id, item.name, contentMap.getNameProp, contentMap.editNameProp, PropertyEventFactory.getNameEvent)
        case "count" => editContentProp(item.id, item.count, contentMap.getCountProp, contentMap.editCountProp, PropertyEventFactory.getCountEvent)
        case "stringProp2" => /*Similar to above*/
        case "intProp" => /*Similar to above*/
        /*Many more similar cases*/
      }
  }

  val receiveRecover: Receive = {case _ => /*reload and persist content info here*/}

  private def editContentProp[A](key: Int, newValue: A, prevGet: Int => A,
                                 editFunc: (Int, A) => Unit, eventFactMethod: (Int, A, A) => PropertyEvent[A]) = {
    val prevValue = prevGet(key)
    persist(eventFactMethod(key, prevValue, newValue)) { evt =>
      editFunc(key, newValue)
      context.system.eventStream.publish(evt)
    }
  }
}

And here is the modified PropertyEventFactory:

object PropertyEventFactory{
  def getCountEvent(id: Int, preValue: Double, newValue: Double): UpdatedCount = UpdatedCount(id, preValue, newValue)
  def getNameEvent(id: Int, preValue: String, newValue: String): UpdatedName = UpdatedName(id, preValue, newValue)
}

If one of the commenters who suggested this approach want to propose an answer with this content I'll be happy to upvote it.

Upvotes: 0

Views: 181

Answers (1)

ygor
ygor

Reputation: 1756

This is my attempt to summarize an answer.

First of all, there is no such thing as a generic factory for your trait. Your trait PropertyEvent only specifies three vals, which every subclass of the trait must fulfill after creation. Every class, which implements the trait, can have very different constructors and/or factories.

So, you really need to "enumerate" those factories manually somewhere. Your first attempt works, but it really suffers from code smell and frankly, I am very surprised, that it even compiles. Scala compiler must somehow be able to narrow down the generic A type to a concrete type, once inside a match/case of a case class.

If you try something like this:

object PropertyEventFactory2 {
  def getEvent[A, B <: PropertyEvent[A]](id: Int, preValue: A, newValue: A, prop: Class[B]): B = prop.getName match {
    case "org.example.UpdatedCount" => UpdatedCount(id, preValue, newValue)
    case "org.example.UpdatedName" => UpdatedName(id, preValue, newValue)
  }
}

Than this does not compile. You'd need to cast preValue and newValue to the appropriate type and this also is a smelly code.

You could create the event before calling editContentProp:

case "name" => {
    val event = UpdatedName(item.id, contentMap.getNameProp(item.id), item.name)
    editContentProp(item.id, item.name, contentMap.getNameProp, contentMap.editNameProp, event)
}

However, all your case branches would repeat the same structure, which is a kind of code duplication. You already recognized it, which is good.

So your best choice really is to pass in a factory for every event. And because all your events are case classes, for every case class you receive a factory method for free, generated by Scala compiler. The factory method resides in the companion object of the case class and it is simply called CaseClass.apply

This leads to the final form of your case branch:

case "name" => editContentProp(item.id, item.name, contentMap.getNameProp, contentMap.editNameProp, UpdatedName.apply)

which is consumed by parameter:

eventFactMethod: (Int, A, A)

Upvotes: 1

Related Questions