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

  • Листинг 10.10.

  • Листинг 10.11.

  • Листинг 10.12.

  • Листинг 10.13.

  • Листинг 10.14.

  • Листинг 11.1.

  • Листинг 11.2.

  • 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
    страница25 из 64
    1   ...   21   22   23   24   25   26   27   28   ...   64
    Листинг 10.9. Класс Element с методами above, beside и toString abstract class Element:
    def contents: Vector[String]
    def width: Int =
    if height == 0 then 0 else contents(0).length def height: Int = contents.length def above(that: Element): Element =
    VectorElement(this.contents ++ that.contents)
    def beside(that: Element): Element =
    VectorElement(
    for (line1, line2) <- this.contents.zip(that.contents)
    yield line1 + line2
    )
    override def toString = contents.mkString("\n")
    end Element
    Обратите внимание: метод toString не требует указания пустого списка па­
    раметров. Это соответствует рекомендациям по соблюдению принципа еди­
    нообразного доступа, поскольку toString
    — чистый метод, не получа ющий никаких параметров. После добавления этих трех методов класс
    Element приобретет вид, показанный в листинге 10.9.

    10 .13 . Определяем фабричный объект 223
    10 .13 . Определяем фабричный объект
    Теперь у вас есть иерархия классов для элементов разметки. Можно предо­
    ставить ее вашим клиентам как есть или выбрать технологию сокрытия иерархии за фабричным объектом.
    В нем содержатся методы, с помощью которых клиенты смогут создавать объ­
    екты вместо того, чтобы делать это непосредственно через их конструкторы.
    Преимущество такого подхода заключается в возможности централизации создания объектов и в сокрытии способа представления объектов с помощью классов. Такое сокрытие сделает вашу библиотеку понятнее для клиентов, по­
    скольку в открытом виде будет предоставлено меньше подробностей. Вдобавок оно обеспечит вам больше возможностей вносить последующие изменения реализации библиотеки, не нарушая работу клиентского кода.
    Первая задача при конструировании фабрики для элементов разметки — выбор места, в котором должны располагаться фабричные методы. Чьими элементами они должны быть — объекта­одиночки или класса? Как должен быть назван содержащий их объект или класс? Существует множество воз­
    можностей. Самое простое решение — создать объект­компаньон класса
    Element и превратить его в фабричный объект для элементов разметки.
    Таким образом, клиентам нужно предоставить только комбинацию «класс — объект
    Element
    », а реализацию трех классов,
    VectorElement
    ,
    LineElement и
    UniformElement
    , можно скрыть.
    В листинге 10.10 представлена структура объекта
    Element
    , соответствующего этой схеме. В объекте
    Element содержатся три переопределяемых варианта метода elem
    , которые конструируют различный вид объекта разметки.
    Листинг 10.10. Фабричный объект с фабричными методами object Element:
    def elem(contents: Vector[String]): Element =
    VectorElement(contents)
    def elem(chr: Char, width: Int, height: Int): Element =
    UniformElement(chr, width, height)
    def elem(line: String): Element =
    LineElement(line)
    С появлением этих фабричных методов наметился смысл изменить реализа­
    цию класса
    Element таким образом, чтобы в нем вместо явного создания но­
    вых экземпляров
    VectorElement выполнялись фабричные методы elem
    . Что­
    бы вызвать фабричные методы, не указывая с ними имя объекта­ одиночки

    224 Глава 10 • Композиция и наследование
    Element
    , мы импортируем в верхней части кода исходный файл
    Element.elem
    Иными словами, вместо вызова фабричных методов с помощью указания
    Element.elem внутри класса
    Element мы импортируем
    Element.elem
    , чтобы можно было просто вызвать фабричные методы по имени elem
    . Код класса
    Element после внесения изменений показан в листинге 10.11.
    Листинг 10.11. Класс Element, реорганизованный для использования фабричных методов import Element.elem abstract class Element:
    def contents: Vector[String]
    def width: Int =
    if height == 0 then 0 else contents(0).length def height: Int = contents.length def above(that: Element): Element =
    elem(this.contents ++ that.contents)
    def beside(that: Element): Element =
    elem(
    for (line1, line2) <- this.contents.zip(that.contents)
    yield line1 + line2
    )
    override def toString = contents.mkString("\n")
    end Element
    Кроме того, благодаря наличию фабричных методов теперь подклассы
    Vec torElement
    ,
    LineElement и
    UniformElement могут стать приватными, поскольку отпадет надобность непосредственного обращения к ним со стороны клиентов. В Scala классы и объекты­одиночки можно определять внутри других классов и объектов­одиночек. Один из способов превратить подклассы класса
    Element в приватные — поместить их внутрь объекта­оди­
    ночки
    Element и объявить их там приватными. Классы по­прежнему будут доступны трем фабричным методам elem там, где в них есть надобность. Как это будет выглядеть, показано в листинге 10.12.
    10 .14 . Методы heighten и widen
    Нам нужно внести еще одно, последнее усовершенствование. Версия
    Element
    , показанная в листинге 10.11, не может всецело нас устроить, поскольку не

    10 .14 . Методы heighten и widen 225
    позволяет клиентам помещать друг на друга элементы разной ширины или помещать рядом друг с другом элементы разной высоты.
    Например, вычисление следующего выражения не будет работать коррект­
    но, так как второй ряд в объединенном элементе длиннее первого (см. ли­
    стинг 10.12).
    Листинг 10.12. Сокрытие реализации с помощью использования приватных классов elem(Vector("hello")) above elem(Vector("world!"))
    object Element:
    private class VectorElement(
    val contents: Vector[String]
    ) extends Element private class LineElement(s: String) extends Element:
    val contents = Vector(s)
    override def width = s.length override def height = 1
    private class UniformElement(
    ch: Char,
    override val width: Int,
    override val height: Int
    ) extends Element:
    private val line = ch.toString * width def contents = Vector.fill(height)(line)
    def elem(contents: Vector[String]): Element =
    VectorElement(contents)
    def elem(chr: Char, width: Int, height: Int): Element =
    UniformElement(chr, width, height)
    def elem(line: String): Element =
    LineElement(line)
    end Element
    Аналогично этому вычисление следующего выражения не будет работать правильно из­за того, что высота первого элемента
    VectorElement составляет два ряда, а второго — только один:
    elem(Vector("one", "two")) beside elem(Vector("one"))

    226 Глава 10 • Композиция и наследование
    В листинге 10.13 показан приватный вспомогательный метод по имени widen
    , который получает ширину и возвращает объект
    Element указанной ширины. Результат включает в себя содержимое этого объекта, которое для достижения нужной ширины отцентрировано за счет создания отступов справа и слева с помощью любого нужного для этого количества пробелов.
    В листинге также показан похожий метод heighten
    , выполняющий то же в вертикальном направлении. Метод widen вызывается методом above
    , что­
    бы обеспечить одинаковую ширину элементов, которые помещаются друг над другом. Аналогично этому метод heighten вызывается методом beside
    , чтобы обеспечить одинаковую высоту элементов, помещаемых рядом друг с другом. После внесения этих изменений библиотека разметки будет готова к использованию.
    Листинг 10.13. Класс Element с методами widen и heighten import Element.elem abstract class Element:
    def contents: Vector[String]
    def width: Int =
    if height == 0 then 0 else contents(0).length def height: Int = contents.length def above(that: Element): Element =
    val this1 = this.widen(that.width)
    val that1 = that.widen(this.width)
    elem(this1.contents ++ that1.contents)
    def beside(that: Element): Element =
    val this1 = this.heighten(that.height)
    val that1 = that.heighten(this.height)
    elem(
    for (line1, line2) <- this1.contents.zip(that1.contents)
    yield line1 + line2
    )
    def widen(w: Int): Element =
    if w <= width then this else val left = elem(' ', (w - width) / 2, height)
    val right = elem(' ', w — width - left.width, height)
    left beside this beside right def heighten(h: Int): Element =
    if h <= height then this else

    10 .15 . Собираем все вместе 227
    val top = elem(' ', width, (h - height) / 2)
    val bot = elem(' ', width, h — height - top.height)
    top above this above bot override def toString = contents.mkString("\n")
    end Element
    10 .15 . Собираем все вместе
    Интересным способом применения почти всех элементов библиотеки разметки будет написание программы, рисующей спираль с заданным количеством ребер. Ее созданием займется программа
    Spiral
    , показанная в листинге 10.14.
    Листинг 10.14. Приложение Spiral import Element.elem object Spiral:
    val space = elem(" ")
    val corner = elem("+")
    def spiral(nEdges: Int, direction: Int): Element =
    if nEdges == 1 then elem("+")
    else val sp = spiral(nEdges - 1, (direction + 3) % 4)
    def verticalBar = elem('|', 1, sp.height)
    def horizontalBar = elem('-', sp.width, 1)
    if direction == 0 then
    (corner beside horizontalBar) above (sp beside space)
    else if direction == 1 then
    (sp above space) beside (corner above verticalBar)
    else if direction == 2 then
    (space beside sp) above (horizontalBar beside corner)
    else
    (verticalBar above corner) beside (space above sp)
    def main(args: Array[String]) =
    val nSides = args(0).toInt println(spiral(nSides, 0))
    end Spiral
    Поскольку
    Spiral является самостоятельным объектом с методом main
    , имеющим надлежащую сигнатуру, этот код можно считать приложением,

    228 Глава 10 • Композиция и наследование написанным на Scala.
    Spiral получает один аргумент командной строки в виде целого числа и рисует спираль с указанным количеством граней.
    Например, можно нарисовать шестигранную спираль, как показано слева, и более крупную спираль, как показано справа.
    $ scala Spiral 6 $ scala Spiral 11 $ scala Spiral 17
    +----– +---------– +----------------
    | | |
    | +-+ | +------+ | +------------+
    | + | | | | | | |
    | | | | +--+ | | | +--------+ |
    +---+ | | | | | | | | | |
    | | ++ | | | | | +----+ | |
    | | | | | | | | | | |
    | +----+ | | | | | ++ | | |
    | | | | | | | | | |
    +--------+ | | | +--+ | | |
    | | | | | |
    | | +------+ | |
    | | | |
    | +----------+ |
    | |
    +--------------+
    Резюме
    В этой главе мы рассмотрели дополнительные концепции объектно­ориен­
    тированного программирования на языке Scala. Среди них — абстрактные классы, наследование и создание подтипов, иерархии классов, параметри­
    ческие поля и переопределение методов. У вас должно было выработаться понимание способов создания в Scala оригинальных иерархий классов.
    А к работе с библиотекой раскладки мы еще вернемся в главе 25.

    11
    Трейты
    Трейты в Scala являются фундаментальными повторно используемыми блоками кода. В трейте инкапсулируются определения тех методов и полей, которые затем могут повторно использоваться путем их примешивания в классы. В отличие от наследования классов, в котором каждый класс дол­
    жен быть наследником только одного суперкласса, в класс может примеши­
    ваться любое количество трейтов. В этой главе мы покажем, как работают трейты. Далее рассмотрим два наиболее распространенных способа их при­
    менения: расширение «тонких» интерфейсов и превращение их в «толстые», а также определение наращиваемых модификаций. Здесь мы покажем, как используется трейт
    Ordered
    , и сравним механизм трейтов с множественным наследованием, имеющимся в других языках.
    11 .1 . Как работают трейты
    Определение трейта похоже на определение класса, за исключением того, что в нем используется ключевое слово trait
    . Пример показан в листинге 11.1.
    Листинг 11.1. Определение трейта Philosophical trait Philosophical:
    def philosophize = "На меня тратится память, следовательно, я существую!"
    Данный трейт называется
    Philosophical
    . В нем не объявлен суперкласс, следовательно, как и у класса, у него есть суперкласс по умолчанию —
    AnyRef
    . В нем определяется один конкретный метод по имени philosophize
    Это простой трейт, и его вполне достаточно, чтобы показать, как работают трейты.

    230 Глава 11 • Трейты
    После того как трейт определен, он может быть примешан в класс либо с помощью ключевого слова extends или with
    , либо с помощью запятой.
    Программисты, работающие со Scala, примешивают трейты, а не наследуют их, поскольку примешивание трейта весьма отличается от множественно­
    го наследования, встречающегося во многих других языках. Этот вопрос рассматривается в разделе 11.4. Например, в листинге 11.2 показан класс, в который с помощью ключевого слова extends примешивается трейт
    Philosophical
    Листинг 11.2. Примешивание трейта с использованием ключевого слова extends class Frog extends Philosophical:
    override def toString = "зеленая"
    Примешивать трейт можно с помощью ключевого слова extends
    , в таком слу­
    чае происходит неявное наследование суперкласса трейта. Например, в ли­
    стинге 11.2 класс
    Frog
    (лягушка) становится подклассом
    AnyRef
    (это супер­
    класс для трейта
    Philosophical
    ) и примешивает в себя трейт
    Philosophical
    Методы, унаследованные от трейта, могут использоваться точно так же, как и методы, унаследованные от суперкласса. Рассмотрим пример:
    val frog = new Frog frog.philosophize() // На меня тратится память, следовательно, я существую!
    Трейт также определяет тип. Рассмотрим пример, в котором
    Philosophical используется как тип:
    val phil: Philosophical = frog phil.philosophize // На меня тратится память, следовательно, я существую!
    Типом phil является
    Philosophical
    , то есть трейт. Таким образом, пере­
    менная phil может быть инициализирована любым объектом, в чей класс примешан трейт
    Philosophical
    Если нужно примешать трейт в класс, который явно расширяет супер­
    класс, то ключевое слово extends используется для указания суперкласса, а для примешивания трейта — запятая (или ключевое слово with
    ). Пример показан в листинге 11.3. Если нужно примешать сразу несколько трейтов, то дополнительные трейты указываются с помощью ключевого слова with
    Например, располагая трейтом
    HasLegs
    , вы, как показано в листинге 11.4, можете примешать в класс
    Frog как трейт
    Philosophical
    , так и трейт
    HasLegs

    11 .1 . Как работают трейты 231
    Листинг 11.3. Примешивание трейта с использованием запятой class Animal class Frog extends Animal with Philosophical {
    override def toString = "зеленая"
    }
    Листинг 11.4. Примешивание нескольких трейтов class Animal trait HasLegs class Frog extends Animal, Philosophical, HasLegs:
    override def toString = "зеленая"
    В показанных ранее примерах класс
    Frog наследовал реализацию метода philosophize из трейта
    Philosophical
    . В качестве альтернативного вариан­
    та метод philosophize в классе
    Frog может быть переопределен. Синтаксис выглядит точно так же, как и при переопределении метода, объявленного в суперклассе. Рассмотрим пример:
    class Animal class Frog extends Animal, Philosophical:
    override def toString = "зеленая"
    override def philosophize = s"Мне живется нелегко, потому что я $this!"
    В новое определение класса
    Frog по­прежнему примешивается трейт
    Phi- losophical
    , поэтому его, как и раньше, можно использовать из перемен­
    ной данного типа. Но так как во
    Frog переопределено определение метода philosophize
    , которое было дано в трейте
    Philosophical
    , при вызове будет получено новое поведение:
    val phrog: Philosophical = new Frog phrog.philosophize // Мне живется нелегко, потому что я зеленая!
    Теперь можно прийти к философскому умозаключению, что трейты подобны
    Java­интерфейсам со стандартными методами, но фактически их возмож­
    ности гораздо шире. Так, в трейтах можно объявлять поля и сохранять со­
    стояние. Фактически в определении трейта можно делать то же самое, что и в определении класса, и синтаксис выглядит почти так же.
    Ключевое отличие классов от трейтов заключается в том, что в классах вызовы super имеют статическую привязку, а в трейтах — динамическую.
    Если в классе воспользоваться кодом super.toString
    , то вы будете точно знать, какая именно реализация метода будет вызвана. Но когда точно такой

    232 Глава 11 • Трейты же код применяется в трейте, то вызываемая с помощью super реализация метода при определении трейта еще не определена. Вызываемая реализация станет определяться заново при каждом примешивании трейта в конкретный класс. Такое своеобразное поведение super является ключевым фактором, позволяющим трейтам работать в качестве наращиваемых модификаций, и рассматривается в разделе 11.3. А правила разрешения вызовов super будут изложены в разделе 11.4.
    11 .2 . Сравнение «тонких» и «толстых» интерфейсов
    Чаще всего трейты используются для автоматического добавления к классу методов в дополнение к тем методам, которые в нем уже имеются. То есть трейты способны расширить «тонкий» интерфейс, превратив его в «тол-
    стый».
    Противопоставление «тонких» интерфейсов «толстым» представляет собой компромисс, который довольно часто встречается в объектно­ориентирован­
    ном проектировании. Это компромисс между теми, кто реализует интерфейс, и теми, кто им пользуется. В «толстых» интерфейсах имеется множество методов, обеспечивающих удобство применения для тех, кто их вызывает.
    Клиенты могут выбрать метод, целиком отвечающий их функциональным запросам. В то же время «тонкий» интерфейс имеет незначительное количе­
    ство методов и поэтому проще обходится тем, кто их реализует. Но клиентам, обращающимся к «тонким» интерфейсам, приходится создавать больше собственного кода. При более скудном выборе доступных для вызова мето­
    дов им приходится выбирать то, что хотя бы в какой­то мере отвечает их по­
    требностям, а чтобы использовать выбранный метод, им требуется создавать дополнительный код.
    Добавление в трейт конкретного метода уводит компромисс «тонкий — тол­
    стый» в сторону более «толстых» интерфейсов — это одноразовое действие.
    Вам нужно единожды реализовать конкретный метод, сделав это в самом трейте, вместо того чтобы возиться с его повторной реализацией для каждого класса, в который примешивается трейт. Таким образом, создание «толстых» интерфейсов в Scala требует меньше работы, чем в языках без трейтов.
    Чтобы расширить интерфейс с помощью трейтов, просто определите трейт с небольшим количеством абстрактных методов — «тонкую» часть интерфей­
    са трейта — и с потенциально большим количеством конкретных методов, реализованных в терминах абстрактных методов. Затем можно будет при­

    11 .2 . Сравнение «тонких» и «толстых» интерфейсов
    1   ...   21   22   23   24   25   26   27   28   ...   64


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