我对Scala和Akka都很新,我想弄清楚如何创建一个合适的域模型,它也是一个Actor。
让我们假设我们有一个简单的商业案例,您可以在其中开设一个新的银行帐户。假设其中一条规则是您只能为每个姓氏创建一个银行帐户(不现实,但仅为了简单起见)。我的第一种方法,没有应用任何业务规则,看起来像这样:
object Main {
def main(args: Array[String]): Unit = {
implicit val system = ActorSystem("accout")
implicit val materializer = ActorMaterializer()
implicit val executionContext = system.dispatcher
val account = system.actorOf(Props[Account])
account ! CreateAccount("Doe")
}
}
case class CreateAccount(lastName: String)
class Account extends Actor {
var lastName: String = null
override def receive: Receive = {
case createAccount: CreateAccount =>
this.lastName = lastName
}
}
最终你会在某个地方保留这些数据。但是,在添加每个姓氏只能有一个银行帐户的规则时,需要对某些数据存储进行查询。假设我们将该逻辑放在存储库中,并且存储库最终返回Account
,我们得到Account
不再是Actor的问题,因为存储库将无法创建Actors
这绝对是一个错误的实现,而不是如何使用Actors。我的问题是,有什么方法可以解决这些问题?我知道我对Akka的了解还不是很好,所以这可能是一个奇怪/愚蠢的问题。
答案 0 :(得分:5)
一般设计
参与者通常应该是业务逻辑的简单调度程序,并且包含尽可能少的功能。将Actors视为与Future
类似;当你想要scala中的并发时,你不扩展Future类,你只需使用现有逻辑的Future功能。
将你的演员限制为赤裸裸的责任有几个好处:
商业逻辑(No Akka)
在这里,我们将设置所有特定于域的逻辑,而不使用任何 akka相关的“东西”。
object BusinessLogicDomain {
type FirstName = String
type LastName = String
type Balance = Double
val defaultBalance : Balance = 0.0
case class Account(firstName : FirstName,
lastName : LastName,
balance : Balance = defaultBalance)
让我们将您的帐户目录建模为HashMap
:
type AccountDirectory = HashMap[LastName, Account]
val emptyDirectory : AccountDirectory = HashMap.empty[LastName, Account]
我们现在可以创建一个符合您对每个姓氏的不同帐户的要求的函数:
val addAccount : (AccountDirectory, Account) => AccountDirectory =
(accountDirectory, account) =>
if(accountDirectory contains account.lastName)
accountDirectory
else
accountDirectory + (account.lastName -> account)
}//end object BusinessLogicDomain
存储库(Akka)
现在未受污染的业务代码已经完成并且已经隔离,我们可以在基础逻辑之上添加并发层。
我们可以使用Actors的become
功能来存储状态并响应请求:
import BusinessLogicDomain.{Account, AccountDirectory, emptyDirectory, addAccount}
case object QueryAccountDirectory
class RepoActor(accountDirectory : AccountDirectory = emptyDirectory) extends Actor {
val statefulReceive : AccountDirectory => Receive =
currentDirectory => {
case account : Account =>
context become statefulReceive(addAccount(currentDirectory, account))
case QueryAccountDirectory =>
sender ! currentDirectory
}
override def receive : Receive = statefulReceive(accountDirectory)
}
答案 1 :(得分:5)
这可能是一个很长的答案,我很抱歉没有TLDR版本。 :)
好的,所以你想要“演员化”你的领域模型?馊主意。领域模型不一定是演员。有时他们是,但往往他们不是。每个域模型部署一个actor是一种反模式,因为如果你这样做,你只是卸载调用消息调用的方法,但是丢失了方法调用的所有单线程范例。你不能保证消息的时间到达你的演员和基于ASK模式的编程是一个很好的方式来引入一个不可扩展的系统,最终你有太多的线程和太多的未来,不能继续进一步,系统沼泽和扼流圈。那对你的特定问题意味着什么呢?
首先,您必须停止将域模型视为单一事物,并且绝对不再使用POJO实体。当我讨论贫血领域模型时,我完全赞同马丁福勒。在一个构建良好的演员系统中,通常会有三个领域模型。一个是持久化模型,它具有为数据库建模的实体。第二个是不可变模型。这是演员用来相互沟通的模型。所有实体从下到上是不可变的,所有集合都是不可修改的,所有对象只有getter,所有构造函数都将集合复制到新的不可变集合中。不可变模型意味着你的演员永远不必复制任何东西,他们只是传递对数据的引用。最后,您将拥有API模型,这通常是为客户端使用的JSON建模的实体集。 API模型用于将后端与客户端代码更改隔离,反之亦然,它是系统之间的契约。
创建你的演员不再考虑你的持久模型以及你将用它做什么,而是开始考虑用例。你的系统要做什么?根据用例对您的actor进行建模,这将改变actor的实现及其部署策略。
例如,考虑向用户提供库存信息的服务器,包括当前库存水平,用户对用户的评论等等。用户锤击这些信息,随着库存水平的变化,它会迅速变化。该信息可能存储在六个不同的表中。我们不为每个表建模一个actor,而是为这个用例提供单个actor。在这种情况下,这些信息由一大群人在重负载环境中访问。因此,我们最好创建一个actor来聚合所有这些数据并将actor复制到每个节点,每当数据发生变化时,我们都会通知所有节点上的所有复制者。这意味着获得概述的用户甚至不会触摸数据库。他们击中了演员,获得了不可变模型,将其转换为API模型,然后返回数据。
另一方面,如果用户想要更改库存水平,我们需要确保两个用户不同时执行此操作,但大型数据库事务会大幅减慢系统速度。因此,我们选择一个节点来保存该供应商的库存管理参与者,然后我们将碎片聚集为参与者。任何请求都会路由到该actor并按顺序处理。公司用户登录并记录收到的20个新项目的交付。消息从它们点击的任何节点传递到持有该供应商的actor的节点,然后供应商进行适当的数据库更改并广播由所有复制的库存视图参与者拾取的更改以更改其数据。
现在这很简单,因为你必须处理丢失的消息(阅读有关为什么不需要可靠消息传递的文章)。然而,一旦你开始沿着这条路走下去,你很快就会意识到简单地让你的领域模型成为一个演员系统是一种反模式,并且有更好的方法来做事。
无论如何这是我的2美分:)