简体   繁体   English

Scala通用特质工厂

[英]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 : 这由另一个使用部分函数获取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's intelliSense complains about the preValue and newValue , but the compiler is able to figure it out and build successfully. IntelliJ的intelliSense抱怨preValuenewValue ,但是编译器能够找出并成功构建。

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? 有没有一种方法可以通过将UpdatedCount作为类型而不是对象来传递? Creating a temporary version of UpdatedCount just to get the actual UpdatedCount Event has code smell to me. 创建一个临时版本的UpdatedCount只是为了获得实际的UpdatedCount事件对我来说有代码味。 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. 编辑1:添加了getEvent调用函数和一些其他支持代码,以帮助演示使用模式。

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. 请原谅在case类中使用vars,因为它使示例更加简单。

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 . 这是一个持久性BoxContent ,它接收更新Box BoxContent请求。 The method that calls the factory is in here in the editContentProp function: 调用工厂的方法位于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: 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. Edit2:注释中建议的建议是为每个事件公开一个工厂方法,然后传递该工厂方法似乎是最好的方法。

Here is the modified Box class: 这是修改后的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)
    }
  }
}

And here is the modified PropertyEventFactory : 这是修改后的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. 如果建议该方法的评论者之一想提出有关此内容的答案,我将很乐意对此进行投票。

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. 您的特征PropertyEvent仅指定三个vals ,特征创建必须满足该特征的每个子类。 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. 一旦在case类的match / case ,Scala编译器必须以某种方式能够将泛型A类型缩小为具体类型。

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. 您需要将preValuenewValue preValue转换为适当的类型,这也是一个preValue代码。

You could create the event before calling editContentProp : 您可以在调用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. 但是,您所有的case分支都将重复相同的结构,这是一种代码重复。 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. 并且由于所有事件都是案例类,因此对于每个案例类,您都会免费获得由Scala编译器生成的工厂方法。 The factory method resides in the companion object of the case class and it is simply called CaseClass.apply factory方法驻留在case类的伴随对象中,简称为CaseClass.apply

This leads to the final form of your case branch: 这导致您的case分支的最终形式:

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

which is consumed by parameter: 由参数消耗:

eventFactMethod: (Int, A, A)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM