Scala 阿克卡;使用EventSourcedBehavior传递太多参数
我们正在构建一个基于Akka类型的事件源系统。我们很快就会陷入这样一种情况:我们的状态需要许多参数作为其参数传递 《样式指南》中有一个使用封闭类的解决方案 但是,这是针对“简单”行为的,而不是针对EventSourcedBehaviers。这种使用产生行为的封闭类的技术不适用于事件源行为,因为事件源行为由一个命令处理程序和一个事件处理程序组成 在我们的例子中,我们有一个Scala 阿克卡;使用EventSourcedBehavior传递太多参数,scala,akka,event-sourcing,Scala,Akka,Event Sourcing,我们正在构建一个基于Akka类型的事件源系统。我们很快就会陷入这样一种情况:我们的状态需要许多参数作为其参数传递 《样式指南》中有一个使用封闭类的解决方案 但是,这是针对“简单”行为的,而不是针对EventSourcedBehaviers。这种使用产生行为的封闭类的技术不适用于事件源行为,因为事件源行为由一个命令处理程序和一个事件处理程序组成 在我们的例子中,我们有一个状态特性,它定义了一个方法来接收命令,另一个方法来处理事件。如果我们应用封闭类技术,我们必须为所有状态创建匿名类,但这些状态不能
状态
特性,它定义了一个方法来接收命令,另一个方法来处理事件。如果我们应用封闭类技术,我们必须为所有状态创建匿名类,但这些状态不能序列化
object SampleEventSourced {
trait State extends CborSerializable {
def execute(cmd: Command): ReplyEffect
def apply(evt: Event): State
}
def apply(
persistenceId: PersistenceId,
config: Config,
): Behavior[Command] = {
EventSourcedBehavior
.withEnforcedReplies[Command, Event, State](
persistenceId,
new SampleEventSourced(config).empty(),
(state, cmd) => state.execute(cmd),
(state, evt) => state.apply(evt)
)// ...
}
}
class SampleEventSourced private(config: Config) {
import SampleEventSourced._
private def empty(): State = new State {
override def execute(cmd: Command): ReplyEffect = cmd match {
// ..
}
override def apply(evt: Event): State = evt match {
// ..
}
}
java.lang.IllegalArgumentException:空状态[org.acme.sampleEventSourceed$$anon$1@36d7d2fe]不可序列化
一种解决方案是在每个“状态”方法中复制事件源行为的创建。但这将产生大量重复
object SampleEventSourced {
def apply(
persistenceId: PersistenceId,
config: Config,
): Behavior[Command] = new SampleEventSourced(config).empty()
}
class SampleEventSourced private(config: Config) {
import SampleEventSourced._
private def empty(): Behavior[Command] = EventSourcedBehavior
.withEnforcedReplies[Command, Event, State](
persistenceId,
new State(),
(state, cmd) => state.execute(cmd),
(state, evt) => state.apply(evt)
)// ...
}
另一种方法是创建State
的具体子类,但我们必须在所有这些状态之间传递参数
object SampleEventSourced {
class EmptyState extends State(config:Config, otherUselessParameter:Any) {
// ...
override def apply(evt: Event): evt match {
case _ => new OtherState(config, otherUselessParameter)
}
}
class OtherState extends State(config:Config, veryImportantParameter:Any) {
// ..
}
}
将这些状态类放在封闭类中不会起作用,因为这些非静态内部类无法反序列化
object SampleEventSourced {
trait State extends CborSerializable {
def execute(cmd: Command): ReplyEffect
def apply(evt: Event): State
}
def apply(
persistenceId: PersistenceId,
config: Config,
): Behavior[Command] = {
EventSourcedBehavior
.withEnforcedReplies[Command, Event, State](
persistenceId,
new SampleEventSourced(config).empty(),
(state, cmd) => state.execute(cmd),
(state, evt) => state.apply(evt)
)// ...
}
}
class SampleEventSourced private(config: Config) {
import SampleEventSourced._
private def empty(): State = new State {
override def execute(cmd: Command): ReplyEffect = cmd match {
// ..
}
override def apply(evt: Event): State = evt match {
// ..
}
}
那么,对于这种情况,您的解决方案是什么?您如何处理需要许多参数的状态的EventSourcedBehavior?问题是,除了基于反射之外,
jackson cbor
,在以惯用的Scala方式序列化所做的事情时不是很有效(唯一对它有利的地方是它比Java序列化更好)
因此,显而易见的解决方案是:
- 将状态建模为代数数据类型(通常是由
案例类扩展的
,对于空/初始状态案例,可能是密封特征
)案例对象
- 不要通过Jackson对它们进行序列化,而是使用Scala的本机序列化程序(例如play json、circe等);定义标记特征(例如
)可能更容易因此,在配置中,您只需担心一个binding.Events&state就可以用这个特性来标记:命令及其回复(至少可以通过网络发送的也可以)CirceSerizable
ActorRef
,这将非常复杂,因为ActorRef
的序列化程序最好借助ActorSystem完成代码>(EntityRef
现在可以检查序列化,并且在给定生命周期的情况下,从概念上讲对持久性更有意义,但这将是相当定制的):因此,您可能希望将ActorRef
序列化器/反序列化器的派生延迟到ActorSystem
启动,并将包含ActorRef
的事件/状态/命令/回复的派生延迟到启动之后
我可能倾向于使每个事件的处理程序成为sealed trait
中的一个方法,而不让状态知道事件是如何具体化的。这样做的好处是,您可以在不需要事件或命令的情况下对状态转换进行单元测试
case class ItemAdded(name: String) extends Event
sealed trait State {
def items: Set[String]
def addItem(name: String): State.NonEmpty
}
object State {
case object Empty extends State {
def items: Set[String] = Set.empty
def addItem(name: String): NonEmpty = NonEmpty(Set(name))
}
case class NonEmpty(items: Set[String]) extends State {
def addItem(name: String): NonEmpty = copy(items = items + name)
}
}
那么您的事件处理程序就相当简单了
(state, evt) => {
evt match {
case ItemAdded(name) => state.addItem(name)
}
}
根据您的命令处理程序进行的验证程度,这些验证可能会变得复杂。操作原理是,命令被解释为零个或多个事件,而事件被解释为状态上的操作(状态上的方法会导致新状态)。因此,状态应具有足够的查询方法(例如,项
)以允许命令处理程序使用状态