Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
Листинг 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 требует меньше работы, чем в языках без трейтов. Чтобы расширить интерфейс с помощью трейтов, просто определите трейт с небольшим количеством абстрактных методов — «тонкую» часть интерфей са трейта — и с потенциально большим количеством конкретных методов, реализованных в терминах абстрактных методов. Затем можно будет при |