如何在 Scala 中使 class 完全不可变

How to make a class fully immutable in Scala

我正在尝试使以下 class 不可变。 我知道如何做到这一点的理论,但我认为我的实施是错误的。你能帮忙吗?

谢谢

可变 class:

class BankAccount {
private var balance = 0
def deposit(amount: Int) {
    if (amount > 0)
    balance += amount
}
def withdraw(amount: Int): Int =
if (0 < amount && amount <= balance) {
    balance -= amount
    balance        
} else {
    error("insufficient funds")
}

不可变Class

case class BankAccount(b:Int) {

private def deposit(amount: Int):BankAccount {
    if (amount > 0)
    {
        return BankAccount(amount)
    }

}
private def withdraw(amount: Int): BankAccount ={
    if (0 < amount && amount <= balance) {
        return BankAccount(b-amount)       
    } else {
        error("insufficient funds")
    }
}  

}

在函数式编程中,您不会就地更改状态,而是创建新状态并return它。

以下是如何使用函数式编程解决您的用例。

case class BankAccount(val money: Int)

上例class表示BankAccount

不是改变状态,而是创建具有计算值的新状态,然后return它给用户。

def deposit(bankAccount: BankAccount, money: Int): BankAccount = {
  BankAccount(money + backAccount.money)
}

以同样的方式,检查资金并创建新状态并return它给用户。

def withDraw(bankAccount: BankAccount, money: Int): BankAccount = {
  if (money >= 0 && bankAccount.money >= money) {
    BankAccount(bankAccount.money - money)
  } else error("in sufficient funds")
}

在函数式编程中,创建新状态而不是尝试改变旧状态是很常见的。

创建新状态 return,就是这样 !!!

首先,好消息是:您的对象几乎 是不可变的。现在,坏消息是:它们不起作用。

只有 "almost" 是不可变的,因为你的 class 不是 final:我可以扩展它并覆盖方法来改变某些状态。

现在,为什么它不起作用?最明显的错误是,在您的 deposit 方法中,您 return 一个新的 BankAccount 其余额设置为存入的金额。因此,您损失了存款前的所有资金!您需要将存款添加到余额中,而不是用存款替换余额。

还有其他问题:你的deposit方法有一个return类型的BankAccount,但并不总是return一个BankAccount : 如果 amount 小于或等于零,则 returns UnitBankAccountUnit 最具体的公共超类型是 Any,所以你的方法实际上是 returns Any。有多种方法可以解决这个问题,例如returning Option[BankAccount]Try[BankAccount]Either[SomeErrorType, BankAccount],或者只是抛出异常。对于我的示例,我将完全忽略验证。 (withdraw中也存在类似的问题。)

像这样:

final case class BankAccount(balance: Int) {
  private def deposit(amount: Int) = copy(balance = balance + amount)
  private def withdraw(amount: Int) = copy(balance = balance - amount)       
}

请注意,我使用编译器生成的 copy 方法来处理 classes 的情况,它允许您创建仅更改一个字段的实例的副本。在您的特定情况下,您只有一个字段,但进入该字段是一种很好的做法。

所以,这行得通。或者……是吗?好吧,不,实际上,它没有!问题是我们正在创建新的银行账户……里面有钱……我们正在凭空创造新的钱!如果我的帐户中有 100 美元,我可以提取其中的 90 美元,然后我得到 returned 一个新的银行帐户对象,其中有 10 美元。但是我仍然可以访问里面有 100 美元的旧银行帐户对象!所以,我有两个银行账户,总共有 110 美元加上我提取的 90 美元;我现在有200块钱了!

解决这个问题很重要,我暂时搁置它。

最后,我想向您展示一些与现实世界银行系统实际工作方式有点接近的东西,我的意思是"banking systems in the real-world, as in, before the invention of electronic banking" ,以及 "electronic banking systems as they are actually used",因为令人惊讶(或没有),它们实际上是一样的。

在您的系统中,余额为数据,存取款为操作。但在现实世界中,恰恰是对偶:存款和取款是 数据,计算余额是操作。在我们使用电脑之前,银行出纳员会为每笔交易写交易单,然后在一天结束时收集这些交易单,所有的资金流动加起来。和电子银行系统一样,大致是这样的:

final case class TransactionSlip(source: BankAccount, destination: BankAccount, amount: BigDecimal)

final case class BankAccount {
  def balance =
    TransactionLog.filter(slip.destination == this).map(_.amount).reduce(_ + _) - 
    TransactionLog.filter(slip.source == this).map(_.amount).reduce(_ + _)
}

因此,单笔交易记录在日志中,余额的计算方法是将以该账户为目的地的所有交易金额相加,然后减去以该账户为目的地的所有交易金额之和。帐户作为来源。显然有很多实现细节我没有给你看,例如事务日志是如何工作的,并且可能应该对余额进行一些缓存,这样您就不需要一遍又一遍地计算它。另外,我忽略了验证(这也需要计算余额)。

我添加此示例是为了向您展示可以通过截然不同的设计来解决相同的问题,并且某些设计更自然地适合函数式方法。请注意,这第二个系统是几十年来银行业的运作方式,远在计算机存在之前,它非常自然地适用于函数式编程。