Scala通用特质工厂

时间:2018-11-15 20:23:08

标签: scala generics traits

在我的项目中,我有很多非常相似的事件。这是一个简短的示例:

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]
}

特征看起来像这样:

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

有一个工厂用于在运行时获取适当的事件。另一个使用部分函数获取preValuenewValue的泛型方法调用了此方法:

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的intelliSense抱怨preValuenewValue,但是编译器能够弄清楚并成功构建。

这是一个基本规范,说明如何调用它:

"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)
}

是否有一种方法可以通过将UpdatedCount作为类型而不是对象来传递?创建UpdatedCount的临时版本只是为了获得实际的UpdatedCount事件对我来说有代码味。我尝试了很多方法,但最终遇到其他问题。有什么想法吗?

修改1: 添加了getEvent调用函数和一些其他支持代码,以帮助演示使用模式。

这是正在更新的基本实体。原谅在case类中使用vars,因为它使示例更加简单。

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

用于请求更新的命令:

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

这是一个持久性参与者,它接收更新BoxContent中的Box的请求。调用工厂的方法位于editContentProp函数中:

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: 注释中提出的建议是,为每个事件公开一个工厂方法,然后通过工厂方法似乎是最好的方法。

这是经过修改的Box类:

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)
    }
  }
}

这是经过修改的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)
}

如果建议这种方法的评论者之一想提出对此内容的回答,我将很乐意对此进行投票。

1 个答案:

答案 0 :(得分:1)

这是我总结答案的尝试。

首先,没有像您的特质那样的通用工厂。您的特征PropertyEvent仅指定三个vals,特征的每个子类在创建后必须满足 的要求。每个实现特征的类都可以具有非常不同的构造函数和/或工厂。

因此,您确实需要在某个地方手动“枚举”这些工厂。您的第一次尝试是可行的,但是它确实会遭受代码气味的困扰,坦率地说,它甚至可以编译,这让我感到非常惊讶。一旦进入case类的A / match内部,Scala编译器必须能够以某种方式将通用case类型缩小为具体类型。

如果您尝试这样的操作:

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)
  }
}

比这还不能编译。您需要将preValuenewValue转换为适当的类型,这也是一个臭代码。

您可以在致电editContentProp之前创建活动:

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

但是,您所有的case分支都将重复相同的结构,这是一种代码重复。您已经认识到了,很好。

因此,最好的选择实际上是在每次活动时都经过工厂检验。并且由于所有事件都是案例类,因此对于每个案例类,您都会免费获得由Scala编译器生成的工厂方法。 factory方法驻留在case类的伴随对象中,简称为CaseClass.apply

这将导致您的case分支的最终形式:

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

由参数消耗:

eventFactMethod: (Int, A, A)