Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 788
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
20 .9 . Практический пример: работа с валютой 439
type Currency <: AbstractCurrency val amount: Long def designation: String override def toString = s"$amount $designation"
def + (that: Currency): Currency = ...
def * (x: Double): Currency = ...
Единственное отличие от прежней ситуации заключается в том, что класс теперь называется
AbstractCurrency и содержит абстрактный тип
Currency
, представляющий стоящую под вопросом реальную валюту. Каждому кон
кретному подклассу
AbstractCurrency придется фиксировать тип
Currency
, чтобы обозначать конкретный подкласс как таковой, тем самым затягивая узел.
Вот как, к примеру, выглядит новая версия класса
Dollar
, которая теперь расширяет класс
AbstractCurrency
:
abstract class Dollar extends AbstractCurrency:
type Currency = Dollar def designation = "USD"
Данная конструкция вполне работоспособна, но попрежнему далека от совершенства. Есть проблема, скрывающаяся за многоточиями, которые по
казывают в классе
AbstractCurrency пропущенные определения методов
+
и
*
. В частности, как в этом классе должен быть реализован метод сложения?
Нетрудно вычислить правильную сумму в новой валюте как this.amount
+
that.amount
, но как преобразовать сумму в валюту нужного типа?
Можно попробовать применить следующий код:
def + (that: Currency): Currency =
new Currency:
val amount = this.amount + that.amount
Но он не пройдет компиляцию:
7 | new Currency:
| ˆˆˆˆˆˆˆˆ
| AbstractCurrency.this.Currency is not a class type
8 | val amount = this.amount + that.amount
| ˆ
| Recursive value amount needs type
Одно из ограничений в трактовке в Scala абстрактных типов заключается в невозможности создать экземпляр абстрактного типа, а также в невоз
можности абстрактного типа играть роль супертипа для другого класса.
Следовательно, компилятор будет отвергать код показанного здесь примера, в котором предпринимается попытка создать экземпляр
Currency
440 Глава 20 • Абстрактные члены
Но эти ограничения можно обойти, используя фабричный метод. Вместо того чтобы создавать экземпляр абстрактного класса, напрямую объявите абстрактный метод, который делает это. Затем там, где абстрактный тип устанавливается в какойлибо конкретный тип, нужно предоставить кон
кретную реализацию фабричного метода. Для класса
AbstractCurrency все вышесказанное будет выглядеть так:
abstract class AbstractCurrency:
type Currency <: AbstractCurrency // абстрактный тип def make(amount: Long): Currency // фабричный метод
... // вся остальная часть
// определения класса
Подобную конструкцию, конечно, можно заставить работать, но выглядит она както подозрительно. Зачем помещать фабричный метод внутрь класса
AbstractCur rency
? Это выглядит довольно сомнительно как минимум по двум причинам. Вопервых, если есть некая сумма в валюте (скажем, один доллар), то есть и возможность нарастить сумму в той же валюте, используя следующий код:
myDollar.make(100) // здесь еще сто!
В эпоху цветных ксероксов данный скрипт может стать заманчивым, но следует надеяться, что никто не сможет проделывать это слишком долго, не будучи пойманным за руку. Вовторых, этот код, если у вас уже есть ссылка на объект
Currency
, позволяет создавать дополнительные объекты
Currency
Но как получить первый объект данной валюты
Currency
? Вам понадобится другой метод создания, выполняющий практически ту же работу, что и make
То есть вы столкнулись со случаем дублирования кода, являющимся верным признаком «кода с душком».
Решение, конечно же, будет заключаться в перемещении абстрактного типа и фабричного класса за пределы класса
AbstractCurrency
. Нужно создать другой класс, содержащий класс
AbstractCurrency
, тип
Currency и фабрич
ный метод make
Назовем этот класс
CurrencyZone
:
abstract class CurrencyZone:
type Currency <: AbstractCurrency def make(x: Long): Currency abstract class AbstractCurrency:
val amount: Long def designation: String override def toString = s"$amount $designation"
def + (that: Currency): Currency =
20 .9 . Практический пример: работа с валютой 441
make(this.amount + that.amount)
def * (x: Double): Currency =
make((this.amount * x).toLong)
Пример конкретизации
CurrencyZone
— объект
US
, который можно опреде
лить следующим образом:
object US extends CurrencyZone:
abstract class Dollar extends AbstractCurrency:
def designation = "USD"
type Currency = Dollar def make(x: Long) = new Dollar { val amount = x }
Здесь
US
— объект, расширяющий
CurrencyZone
. В нем определяется класс
Dollar
, являющийся подклассом
AbstractCurrency
. Следовательно, тип денежных единиц в этой зоне — доллар США,
US.Dollar
. Объект
US
также устанавливает, что тип
Currency будет псевдонимом для
Dollar
, и предо
ставляет реализацию фабричного метода make для возвращения суммы в долларах.
Конструкция вполне работоспособна. Нужно лишь добавить несколько уточ
нений. Первое из них касается разменных монет. До сих пор каждая валюта измерялась в целых единицах: в долларах, евро или йенах. Но у большинства валют имеются разменные монеты, например, в США есть доллары и центы.
Наиболее простой способ моделировать центы — использовать поле amount в
US.Currency
, представленное в центах, а не в долларах. Чтобы вернуться к доллару, будет полезно ввести в класс
CurrencyZone поле
CurrencyUnit
, содержащее одну стандартную единицу в данной валюте:
abstract class CurrencyZone:
val CurrencyUnit: Currency
Как показано в листинге 20.12, в объекте
US
могут быть определены величи
ны
Cent
,
Dollar и
CurrencyUnit
. Это определение объекта похоже на преды
дущее, за исключением того, что в него добавлены три новых поля. Поле
Cent представляет сумму в 1
US.Currency
. Это объект, аналогичный одноцентовой монете. Поле
Dollar представляет сумму в 100
US.Currency
. Следовательно, объект
US
теперь определяет имя
Dollar двумя способами. Тип
Dollar
, опре
деленный абстрактным внутренним классом по имени
Dollar
, представляет общее название валюты
Currency
, действительное в валютной зоне
US
. В от
личие от этого значение
Dollar
, на которое ссылается val
поле по имени
Dollar
, представляет 1 доллар США, аналогичный однодолларовой купюре.
Третье определение поля
CurrencyUnit указывает на то, что стандартная
442 Глава 20 • Абстрактные члены денежная единица в зоне США — доллар,
Dollar
, то есть значение
Dollar
, на которое ссылается поле, не является типом
Dollar
Метод toString в классе
AbstractCurrency также нуждается в адаптации для восприятия разменных монет на счету. Например, сумма 10 долларов
23 цента должна выводиться как десятичное число:
10.23
USD
. Чтобы до
биться этого результата, принадлежащий
AbstractCurrency метод toString можно реализовать следующим образом:
override def toString =
((amount.toDouble / CurrencyUnit.amount.toDouble)
.formatted(s"%.${decimals(CurrencyUnit.amount)}f")
+ " " + designation)
Здесь formatted является методом, доступным в Scala в нескольких классах, включая
Double
1
. Метод formatted возвращает строку, полученную в ре
зультате форматирования исходного
Double
, в отношении которой он был вызван, в соответствии со строкой форматирования, переданной ему в виде его правого операнда. Синтаксис строк форматирования, передаваемых методу formatted
, аналогичен синтаксису, используемому для Javaметода
String.format
Листинг 20.12. Зона валюты США
object US extends CurrencyZone:
abstract class Dollar extends AbstractCurrency:
def designation = "USD"
type Currency = Dollar def make(cents: Long) =
new Dollar:
val amount = cents val Cent = make(1)
val Dollar = make(100)
val CurrencyUnit = Dollar
Например, строка форматирования
%.2f форматирует число с двумя знака
ми после точки. Строка форматирования, примененная в показанном ранее методе toString
, собирается путем вызова метода decimals в отношении
CurrencyUnit.amount
. Данный метод возвращает число десятичных знаков десятичной степени за вычетом единицы. Например, decimals(10)
— это 1, decimals(100)
— это 2 и т. д. Метод decimals реализован в виде простой рекурсии:
1
Чтобы обеспечить доступность метода formatted
, в Scala используются обогаща
ющие оболочки, рассмотренные в разделе 5.10.
20 .9 . Практический пример: работа с валютой 443
private def decimals(n: Long): Int =
if n == 1 then 0 else 1 + decimals(n / 10)
В листинге 20.13 показаны некоторые другие валютные зоны. В качестве еще одного уточнения к модели можно добавить свойство обмена валют. Сначала, как показано в листинге 20.14, можно создать объект
Converter
, содержащий применяемые обменные курсы валют. Затем к классу
AbstractCurrency можно добавить метод обмена, from
, который выполняет конвертацию из заданной исходной валюты в текущий объект
Currency
:
def from(other: CurrencyZone#AbstractCurrency): Currency =
make(math.round(
other.amount.toDouble * Converter.exchangeRate
(other.designation)(this.designation)))
Листинг 20.13. Валютные зоны для Европы и Японии object Europe extends CurrencyZone:
abstract class Euro extends AbstractCurrency:
def designation = "EUR"
type Currency = Euro def make(cents: Long) =
new Euro:
val amount = cents val Cent = make(1)
val Euro = make(100)
val CurrencyUnit = Euro object Japan extends CurrencyZone:
abstract class Yen extends AbstractCurrency:
def designation = "JPY"
type Currency = Yen def make(yen: Long) =
new Yen:
val amount = yen val Yen = make(1)
val CurrencyUnit = Yen
Листинг 20.14. Объект converter с отображением курсов обмена object Converter:
var exchangeRate =
Map(
"USD" –> Map("USD" –> 1.0, "EUR" –> 0.8498,
"JPY" –> 1.047, "CHF" –> 0.9149),
"EUR" –> Map("USD" –> 1.177, "EUR" –> 1.0,
"JPY" –> 1.232, "CHF" –> 1.0765),
444 Глава 20 • Абстрактные члены "JPY" –> Map("USD" –> 0.9554, "EUR" –> 0.8121,
"JPY" –> 1.0, "CHF" –> 0.8742),
"CHF" –> Map("USD" –> 1.093, "EUR" –> 0.9289,
"JPY" –> 1.144, "CHF" –> 1.0)
)
Метод from получает в качестве аргумента произвольную валюту. Это вы
ражено его формальным типом параметра
CurrencyZone#AbstractCurrency
, который показывает, что переданный как other аргумент должен быть типа
AbstractCurrency в некоторой произвольной и неизвестной валютной зоне
CurrencyZone
. Результат метода — перемножение суммы в другой валюте с курсом обмена между другой и текущей валютами
1
Финальная версия класса
CurrencyZone показана в листинге 20.15.
Листинг 20.15. Полный код класса CurrencyZone abstract class CurrencyZone:
type Currency <: AbstractCurrency def make(x: Long): Currency abstract class AbstractCurrency:
val amount: Long def designation: String def + (that: Currency): Currency =
make(this.amount + that.amount)
def * (x: Double): Currency =
make((this.amount * x).toLong)
def - (that: Currency): Currency =
make(this.amount - that.amount)
def / (that: Double) =
make((this.amount / that).toLong)
def / (that: Currency) =
this.amount.toDouble / that.amount def from(other: CurrencyZone#AbstractCurrency): Currency =
make(math.round(
other.amount.toDouble * Converter.exchangeRate
(other.designation)(this.designation)))
private def decimals(n: Long): Int =
if (n == 1) 0 else 1 + decimals(n / 10)
1
Кстати, если вы полагаете, что сделка по японской йене будет неудачной, то курсы обмена валют основаны на числовых показателях в их
CurrencyZone
. Таким обра
зом, 1.211 — курс обмена центов США на японскую йену.
20 .9 . Практический пример: работа с валютой 445
override def toString =
((amount.toDouble / CurrencyUnit.amount.toDouble)
.formatted(s"%.${decimals(CurrencyUnit.amount)}f")
+ " " + designation)
end AbstractCurrency val CurrencyUnit: Currency end CurrencyZone
Класс можно опробовать, вводя команды в REPL Scala. Предполагается, что класс
CurrencyZone и все конкретные объекты
CurrencyZone определе
ны в пакете org.stairwaybook.currencies
. Сперва нужно импортировать org.stairwaybook.currencies.*
в REPL. Затем можно будет выполнить ряд обменных операций с валютой:
scala> val yen = Japan.Yen.from(US.Dollar * 100)
val yen: Japan.Currency = 10470 JPY
scala> val euros = Europe.Euro.from(yen)
val euros: Europe.Currency = 85.03 EUR
scala> val dollars = US.Dollar.from(euros)
val dollars: US.Currency = 100.08 USD
Из факта получения почти такого же значения после трех конвертаций сле
дует, что у нас весьма выгодные курсы обмена! Кроме того, можно нарастить значение в некоторой валюте:
scala> US.Dollar * 100 + dollars res3: US.Currency = 200.08 USD
В то же время складывать суммы разных валют нельзя:
scala> US.Dollar + Europe.Euro
1 |US.Dollar + Europe.Euro
| ˆˆˆˆˆˆˆˆˆˆˆ
|Found: (Europe.Euro : Europe.Currency)
|Required: US.Currency(2)
|where: Currency is a type in object Europe which
| is an alias of Europe.Euro
| Currency(2) is a type in object US which is
| an alias of US.Dollar
Абстракция типов выполняет свою работу, не позволяя складывать два зна
чения в разных единицах измерения (в данном случае валютах). Она мешает нам выполнять необоснованные вычисления. Неверные преобразования
446 Глава 20 • Абстрактные члены между различными единицами могут показаться небольшими недочетами, но способны привести к весьма серьезным системным сбоям. Например, к аварии спутника Mars Climate Orbiter 23 сентября 1999 года, вызванной тем, что одна команда инженеров использовала метрическую систему мер, а другая — систему мер, принятую в Великобритании. Если бы единицы из
мерения были запрограммированы так же, как сделано с валютой в текущей главе, то данная ошибка была бы выявлена во время простого запуска кода на компиляцию. Вместо этого она стала причиной аварии космического аппарата после почти десятимесячного полета.
Резюме
В Scala предлагается рационально структурированная и самая общая под
держка объектноориентированной абстракции. При этом допускается применение не только абстрактных методов, но и значений, переменных и типов. В данной главе мы показали способы извлечь преимущества из использования абстрактных членов класса. С их помощью реализуется простой, но весьма эффективный принцип структурирования систем: все неизвестное при разработке класса нужно превращать в абстрактные члены.
Тогда система типов задаст направление развитию вашей модели точно так же, как вы увидели в примере с валютой. И неважно, что именно будет неиз
вестно: тип, метод, переменная или значение. Все это в Scala можно объявить абстрактным.
1 ... 42 43 44 45 46 47 48 49 ... 64