На этом шаге мы рассмотрим назначение и задание абстрактного класса.
Абстрактные классы обеспечивают еще один способ организации классов. Абстрактные классы не имеют экземпляров. Их цель - предложить реализации функций через наследование подклассам, которые могут иметь экземпляры.
Абстрактный класс объявляется с помощью ключевого слова abstract. Кроме реализованных функций, абстрактные классы включают абстрактные функции - объявления функций без реализации.
Настало время дать игроку что-то, с чем он будет сражаться в NyetHack. Добавьте абстрактный класс с именем Monster в Creature.kt. Класс Monster реализует интерфейс Fightable, а это значит, что он должен иметь свойство healthPoints и функцию attack().
package com.bignerdranch.nyethack interface Fightable { var healthPoints: Int val diceCount: Int val diceSides: Int val damageRoll: Int get() = (0 until diceCount).map { Random().nextInt(diceSides + 1) }.sum() fun attack(opponent: Fightable): Int } abstract class Monster(val name: String, val description: String, override var healthPoints: Int) : Fightable { override fun attack(opponent: Fightable): Int { val damageDealt = damageRoll opponent.healthPoints -= damageDealt return damageDealt } }
Рис.1. Объявление абстрактного класса (Creature.kt)
Вы объявили Monster как абстрактный класс, поскольку он будет основой для создания конкретных монстров в игре. Вы никогда не создадите экземпляр Monster, да и не сможете. Вместо этого вы создадите экземпляры подклассов Monster: конкретных существ, таких как гоблины, призраки, драконы, то есть определенные версии абстрактного понятия "монстр".
Объявив Monster как абстрактный класс, мы получили шаблон для всех монстров в NyetHack: монстр должен иметь имя и описание, а также удовлетворять критериям интерфейса Fightable.
Теперь создайте конкретную версию абстрактного класса Monster - подкласс Goblin - в Creature.kt.
package com.bignerdranch.nyethack interface Fightable { var healthPoints: Int val diceCount: Int val diceSides: Int val damageRoll: Int get() = (0 until diceCount).map { Random().nextInt(diceSides + 1) }.sum() fun attack(opponent: Fightable): Int } abstract class Monster(val name: String, val description: String, override var healthPoints: Int) : Fightable { override fun attack(opponent: Fightable): Int { val damageDealt = damageRoll opponent.healthPoints -= damageDealt return damageDealt } } class Goblin(name: String = "Goblin", description: String = "A nasty-looking goblin", healthPoints: Int = 30) : Monster(name, description, healthPoints) { }
Рис.2. Создание подклассов из абстрактного класса (Creature.kt)
Так как Goblin - это подкласс Monster, то он обладает всеми свойствами и функциями, которые есть у Monster.
Если вы попытаетесь скомпилировать код сейчас, то компиляция не выполнится. Это происходит из-за того, что свойства diceCount и diceSides объявлены в интерфейсе Fightable, но не реализованы в Monster (и не имеют реализации по умолчанию).
КлассMonster не обязан отвечать всем требованиям интерфейса Fightable, несмотря на то что реализует его, потому что это абстрактный класс и он никогда не будет иметь экземпляров. Но его подклассы должны реализовать все требования Fightable либо через наследование от Monster, либо сами по себе.
Удовлетворите требования Fightable, добавив недостающие свойства в Goblin.
package com.bignerdranch.nyethack interface Fightable { var healthPoints: Int val diceCount: Int val diceSides: Int val damageRoll: Int get() = (0 until diceCount).map { Random().nextInt(diceSides + 1) }.sum() fun attack(opponent: Fightable): Int } abstract class Monster(val name: String, val description: String, override var healthPoints: Int) : Fightable { override fun attack(opponent: Fightable): Int { val damageDealt = damageRoll opponent.healthPoints -= damageDealt return damageDealt } } class Goblin(name: String = "Goblin", description: String = "A nasty-looking goblin", healthPoints: Int = 30) : Monster(name, description, healthPoints) { override val diceCount = 2 override val diceSides = 8 }
Рис.3. Реализация свойств в подклассе абстрактного класса (Creature.kt)
По умолчанию подкласс разделяет всю функциональность со своим суперклассом. Это верно для любого типа суперкласса. Если класс реализует интерфейс, его подкласс также должен удовлетворять требованиям интерфейса.
Вы могли заметить сходство между абстрактными классами и интерфейсами: оба могут объявлять функции и свойства без реализации. В чем же тогда разница?
Во-первых, интерфейс не может определить конструктор. Во-вторых, класс может расширять (наследовать) только один абстрактный подкласс, но реализовать множество интерфейсов. Хорошее правило: если нужна категория поведения или свойств, общая для объектов, но неудобная для наследования, используйте интерфейс. С другой стороны, если наследование имеет смысл, но вам не нужен конкретный класс-предок, используйте абстрактный класс.
На следующем шаге мы рассмотрим сражение в NyetHack.