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

  • Листинг 10.6.

  • Листинг 10.8.

  • 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
    страница24 из 64
    1   ...   20   21   22   23   24   25   26   27   ...   64
    213
    Можно также наметить иные способы выражения элемента. Например, клиенту может понадобиться создать элемент разметки, содержащий один ряд, задаваемый строкой. Объектно­ориентированное программирование упрощает расширение системы новыми вариантами данных. Можно просто добавить подклассы. Например, в листинге 10.6 показан класс
    LineElement
    , расширяющий класс
    VectorElement
    Листинг 10.6. Вызов конструктора суперкласса
    // Расширенный элемент VectorElement, показанный в листинге 10.5.
    class LineElement(s: String) extends VectorElement(Vector(s)):
    override def width = s.length override def height = 1
    Поскольку
    LineElement расширяет
    VectorElement
    , а конструктор
    Vec torEle- ment получает параметр (
    Vector[String]
    ), то
    LineElement нужно передать аргумент первичному конструктору своего суперкласса. В целях вызова конструктора суперкласса аргумент или аргументы, которые нужно пере­
    дать, просто помещаются в круглые скобки, стоящие за именем суперклас­
    са. Например, класс
    LineElement передает аргумент
    Vector(s)
    первичному конструктору класса
    VectorElement
    , поместив его в круглые скобки и указав после имени суперкласса
    VectorElement
    :
    ... extends VectorElement(Vector(s)) ...
    С появлением нового подкласса иерархия наследования для элементов раз­
    метки приобретает вид, показанный на рис. 10.2.
    Рис. 10.2. Схема классов для LineElement
    10 .8 . Используем модификатор override
    Обратите внимание: определения width и height в
    LineElement имеют мо­
    дификатор override
    . В разделе 6.3 он встречался в определении метода

    214 Глава 10 • Композиция и наследование toString
    . В Scala такой модификатор требуется для всех элементов, пере­
    определяющих конкретный член родительского класса. Если элемент явля­
    ется реализацией абстрактного элемента с тем же именем, то модификатор указывать не обязательно. Применять модификатор запрещено, если член не переопределяется или не является реализацией какого­либо другого члена базового класса. Поскольку height и width в классе
    LineElement переопре­
    деляют конкретные определения в классе
    Element
    , то модификатор override указывать обязательно.
    Соблюдение этого правила дает полезную информацию для компилятора, которая помогает избежать некоторых трудно отлавливаемых ошибок и сде­
    лать развитие системы более безопасным. Например, если вы опечатались в названии метода или случайно указали для него не тот список параметров, то компилятор тут же отреагирует, выдав сообщение об ошибке:
    $ scalac LineElement.scala
    -- [E037] Declaration Error: LineElement.scala:3:15 --
    3 | override def hight = 1
    | ˆ
    | method hight overrides nothing
    Соглашение о применении модификатора override приобретает еще большее значение, когда дело доходит до развития системы. Скажем, вы определи­
    ли библиотеку методов рисования двумерных фигур, сделали ее общедо­
    ступной, и она стала использоваться довольно широко. Посмотрев на эту библио теку позже, вы захотели добавить к основному классу
    Shape новый метод с такой сигнатурой:
    def hidden(): Boolean
    Новый метод будут использовать различные средства рисования, чтобы определить необходимости отрисовки той или иной фигуры. Тем самым можно существенно ускорить работу, но сделать это, не рискуя вывести из строя клиентский код, невозможно. Кроме всего прочего, у клиента может быть определен подкласс класса
    Shape с другой реализацией hidden
    . Возмож­
    но, клиентский метод фактически заставит объект­получатель просто исчез­
    нуть вместо того, чтобы проверять, не является ли он невидимым. Две версии hidden переопределяют друг друга, поэтому ваши методы рисования в итоге заставят объекты исчезать, что совершенно не соответствует задуманному!
    Эти «случайные переопределения» — наиболее распространенное проявле­
    ние проблемы так называемого хрупкого базового класса. Проблема в том, что, если вы добавите новые члены в базовые классы (которые мы обычно называем суперклассами), вы рискуете вывести из строя клиентский код.

    10 .9 . Полиморфизм и динамическое связывание 215
    Полностью разрешить проблему хрупкого базового класса в Scala невоз­
    можно, но по сравнению с Java ситуация несколько лучше
    1
    . Если библиотека рисования и ее клиентский код созданы в Scala, то у клиентской исходной реализации hidden не мог использоваться модификатор override
    , поскольку на момент его применения не могло быть другого метода с таким же именем.
    Когда вы добавите метод hidden во вторую версию вашего класса фигур, при повторной компиляции кода клиента будет выдана следующая ошибка:
    -- Error: Circle.scala:3:6 ---------------------------
    3 | def hidden(): Boolean =
    | ˆ
    | error overriding method hidden in class Shape
    | of type (): Boolean; method hidden of type
    | (): Boolean needs `override` modifier
    То есть вместо неверного поведения ваш клиент получит ошибку в ходе компиляции, и такой исход обычно более предпочтителен.
    10 .9 . Полиморфизм и динамическое связывание
    В разделе 10.4 было показано, что переменная типа
    Element может ссылаться на объект типа
    VectorElement
    . Этот феномен называется полиморфизмом, что означает «множество форм». В данном случае объекты типа
    Element могут иметь множество форм
    2
    Ранее вам уже попадались две такие формы:
    VectorElement и
    LineElement
    Можно создать еще больше форм
    Element
    , определив новые подклассы клас­
    са
    Element
    . Например, можно определить новую форму
    Element с заданными шириной и высотой (
    width и height
    ) и полностью заполненную заданным символом:
    // Расширенный элемент, показанный в листинге 10.2
    class UniformElement(
    ch: Char,
    1
    В Java есть аннотация
    @Override
    , работающая аналогично имеющемуся в Scala модификатору override
    , но в отличие от Scala­модификатора override применять ее не обязательно.
    2
    Этот вид полиморфизма называется полиморфизмом подтипов. Другие виды полиморфизма в Scala обсуждаются в последующих главах, универсальный по­
    лиморфизм — в главе 18, а специальный полиморфизм — в главах 21 и 23.

    216 Глава 10 • Композиция и наследование override val width: Int,
    override val height: Int
    ) extends Element:
    private val line = ch.toString * width def contents = Vector.fill(height)(line)
    Иерархия наследования для класса
    Element теперь приобретает вид, пока­
    занный на рис. 10.3. В результате Scala примет все следующие присваивания, поскольку тип выражения присваивания соответствует типу определяемой переменной:
    val e1: Element = VectorElement(Vector("hello", "world"))
    val ve: VectorElement = LineElement("hello")
    val e2: Element = ve val e3: Element = UniformElement('x', 2, 3)
    Рис. 10.3. Иерархия классов элементов разметки
    Если изучить иерархию наследования, то окажется, что в каждом их этих четырех val
    ­определений тип выражения справа от знака равенства при­
    надлежит типу val
    ­переменной, инициализируемой слева от знака равен­
    ства.
    Есть еще одна сторона вопроса: вызовы методов с переменными и выраже­
    ниями динамически связаны. Это значит, текущая реализация вызываемого метода определяется во время выполнения программы на основе класса объекта, а не типа переменной или выражения. Чтобы продемонстрировать данное поведение, мы временно уберем все существующие члены из наших классов
    Element и добавим к
    Element метод по имени demo
    . Затем переопре­
    делим demo в
    VectorElement и
    LineElement
    , но не в
    UniformElement
    :
    abstract class Element:
    def demo = "Element's implementation invoked"
    class VectorElement extends Element:
    override def demo = "VectorElement's implementation invoked"

    10 .10 . Объявляем финальные элементы 217
    class LineElement extends VectorElement:
    override def demo = "LineElement's implementation invoked"
    // UniformElement наследует demo из Element class UniformElement extends Element
    Если ввести данный код в REPL, то можно будет определить метод, который получает объект типа
    Element и вызывает в отношении него метод demo
    :
    def invokeDemo(e: Element) = e.demo
    Если передать методу invokeDemo объект типа
    VectorElement
    , то будет по­
    казано сообщение, свидетельствующее о вызове реализации demo из класса
    VectorElement
    , даже если типом переменной e
    , в отношении которой был вызван метод demo
    , являлся
    Element
    :
    invokeDemo(new VectorElement)
    // Вызвана реализация, определенная в VectorElement
    Аналогично, если передать invokeDemo объект типа
    LineElement
    , будет пока­
    зано сообщение, свидетельствующее о вызове той реализации demo
    , которая была определена в классе
    LineElement
    :
    invokeDemo(new LineElement)
    // Вызвана реализация, определенная в LineElement
    Поведение при передаче объекта типа
    UniformElement может на первый взгляд показаться неожиданным, но оно вполне корректно:
    invokeDemo(new UniformElement)
    // Вызвана реализация, определенная в Element
    В классе
    UniformElement метод demo не переопределяется, поэтому в нем наследуется реализация demo из его суперкласса
    Element
    . Таким образом, реализация, определенная в классе
    Element
    , — это правильная реализация вызова demo
    , когда классом объекта является
    UniformElement
    10 .10 . Объявляем финальные элементы
    Иногда при проектировании иерархии наследования нужно обеспечить не­
    возможность переопределения элемента подклассом. В Scala, как и в Java, это делается путем добавления к элементу модификатора final
    . Как показано в листинге 10.7, модификатор final можно указать для метода demo класса
    VectorElement

    218 Глава 10 • Композиция и наследование
    Листинг 10.7. Объявление финального метода class VectorElement extends Element:
    final override def demo =
    "VectorElement's implementation invoked"
    При наличии данной версии в классе
    VectorElement попытка переопределить demo в его подклассе
    LineElement не пройдет компиляцию:
    -- Error: LineElement.scala:2:15 ----------------------
    2 | override def demo =
    | ˆ
    |error overriding method demo in class VectorElement
    | of type => String; method demo of type => String
    | cannot override final member method demo in class
    | VectorElement
    Порой вы должны быть уверены, что создать подкласс для класса в целом невозможно. Для этого нужно просто сделать весь класс финальным, добавив к объявлению класса модификатор final
    . Например, в листинге 10.8 показа­
    но, как должен быть объявлен финальный класс
    VectorElement
    Листинг 10.8. Объявление финального класса final class VectorElement extends Element:
    override def demo = "VectorElement's implementation invoked"
    При наличии данной версии класса
    VectorElement любая попытка опреде­
    лить подкласс не пройдет компиляцию:
    -- [E093] Syntax Error: LineElement.scala:1:6 ---------
    1 |class LineElement extends VectorElement:
    | ˆ
    | class LineElement cannot extend final class
    | VectorElement
    Теперь мы удалим модификаторы final и методы demo и вернемся к прежней реализации семейства классов
    Element
    . Далее в главе мы сконцентрируемся на завершении создания работоспособной версии библиотеки разметки.
    10 .11 . Используем композицию и наследование
    Композиция и наследование — два способа определить новый класс в по­
    нятиях другого, уже существующего класса. Если вы ориентируетесь преимущественно на повторное использование кода, то, как правило,

    10 .11 . Используем композицию и наследование 219
    предпочтение нужно отдавать композиции, а не наследованию. Только ему свойственна проблема хрупкого базового класса, вследствие которой можно ненароком сделать неработоспособными подклассы, внося изме­
    нения в суперкласс.
    Насчет отношения наследования нужно задаться лишь одним вопросом: не моделируется ли им взаимоотношение типа is-a
    (является) [Mey91].
    Например, нетрудно будет заметить, что класс
    VectorElement
    является раз­
    новидностью
    Element
    . Можно задаться еще одним вопросом: придется ли клиентам использовать тип подкласса в качестве типа суперкласса [Eck98].
    Применительно к классу
    VectorElement не вызывает никаких сомнений, что клиентам потребуется задействовать объекты типа
    VectorElement в качестве объектов типа
    Element
    Если задаться этими вопросами относительно отношений наследования, показанных на рис. 10.3, то не покажутся ли вам какие­либо из них подо­
    зрительными? В частности, насколько для вас очевидно, что
    LineElement
    является (
    is-a
    )
    VectorElement
    ? Как вы думаете, понадобится ли когда­
    нибудь клиентам воспользоваться типом
    LineElement в качестве типа
    VectorElement
    ?
    Фактически класс
    LineElement был определен как подкласс класса
    Vec- torEle ment преимущественно для повторного использования имеющегося в
    VectorElement определения contents
    . Поэтому, возможно, будет лучше определить
    LineElement в качестве прямого подкласса класса
    Element
    :
    class LineElement(s: String) extends Element:
    val contents = Vector(s)
    override def width = s.length override def height = 1
    В предыдущей версии
    LineElement состоял в отношении наследования с
    Vec- torElement
    , откуда наследовал метод contents
    . Теперь же у него отношение композиции с классом
    Vector
    : в нем содержится ссылка на строковый массив из его собственного поля contents
    1
    . При наличии этой реализации класса
    LineElement иерархия наследования для
    Element приобретет вид, показанный на рис. 10.4.
    1
    Класс
    VectorElement также имеет отношение композиции с классом
    Vector
    , по­
    скольку его параметрическое поле contents содержит ссылку на строковый мас­
    сив. Код для
    VectorElement показан в листинге 10.5. Его отношение композиции представлено в схеме классов в виде ромба, к примеру на рис. 10.1.

    220 Глава 10 • Композиция и наследование
    Рис. 10.4. Иерархия класса с пересмотренным определением подкласса LineElement
    10 .12 . Реализуем методы above, beside и toString
    В качестве следующего шага в классе
    Element будет реализован метод above
    Поместить один элемент выше другого с помощью метода above означает объединить два значения содержимого элементов, представленного contents
    Поэтому первый, черновой вариант метода above может иметь следующий вид:
    def above(that: Element): Element =
    VectorElement(this.contents ++ that.contents)
    Операция ++ объединяет два вектора. Некоторые другие векторные методы будут объяснены в этой главе, а более подробное обсуждение будет дано в главе 15.
    Показанный ранее код нельзя считать достаточным, поскольку он не позволяет помещать друг на друга элементы разной ширины. Но, чтобы в этом разделе ничего не усложнять, оставим все как есть и станем передавать в метод above только элементы одинаковой длины. В разделе 10.14 мы усовершенствуем его, чтобы клиенты могли с его помощью объединять элементы разной ширины.
    Следующим будет реализован метод beside
    . Чтобы поставить элементы рядом друг с другом, создадим новый элемент, в котором каждый ряд будет получен путем объединения соответствующих рядов двух элементов. Как и прежде, во избежание усложнений начнем с предположения, что высота двух элементов одинакова. Тогда структура метода beside приобретет такой вид:
    def beside(that: Element): Element =
    val newContents = new Array[String](this.contents.length)
    for i <- 0 until this.contents.length do newContents(i) = this.contents(i) + that.contents(i)
    VectorElement(newContents.toVector)

    10 .12 . Реализуем методы above, beside и toString 221
    Метод beside сначала выделяет массив newContents и заполняет его объеди­
    нением соответствующих векторных элементов this.contents и that.con- tents
    . В итоге получается новый
    VectorElement
    , имеющий новое содержимое toVector
    Хотя эта реализация beside вполне работоспособна, в ней используется им­
    перативный стиль программирования, о чем явно свидетельствует наличие цикла, обходящего векторы по индексам. В альтернативном варианте метод можно сократить до одного выражения:
    VectorElement(
    for (line1, line2) <- this.contents.zip(that.contents)
    yield line1 + line2)
    Здесь с помощью оператора zip векторы this.contents и that.contents преобразуются в вектор пар (так назваемый
    Tuple2
    ). Оператор zip выбирает соответствующие элементы двух своих операндов и формирует вектор пар.
    Например, выражение
    Vector(1, 2, 3).zip(Vector("a", "b"))
    будет вычисляться как
    Vector((1, "a"), (2, "b"))
    Если один из двух векторов­операндов длиннее другого, то оставшиеся элементы оператор zip просто отбрасывает. В показанном ранее выражении третий элемент левого операнда,
    3
    , не формирует пару результата, поскольку для него не находится соответствующий элемент в правом операнде.
    Затем вектор, подвергшийся zip
    , итерируется с помощью выражения for
    Здесь синтаксис for
    ((line1,
    line2)
    <–
    ...)
    позволяет указать имена обоих элементов пары в одном паттерне (то есть теперь line1
    обозначает первый элемент пары, а line2
    — второй). Имеющаяся в Scala система сопоставления с образцом (паттерном) будет подробнее рассмотрена в главе 13. А сейчас все это можно представить себе как способ определения для каждого шага итерации двух val
    ­переменных: line1
    и line2
    У выражения for есть часть yield
    , и поэтому оно выдает (yields) результат. Дан­
    ный результат того же вида, что и перебираемый выражением объект (то есть данный вектор). Каждый элемент — результат объединения соответствующих рядов: line1
    и line2
    . Следовательно, конечный результат выполнения этого кода получается таким же, как и результат выполнения первой версии beside
    , но поскольку в нем удалось обойтись без явной индексации векторов, результат достигается способом, при котором допускается меньше ошибок.

    222 Глава 10 • Композиция и наследование
    Но вам все еще нужен способ отображения элементов. Как обычно, ото­
    бражение выполняется с помощью определения метода toString
    , возвра­
    щающего элемент, отформатированный в виде строки. Его определение выглядит так:
    override def toString = contents.mkString("\n")
    В реализации toString используется метод mkString
    , который определен для всех последовательностей, включая векторы. В разделе 7.8 было показано выражение vec.mkString sep
    , которое возвращает строку, состоящую из всех векторов vec
    . Каждый элемент отображается на строку путем вызова его метода toString
    . Между последовательными элементами строк вставляется разделитель sep
    . Следовательно, выражение contents.mkString
    ("\n")
    фор­
    матирует содержимое векторов как строку, где каждый вектор появляется в собственном ряду.
    1   ...   20   21   22   23   24   25   26   27   ...   64


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