Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 791
Скачиваний: 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
Листинг 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.