Files
obsidian/Kotlin/ООП/Абстрактные классы.md
T

13 KiB
Raw Blame History

title, description, tags
title description tags
Абстрактные классы abstract против open, шаблонный метод и выбор между абстрактным классом и интерфейсом
kotlin
oop

Абстрактный класс нельзя создать - только унаследовать.

abstract class Shape4 {
    abstract val area: Double                 // без реализации
    abstract fun name(): String

    fun describe() = "${name()}: площадь %.2f".format(java.util.Locale.US, area)
}

class Circle3(val radius: Double) : Shape4() {
    override val area get() = Math.PI * radius * radius
    override fun name() = "круг"
}

class Square3(val side: Double) : Shape4() {
    override val area get() = side * side
    override fun name() = "квадрат"
}

fun main() {
    val shapes = listOf(Circle3(1.0), Square3(2.0))
    shapes.forEach { println(it.describe()) }
    // круг: площадь 3.14
    // квадрат: площадь 4.00
    // println(Shape4())    // ошибка: нельзя создать абстрактный класс
}

abstract и open

abstract подразумевает open - писать его не нужно:

abstract class Base32 {
    abstract fun required()           // open не нужен, реализации нет
    open fun optional() = "по умолчанию"    // есть реализация, но переопределяема
    fun fixed() = "не переопределить"        // final
}

class Impl6 : Base32() {
    override fun required() = Unit
    override fun optional() = "своё"
}

fun main() {
    val i = Impl6()
    println("${i.optional()} / ${i.fixed()}")   // своё / не переопределить
}
Модификатор Реализация Нужно переопределять
abstract нет обязательно
open есть можно
нет модификатора есть нельзя

Абстрактный класс может не иметь ни одного абстрактного члена - тогда он просто запрещает создание экземпляра:

abstract class Utils {
    fun helper() = "общая логика"
}

class Concrete : Utils()

fun main() { println(Concrete().helper()) }    // общая логика

Абстрактный класс может иметь состояние

Это главное отличие от интерфейса:

abstract class Repository2(protected val tableName: String) {
    private val cache = mutableMapOf<Int, String>()      // состояние

    protected abstract fun loadFromDb(id: Int): String?

    fun find(id: Int): String? = cache.getOrPut(id) {
        loadFromDb(id) ?: return null
    }

    fun cacheSize() = cache.size
}

class UserRepository2 : Repository2("users") {
    override fun loadFromDb(id: Int) = if (id == 1) "Иван" else null
}

fun main() {
    val repo = UserRepository2()
    println(repo.find(1))         // Иван
    println(repo.find(1))         // Иван — из кеша
    println(repo.cacheSize())     // 1
    println(repo.find(99))        // null
}

Конструктор, приватные поля, инициализация - всё доступно. У интерфейса этого нет.

Шаблонный метод

Самое частое применение: алгоритм зафиксирован в базовом классе, шаги переопределяются.

abstract class Importer {
    fun import(data: String): String {
        val parsed = parse(data)
        val valid = parsed.filter { validate(it) }
        return "загружено ${valid.size} из ${parsed.size}: $valid"
    }

    protected abstract fun parse(data: String): List<String>
    protected open fun validate(item: String) = item.isNotBlank()
}

class CsvImporter : Importer() {
    override fun parse(data: String) = data.split(",").map { it.trim() }
}

class StrictCsvImporter : Importer() {
    override fun parse(data: String) = data.split(",").map { it.trim() }
    override fun validate(item: String) = item.length >= 3
}

fun main() {
    println(CsvImporter().import("a, b, , c"))
    // загружено 3 из 4: [a, b, c]
    println(StrictCsvImporter().import("abc, b, defg"))
    // загружено 2 из 3: [abc, defg]
}

import не open - порядок шагов менять нельзя. parse обязателен, validate имеет разумное поведение по умолчанию.

protected для членов, нужных подклассам

abstract class Connection {
    protected var isOpen = false
        private set

    protected abstract fun doConnect(): Boolean

    fun connect(): String {
        isOpen = doConnect()
        return if (isOpen) "подключено" else "ошибка"
    }
}

class TcpConnection(private val host: String) : Connection() {
    override fun doConnect() = host.isNotBlank()
    fun status() = if (isOpen) "$host: активно" else "$host: закрыто"
}

fun main() {
    val c = TcpConnection("example.com")
    println(c.connect())     // подключено
    println(c.status())      // example.com: активно
}

Абстрактный класс или интерфейс

interface Drawable { fun draw(): String }            // контракт

abstract class Widget(val id: String) : Drawable {   // + состояние и общая логика
    protected var visible = true
    fun hide() { visible = false }
    override fun draw() = if (visible) render() else ""
    protected abstract fun render(): String
}

