函数式编程提升了不可变类和引用透明性。
Domain-driven design 由 Value Object(不可变)和 Entities(可变)组成。
我们应该创建不可变实体而不是可变实体吗?
让我们假设,项目使用 Scala 作为主要语言,如果我们处理并发问题,我们如何将实体编写为案例类(不可变的那样)而不会有陈旧状态的风险?
什么是好习惯?保持实体可变(var字段 etc...)并避免案例类的良好语法?
var
您可以在 Scala 中有效地使用不可变实体,并避免可变字段的恐怖以及源自可变状态的所有错误。使用 Immutable 实体可以帮助您实现并发性,不会让事情变得更糟。您之前的可变状态将成为一组转换,它将在每次更改时创建新的引用。
但是,在您的应用程序的某个级别,您将需要具有可变状态,否则您的应用程序将无用。我们的想法是在程序逻辑中尽可能地推动它。我们举一个银行账户的例子,它可以因利率和 ATM 取款或存款而改变。
您有两种有效的方法:
您公开了可以修改内部属性的方法,并且您可以管理这些方法的并发性(事实上很少)
您使所有类都不可变,并使用可以更改帐户的“管理器”将其包围。
由于第一个非常简单,我将详述第一个。
case class BankAccount(val balance:Double, val code:Int) class BankAccountRef(private var bankAccount:BankAccount){ def withdraw(withdrawal) = { bankAccount = bankAccount.copy(balance = bankAccount.balance - withdrawal) bankAccount.balance } }
这很好,但天哪,你仍然坚持管理并发。那么,Scala 为您提供了解决方案。这里的问题是,如果您将对 BankAccountRef 的引用分享给后台作业,则必须同步该调用。问题是你以不理想的方式进行并发。
进行并发的最佳方式:消息传递
如果另一方面,不同的作业不能直接在 BankAccount 或 BankAccountRef 上调用方法,而只是通知他们需要执行某些操作,该怎么办?那么,你有一个 Actor,这是在 Scala 中做并发的最喜欢的方式。
class BankAccountActor(private var bankAccount:BankAccount) extends Actor { def receive { case BalanceRequest => sender ! Balance(bankAccount.balance) case Withdraw(amount) => { this.bankAccount = bankAccount.copy(balance = bankAccount.balance - amount) } case Deposit(amount) => { this.bankAccount = bankAccount.copy(balance = bankAccount.balance + amount) } } }
该解决方案在 Akka 文档中有详细描述:http://doc.akka.io/docs/akka/2.1.0/scala/actors.html。我们的想法是通过向其邮箱发送消息与 Actor 通信,并按接收顺序处理这些消息。因此,如果使用此模型,您将永远不会遇到并发缺陷。
这是一个意见问题,与您认为的 scala 具体相比较少。
如果你真的想拥抱 FP 我会为你所有的域对象都采用不可变的路由,并且永远不会放任何行为。
这就是有些人将上述服务模式称为行为和状态之间始终存在分离。这在 OOP 中避开,但在 FP 中是自然的。
它还取决于您的域名。使用 UI 和视频游戏等有状态的东西,OOP 有时会更容易。对于像网站或 REST 这样的硬核后端服务,我认为服务模式更好。
除了经常提到的并发性之外,我喜欢两个非常好的不可变对象的东西是它们对于缓存更加可靠,并且它们对于分布式消息传递(e.g. protobuf over amqp)也非常好,因为意图非常明确。
同样在 FP 中,人们通过创建“语言”或“对话”(即 DSL)来实现可变到不可变的桥梁(建设者,Monads,Pipes,Arrows,STM etc...),使您能够变异,然后转换回不可变域。上面提到的使用 DSL 进行更改。这比您想象的更自然(e.g. SQL 是一个示例“对话”)。另一方面,OOP 更喜欢具有可变域并利用该语言的现有过程部分。
2 回答
您可以在 Scala 中有效地使用不可变实体,并避免可变字段的恐怖以及源自可变状态的所有错误。使用 Immutable 实体可以帮助您实现并发性,不会让事情变得更糟。您之前的可变状态将成为一组转换,它将在每次更改时创建新的引用。
但是,在您的应用程序的某个级别,您将需要具有可变状态,否则您的应用程序将无用。我们的想法是在程序逻辑中尽可能地推动它。我们举一个银行账户的例子,它可以因利率和 ATM 取款或存款而改变。
您有两种有效的方法:
您公开了可以修改内部属性的方法,并且您可以管理这些方法的并发性(事实上很少)
您使所有类都不可变,并使用可以更改帐户的“管理器”将其包围。
由于第一个非常简单,我将详述第一个。
这很好,但天哪,你仍然坚持管理并发。那么,Scala 为您提供了解决方案。这里的问题是,如果您将对 BankAccountRef 的引用分享给后台作业,则必须同步该调用。问题是你以不理想的方式进行并发。
进行并发的最佳方式:消息传递
如果另一方面,不同的作业不能直接在 BankAccount 或 BankAccountRef 上调用方法,而只是通知他们需要执行某些操作,该怎么办?那么,你有一个 Actor,这是在 Scala 中做并发的最喜欢的方式。
该解决方案在 Akka 文档中有详细描述:http://doc.akka.io/docs/akka/2.1.0/scala/actors.html。我们的想法是通过向其邮箱发送消息与 Actor 通信,并按接收顺序处理这些消息。因此,如果使用此模型,您将永远不会遇到并发缺陷。
这是一个意见问题,与您认为的 scala 具体相比较少。
如果你真的想拥抱 FP 我会为你所有的域对象都采用不可变的路由,并且永远不会放任何行为。
这就是有些人将上述服务模式称为行为和状态之间始终存在分离。这在 OOP 中避开,但在 FP 中是自然的。
它还取决于您的域名。使用 UI 和视频游戏等有状态的东西,OOP 有时会更容易。对于像网站或 REST 这样的硬核后端服务,我认为服务模式更好。
除了经常提到的并发性之外,我喜欢两个非常好的不可变对象的东西是它们对于缓存更加可靠,并且它们对于分布式消息传递(e.g. protobuf over amqp)也非常好,因为意图非常明确。
同样在 FP 中,人们通过创建“语言”或“对话”(即 DSL)来实现可变到不可变的桥梁(建设者,Monads,Pipes,Arrows,STM etc...),使您能够变异,然后转换回不可变域。上面提到的使用 DSL 进行更改。这比您想象的更自然(e.g. SQL 是一个示例“对话”)。另一方面,OOP 更喜欢具有可变域并利用该语言的现有过程部分。