Главная страница
Навигация по странице:

  • Листинг 20.12.

  • Листинг 20.13.

  • Листинг 20.14.

  • Листинг 20.15.

  • Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста


    Скачать 6.24 Mb.
    НазваниеОдерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
    Дата27.04.2023
    Размер6.24 Mb.
    Формат файлаpdf
    Имя файлаScala. Профессиональное программирование 2022.pdf
    ТипДокументы
    #1094967
    страница46 из 64
    1   ...   42   43   44   45   46   47   48   49   ...   64
    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 можно объявить абстрактным.

    1   ...   42   43   44   45   46   47   48   49   ...   64


    написать администратору сайта