На этом шаге мы рассмотрим особенности построения иерархии подклассов.
Вы также можете определить подкласс подкласса, создав более глубокую иерархию. Если создать класс Plazza, унаследовав TownSquare, то Plazza будет типом TownSquare и типом Room. Глубина наследования ограничивается только здравым смыслом для организации кода. (И конечно же, вашим воображением.)
Вызов разных версий load(), в зависимости от класса объекта, - это пример идеи объектно-ориентированного программирования, названной полиморфизмом.
Полиморфизм - это подход к упрощению структуры программы. Он позволяет повторно использовать функции, описывающие общие черты поведения группы классов (например, что происходит, когда игрок заходит в комнату), а также изменять поведение под уникальные потребности класса (например, ликующая толпа в TownSquare). Определяя класс TownSquare как подкласс Room, вы объявили новую реализацию load(), которая переопределила версию в Room. Теперь при вызове метода load() объекта currentRoom будет вызываться версия load() для TownSquare. Соответственно, никаких изменений в Game.kt вносить не требуется.
Рассмотрим следующий заголовок функции:
fun drawBlueprint(room: Room)
Функция drawBlueprint() принимает Room в качестве параметра. Она также может принять любой подкласс Room, потому что любой подкласс будет обладать всеми характеристиками Room. Полиморфизм позволяет писать функции, которым важны только возможности класса, а не их реализации.
Открывать функции для переопределения может быть полезно, но тут есть и побочный эффект. Когда вы переопределяете функцию в Kotlin, переопределяющая функция в подклассе по умолчанию открыта для переопределения (если сам подкласс отмечен ключевым словом open).
Что делать, если это нежелательно? Давайте рассмотрим пример с TownSquare. Допустим, вы хотите, чтобы любой подкласс TownSquare мог менять свое описание description(), но не способ загрузки load().
Добавьте ключевое слово final, чтобы запретить возможность переопределения функции. Откройте TownSquare и добавьте ключевое слово final в определение функции load(), чтобы никто не мог переопределить ликование жителей, когда герой приходит на городскую площадь.
open class Room(val name: String) { protected open val dangerLevel = 5 fun description() = "Room: $name\n" + "Danger level: $dangerLevel" open fun load() = "Nothing much to see here..." } class TownSquare : Room("Town Square") { override val dangerLevel = super.dangerLevel - 3 private var bellSound = "GWONG" final override fun load() = "The villagers rally and cheer as you enter!\n${ringBell()}" private fun ringBell() = "The bell tower announces your arrival. $bellSound" }
Рис.1. Объявление функции final (Room.kt)
Теперь любой подкласс TownSquare сможет переопределить функцию description(), но не load(), потому что перед ней стоит ключевое слово final.
Как вы уже успели заметить, когда в первый раз пытались переопределить load(), функции по умолчанию недоступны для переопределения, если не объявить их открытыми, добавив модификатор open. Добавление ключевого слова final в объявление переопределяющей функции гарантирует, что она не будет переопределена, даже если класс, в котором она объявлена, имеет модификатор open.
Итак, вы ознакомились с тем, как использовать наследование, чтобы обеспечить совместное использование данных и поведения родственными классами. Также вы увидели, как использовать ключевые слова open, final и override для управления возможностью переопределения. Требуя явного использования ключевых слов open и override, Kotlin побуждает согласиться с наследованием. Это уменьшает шансы повлиять на работу классов, не предназначенных для создания подклассов, и не дает вам или кому-то другому переопределить функции там, где это не предлагалось.
На следующем шаге мы рассмотрим проверку типов.