Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
437 Вот как записывается тип «животное, поедающее траву»: Animal { type SuitableFood = Grass } Теперь, имея в своем распоряжении этот тип, класс «пастбища» можно за писать следующим образом: class Pasture: var animals: List[Animal { type SuitableFood = Grass }] = Nil // ... 20 .9 . Практический пример: работа с валютой Далее в главе рассмотрен практический пример, объясняющий порядок ис пользования в Scala абстрактных типов. При этом будет поставлена задача разработать класс Currency . Обычный его экземпляр будет представлять денежную сумму в долларах, евро, йенах и некоторых других валютах. Он позволит совершать с валютой ряд арифметических операций. Например, можно будет сложить две суммы в одной и той же валюте. Или умножить текущую сумму на коэффициент процентной ставки. Эти соображения приводят к следующей первой конструкции класса валют: // первая (нерабочая) конструкция класса Currency abstract class Currency: val amount: Long def designation: String override def toString = s"$amount $designation" def + (that: Currency): Currency = ... def * (x: Double): Currency = ... Поле amount (сумма) в классе валют — количество представляемых ею валют ных единиц. Оно имеет тип Long , то есть представляемая сумма денежных средств может быть очень крупной, сравнимой с рыночной капитализацией Google или Apple. Здесь поле оставлено абстрактным в ожидании своего определения, когда в подклассе зайдет речь о конкретной сумме. Наимено вание валюты designation — строка, которая идентифицирует эту валюту. Метод toString класса Currency показывает сумму и наименование валюты. Он будет выдавать результат следующего вида: 79 USD 11000 Yen 99 Euro 438 Глава 20 • Абстрактные члены И наконец, имеются методы + для сложения сумм в валюте и * для умно жения суммы в валюте на число с плавающей точкой. Конкретное значение в валюте можно создать, предоставив конкретные значения суммы и наи менования валюты: new Currency: val amount = 79L def designation = "USD" Эта конструкция не вызовет нареканий, если задумано моделирование с ис пользованием лишь одной валюты, например только долларов или только евро. Но она не будет работать при необходимости иметь дело сразу с не сколькими валютами. Предположим, выполняется моделирование долларов и евро в качестве двух подклассов класса валюты: abstract class Dollar extends Currency: def designation = "USD" abstract class Euro extends Currency: def designation = "Euro" На первый взгляд все выглядит вполне разумно. Но данный код позво лит складывать доллары с евро. Результатом такого сложения будет тип Currency . Но это будет весьма забавная валюта — смесь евро и долларов. Вме сто этого нужно получить более специализированную версию метода + . При его реализации в классе Dollar он должен получать аргументы типа Dollar и выдавать результат типа Dollar ; при реализации в классе Euro — получать аргументы типа Euro и выдавать результат типа Euro . Следовательно, тип метода сложения будет изменяться в зависимости от того, в каком классе находится. И все же хотелось бы создать метод сложения единожды, а не делать это при каждом новом определении валюты. Чтобы помочь справиться с подобными ситуациями, Scala предоставляет весьма простую технологию. Если к моменту определения класса чтото еще неизвестно, то нужно сделать это «чтото» абстрактным. Технология применима как к значениям, так и к типам. В случае с валютами точные типы аргументов и результирующие типы метода сложения неизвестны, следовательно, являются подходящими кандидатами для того, чтобы стать абстрактными. Это привело бы к следующей предварительной версии кода класса AbstractCur rency : // вторая (все еще несовершенная) конструкция класса Currency abstract class AbstractCurrency: 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 можно объявить абстрактным. |