在我的项目中,我有很多非常相似的事件。这是一个简短的示例:
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
}
有一个工厂用于在运行时获取适当的事件。另一个使用部分函数获取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的intelliSense抱怨preValue
和newValue
,但是编译器能够弄清楚并成功构建。
这是一个基本规范,说明如何调用它:
"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)
}
如果建议这种方法的评论者之一想提出对此内容的回答,我将很乐意对此进行投票。
答案 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)
}
}
比这还不能编译。您需要将preValue
和newValue
转换为适当的类型,这也是一个臭代码。
您可以在致电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)