class Button(id: String, val label: String) : Widget(id) {
    override fun render() = "[$label]"
}

fun main() {
    val b = Button("b1", "ОК")
    println(b.draw())      // [ОК]
    b.hide()
    println("'${b.draw()}'")   // ''
}
Интерфейс Абстрактный класс
сколько можно реализовать много один
состояние (свойство с полем) нет да
конструктор нет да
init нет да
private-члены да (с реализацией) да
protected-члены нет да
видимость членов только public или private любая

Правило выбора:

  • интерфейс - когда описываете возможность («умеет рисоваться», «сравним»), особенно если её могут захотеть добавить к существующим классам
  • абстрактный класс - когда у наследников общее состояние или нужен шаблонный метод с protected-шагами

На практике часто сочетают: публичный интерфейс для клиентов и абстрактный класс как база для реализаций.

Подробнее: Интерфейсы

Абстрактный класс и sealed

sealed class Result4 {
    abstract val isSuccess: Boolean
}

class Ok2(val data: String) : Result4() {
    override val isSuccess = true
}

class Fail(val error: String) : Result4() {
    override val isSuccess = false
}

fun handle(r: Result4) = when (r) {       // else не нужен
    is Ok2 -> "данные: ${r.data}"
    is Fail -> "ошибка: ${r.error}"
}

fun main() {
    println(handle(Ok2("готово")))      // данные: готово
    println(Fail("сбой").isSuccess)     // false
}

sealed класс абстрактен по определению. Разница: abstract допускает наследников где угодно, sealed - только в своём модуле и пакете, зато даёт исчерпывающий when. Подробнее: Sealed class и interface

[!info] Отличия от Java

Java Kotlin
abstract class A то же
abstract void f(); abstract fun f()
поля protected свойства protected
абстрактное поле невозможно abstract val
default-методы в интерфейсе (Java 8+) реализация в интерфейсе без ключевого слова

abstract val - то, чего в Java нет. Java позволяет объявить абстрактным только метод, поэтому «абстрактное свойство» выражается парой геттер/сеттер:

abstract class Shape {
    abstract double getArea();     // абстрактный геттер
}

В Kotlin abstract val area: Double оставляет наследнику выбор: хранимое свойство, геттер или параметр конструктора. Клиент разницы не видит.

abstract не требует open, потому что отсутствие реализации уже означает необходимость переопределения. В Java final abstract - ошибка компиляции по той же причине.

Доля абстрактных классов в Kotlin ниже. Часть их задач закрывают другие средства: sealed для закрытых иерархий, делегирование для переиспользования кода, функции-расширения для добавления поведения без наследования.

Сравнить: Абстрактные классы в Java

[!warning] Подводные камни Вызов абстрактного метода из init - гарантированная ловушка:

abstract class Base33 {
    init { println("из init: ${describe()}") }
    abstract fun describe(): String
}

class Derived32 : Base33() {
    private val name: String = "Иван"
    override fun describe() = "name=$name"
}

fun main() { Derived32() }      // из init: name=null

Подкласс ещё не инициализирован, поэтому non-null свойство оказывается null. Подробнее: Инициализация

Абстрактный класс расходует единственный слот наследования. Класс, унаследованный от абстрактного, больше ни от чего наследоваться не сможет. Поэтому публичный API лучше начинать с интерфейса.

protected abstract - часть контракта с наследниками, и менять его так же больно, как публичный API: изменение сигнатуры сломает все подклассы.

Абстрактный класс с конструктором усложняет тестирование. Чтобы проверить логику шаблонного метода, нужен наследник; с интерфейсом достаточно лямбды или объекта-выражения.

abstract и private несовместимы - приватный член невозможно переопределить:

abstract class Base34 {
    // private abstract fun f()     // ошибка
    protected abstract fun f(): String
    fun run3() = f()
}

class Impl7 : Base34() { override fun f() = "готово" }

fun main() { println(Impl7().run3()) }     // готово

Абстрактное свойство легко переопределить несогласованно:

abstract class Base35 { abstract val id: Int }

class Unstable : Base35() {
    override val id: Int get() = (0..100).random()    // разное при каждом чтении
}

fun main() {
    val u = Unstable()
    println(u.id == u.id)      // чаще false
}

Контракт «свойство стабильно» компилятором не проверяется; при использовании в equals или как ключа это даст тяжёлые для отладки ошибки.

Глубокие абстрактные иерархии трудны для чтения. Если шаблонный метод вызывает абстрактные шаги, которые вызывают другие шаблонные методы, поведение собирается из четырёх файлов.

Дополнительно: Интерфейсы Далее: Интерфейсы