Scala 通用特征工厂

Scala Generic Trait Factory

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

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 的智能感知抱怨 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 调用函数和一些额外的支持代码以帮助演示使用模式。

这是正在更新的基本实体。请原谅在案例 class 中使用 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])
}

这是一个持久性 actor,它接收更新 Box 中的 BoxContent 的请求。调用工厂的方法在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)
    }
  }
}

编辑2: 评论中提出的为每个事件公开工厂方法然后传递工厂方法的建议似乎是最好的方法。

这里是修改后的Boxclass:

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

如果提出这种方法的评论者之一想针对此内容提出一个答案,我将很乐意对其投赞成票。

这是我尝试总结的一个答案。

首先,没有适合您特征的通用工厂之类的东西。您的特征 PropertyEvent 仅指定了三个 vals,特征的每个子class 必须在 创建后 完成。每个实现特征的 class 都可以有非常不同的构造函数 and/or 工厂。

所以,您确实需要在某个地方手动 "enumerate" 这些工厂。你的第一次尝试成功了,但它确实有代码味道,坦率地说,我很惊讶,它甚至可以编译。一旦进入案例 class.

match/case,Scala 编译器必须能够以某种方式将泛型 A 类型缩小为具体类型

如果您尝试这样的操作:

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 分支都会重复相同的结构,这是一种代码重复。你已经认出来了,很好

所以你最好的选择就是为每个事件传递一个工厂。因为您所有的事件都是 case classes,对于每个 case class,您都会免费收到一个由 Scala 编译器生成的工厂方法。工厂方法驻留在案例 class 的伴生对象中,简称为 CaseClass.apply

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

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

被参数消耗:

eventFactMethod: (Int, A, A)