【问题标题】:Scala Generic Trait FactoryScala 通用特征工厂
【发布时间】:2019-04-19 00:32:29
【问题描述】:

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

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调用函数和一些额外的支持代码来帮助演示使用模式。

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

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

这是一个持久性参与者,它接收更新 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: cmets 中提出的为每个事件公开工厂方法然后传递工厂方法的建议似乎是最好的方法。

这里是修改后的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)
}

如果建议此方法的评论者之一想针对此内容提出答案,我将很乐意支持它。

【问题讨论】:

  • 如果你在调用getEvent的时候就知道了事件类型,那为什么不给每个事件一个单独的工厂方法,只调用合适的方法呢?
  • getEvent 被另一个通用方法调用,所以我不知道事件类型。它有 2 个函数参数(用于获取 preValuenewValue 值)和一个 PropertyEvent[A] 参数,然后它会在发出请求时传递给 getEvent
  • 我认为我们需要查看调用函数,因为这可能是问题所在。但一般来说,您不能使用类型参数来决定如何处理数据,您需要一个值。
  • 为什么你的例子编译,让我感到困惑。谁能解释一下?
  • 回复第一条评论:def getEvent[A, B &lt;: PropertyEvent[A]](id: Int, preValue: A, newValue: A, fact: (Int, A, A) =&gt; B): PropertyEvent[A] = fact(id, preValue, newValue),然后使用PropertyEventFactory3.getEvent(0, 1d, 2d, UpdatedCount.apply)调用它

标签: scala generics traits


【解决方案1】:

这是我试图总结的答案。

首先,对于你的 trait,没有通用工厂这样的东西。你的 trait PropertyEvent 只指定了三个 vals,每个 trait 的子类都必须在创建后完成。每个实现 trait 的类都可以有非常不同的构造函数和/或工厂。

因此,您确实需要在某处手动“枚举”这些工厂。您的第一次尝试有效,但它确实存在代码异味,坦率地说,我很惊讶,它甚至可以编译。 Scala 编译器必须能够以某种方式将泛型 A 类型缩小为具体类型,一旦在案例类的 match/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 编译器生成的工厂方法。工厂方法驻留在案例类的伴生对象中,简称为CaseClass.apply

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

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

被参数消耗:

eventFactMethod: (Int, A, A)

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2011-03-23
  • 1970-01-01
  • 1970-01-01
  • 2017-10-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-08-20
相关资源
最近更新 更多