Когда использовать изменяемые vs неизменяемые классы в Scala

Много написано о преимуществах неизменяемого состояния, но существуют ли распространенные случаи в Scala, где имеет смысл предпочесть изменчивые классы? (Это вопрос Scala новичков от кого-то с фоном в "классическом" дизайне ООП с использованием изменяемых классов.)

Для чего-то тривиального, как трехмерный класс Point, я получаю преимущества неизменности. Но как насчет чего-то типа класса Motor, который предоставляет множество контрольных переменных и/или показаний датчиков? Может ли опытный разработчик Scala писать такой класс неизменным? В этом случае "скорость" будет отображаться внутри как "val" вместо "var", а метод "setSpeed" возвращает новый экземпляр класса? Точно так же каждое новое считывание с датчика, описывающего внутреннее состояние двигателя, вызывает создание экземпляра двигателя для экземпляра?

"Старый способ" выполнения ООП в Java или С# с использованием классов для инкапсуляции изменчивого состояния, похоже, очень хорошо подходит для примера с двигателем. Поэтому мне любопытно узнать, сможете ли вы когда-либо получить опыт использования парадигмы неизменяемого состояния, вы бы даже создали такой класс, как Motor, который был бы неизменным.

Ответ 1

Я использую другой классический пример моделирования OO: банковские счета.

Они используются практически на всех курсах OO на планете, и дизайн, который вы обычно заканчиваете, выглядит примерно так:

class Account(var balance: BigDecimal) {
  def transfer(amount: BigDecimal, to: Account): Unit = { 
    balance -= amount
    to.balance += amount
  }
}

IOW: баланс - это данные, а передача - операция. (Заметим также, что передача является сложной операцией, включающей несколько изменяемых объектов, которые, однако, должны быть атомарными, а не сложными... так что вам нужно блокировать и т.д.)

Однако это неправильно. Это не то, как разрабатываются банковские системы. Фактически, это не то, как работает реальный реальный (физический) банковский банкинг. Фактические физические банковские и фактические банковские системы работают следующим образом:

class Account(implicit transactionLog: TransactionLog) {
  def balance = transactionLog.reduceLeft(_ + _)
}

class TransactionSlip(from: Account, to: Account, amount: BigDecimal)

IOW: баланс - это операция, а передача - данные. Обратите внимание, что все здесь неизменно. Баланс - это только левая справка журнала транзакций.

Заметим также, что мы даже не закончили чисто функциональный, неизменный дизайн как явную цель дизайна. Мы просто хотели правильно смоделировать банковскую систему и, по случайности, оказались чисто функциональным, неизменным дизайном. (Ну, это на самом деле не случайно. Там причина, почему реальный банкинг работает именно так, и имеет те же преимущества, что и в программировании: изменчивое состояние и побочные эффекты делают системы сложными и запутанными... и в банковском деле, что означает деньги исчезают.)

Дело здесь в том, что точно такую ​​же проблему можно смоделировать по-разному, и в зависимости от модели вы можете столкнуться с чем-то, что тривиально сделать чисто неизменным или очень тяжелым.

Ответ 2

Я думаю, что короткий ответ, скорее всего: Да, неизменные структуры данных гораздо более эффективны и эффективны, чем вы понимаете.

Вопрос, который вы задали, немного неоднозначен, потому что ответ зависит от описанного вами двигателя, чем от того программного обеспечения, которое вы не описали. Большая ошибка того, как ООП всегда учит, по моему мнению, рекомендует восходящий дизайн классов "домен", прежде чем рассматривать, как будут использоваться классы. Возможно, вашей системе даже требуется более одной структуры данных, содержащей разные данные о двигателе по-разному.

"Старый способ" выполнения ООП в Java или С# с использованием классов для инкапсуляции изменчивого состояния, похоже, очень хорошо подходит к примеру двигателя.

"Новый способ" (возможно), поддерживающий многопоточные системы, заключается в инкапсулировании изменчивого состояния в актеры. Актер, который представляет текущее состояние двигателя, будет изменчивым. Но если вы должны сделать "моментальный снимок" состояния двигателя и передать эту информацию другому актеру, сообщение должно быть неизменным.

В этом [неизменяемом] случае "скорость" будет отображаться внутри как "val" вместо "var", а метод "setSpeed" возвращает новый экземпляр класса?

Да, но на самом деле вам не нужно писать этот метод, если вы используете класс case . Предположим, что у вас есть класс, определенный как case class Motor(speed: Speed, rpm: Int, mass: Mass, color: Color). Используя метод copy, вы можете написать что-то вроде motor2 = motor1.copy(rpm = 3500, speed = 88.mph).