[英]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
: 这由另一个使用部分函数获取preValue
和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. IntelliJ的intelliSense抱怨preValue
和newValue
,但是编译器能够找出并成功构建。
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. 您需要将preValue
和newValue
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.