Шаг 168.
Основы Kotlin.
Наследование. Создание подкласса (окончание)

    На этом шаге мы рассмотрим особенности построения иерархии подклассов.

    Вы также можете определить подкласс подкласса, создав более глубокую иерархию. Если создать класс 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 побуждает согласиться с наследованием. Это уменьшает шансы повлиять на работу классов, не предназначенных для создания подклассов, и не дает вам или кому-то другому переопределить функции там, где это не предлагалось.

    На следующем шаге мы рассмотрим проверку типов.




Предыдущий шаг Содержание Следующий шаг