Как стать автором
Обновить
834.7
OTUS
Цифровые навыки от ведущих экспертов

Риски, связанные с наследованием

Время на прочтение 4 мин
Количество просмотров 2.1K
Автор оригинала: Allan Caine

Эта статья расскажет о рисках, связанных с наследованием классов. Здесь будет показана альтернатива наследованию классов – композиция. После прочтения вы поймете, почему Kotlin по умолчанию делает все классы конечными. Статья объяснит, почему не следует делать класс Kotlin open (открытый), если на то нет веских причин.

Предположим, что у нас есть следующий интерфейс:

interface Insertable<T> {

    fun insert(item: T)

    fun insertAll(vararg items: T)

    val items: List<T>
}

Интерфейс Insertable (вставляемый)

Также, BaseInsert является имплементацией интерфейса Insertable<Number>. BaseInsert open (открыт). Поэтому мы можем его расширять.

Пусть CountingInsert будет расширением BaseInsert. Каждый раз, когда код вставляет Number, он должен увеличивать переменную count на единицу. Итак, получаем:

class CountingInsert : BaseInsert() {

    var count: Int = 0
        private set

    override fun insert(item: Number) {
        super.insert(item)
        count++
    }

    override fun insertAll(vararg items: Number) {
        super.insertAll(*items)
        count += items.size
    }
}

Алгоритм подсчета, реализованный через наследование

Данная имплементация должна работать. Строка 8 увеличивает count на единицу; строка 13 - на количество аргументов переменной.

Код работает не так, как ожидалось. См. строку 7 ниже.

fun main(args: Array<String>) {

    CountingInsert().apply {
        insert(1)
        insert(2)
        insertAll(3, 4)
        println(count) // prints 6, the incorrect value; should be 4
        println(items) // prints [1 2 3 4]
    }
}

CountingInsert() выдает неверный результат

Ошибка в строке 10 ниже:

open class BaseInsert : Insertable<Number> {
    
    private val numberList = mutableListOf<Number>()

    override fun insert(item: Number) {
        numberList.add(item)
    }

    override fun insertAll(vararg items: Number) {
        items.forEach { number -> insert(number) }
    }

    override val items: List<Number>
        get() = numberList
}

Имплементация BaseInsert

BaseInsert.insertAll является функцией удобства. Функция insertAll вызывает insert для каждого элемента в списке vararg. Класс CountingInsert повторно подсчитал вставку чисел 3 и 4. CountingInsert.insertAll дважды выполнил оператор count++; один раз - оператор count += items.size. Функция CountingInsert.insertAll увеличила count на четыре вместо двух.

Существуют ли альтернативы? Да. Мы можем изменить код или использовать композицию.

Изменение кода кажется очевидным решением. Предположим, нам разрешено изменить базовый класс. Можно изменить реализацию BaseInsert.insertAll на:

override fun insertAll(vararg items: Number) {
    numberList += items.toList()
}

Обновленная имплементация BaseInsert.insertAll

Такая имплементация позволяет избежать вызова BaseInsert.insert(), источника наших проблем.

Предположим, что у нас нет доступа к классу BaseInsert. Тогда можно удалить переопределение insertAll():

class CountingInsert : BaseInsert() {

    var count: Int = 0
        private set

    override fun insert(item: Number) {
        super.insert(item)
        count++
    }
}

Класс CountingInsert без переопределения insertAll

Решение проблемы путем изменения кода достаточно уязвимо. Класс CountingInsert зависит от тонкостей имплементации BaseInsert. Есть ли более эффективный способ? Да, давайте воспользуемся композицией.

Здесь показана имплементация с помощью композиции:

class CompositionInsert(private val insertable: Insertable<Number> = BaseInsert())
    : Insertable<Number> by insertable {

    var count: Int = 0
        private set

    override fun insert(item: Number) {
        insertable.insert(item)
        count++
    }

    override fun insertAll(vararg items: Number) {
        insertable.insertAll(*items)
        count += items.size
    }
}

Имплементация с помощью композиции

Предположим, что класс BaseInsert использует имплементацию с рис. 4. После тестирования класса InsertDelegation результат будет правильным. См. строку 15:

fun main(args: Array<String>) {

    CountingInsert().apply {
        insert(1)
        insert(2)
        insertAll(3, 4)
        println(count) // prints 6, the incorrect value; should be 4
        println(items) // prints [1 2 3 4]
    }

    CompositionInsert().apply {
        insert(1)
        insert(2)
        insertAll(3, 4)
        println(count) // prints 4, which is correct
        println(items) // prints [1 2 3 4]
    }
}

Результаты тестирования для CompositionInsert

Сравнивая фрагменты кода 2 и 7, можно сказать, что имплементации insert и insertAll похожи. См ниже:

// By inheritance
override fun insert(item: Number) {
    super.insert(item)
    count++
}
override fun insertAll(vararg items: Number) {
    super.insertAll(*items)
    count += items.size
}
// By delegation
override fun insert(item: Number) {
    insertable.insert(item)
    count++
}
override fun insertAll(vararg items: Number) {
    insertable.insertAll(*items)
    count += items.size
}

Сравнение наследования и композиции

Сравниваемые методы одинаковы за одним исключением. В наследовании используется super, а в композиции – insertable. Сравните строки 3 и 12; а также 7 и 16 на рис. 8. Шаблон делегирования передает выполнение задачи по вставке на insertable. Класс CompositionInsert увеличивает переменную count. Наследование, напротив, нарушает инкапсуляцию класса BaseInsert.

Какова первопричина проблемы? Предположим, что BaseInsert не open (открыт). См. строку 1 во фрагменте кода 4. Если бы BaseInsert был final (конечный), то компилятор Kotlin отметил бы код на фрагменте 2 и 5 как ошибку. Только решение на фрагменте 7 оказалось бы работоспособным. Когда мы делаем класс BaseInsert final, инкапсуляция BaseInsert не нарушается.

Kotlin понимает риски, связанные с наследованием. Kotlin запрещает наследование, если только разработчик не пометит класс как open. Вывод: в целом, классы Kotlin должны быть final, если только нет веской причины сделать класс open.


Приглашаем на открытое занятие «Основы бизнес-логики и разработка библиотеки для шаблона CoR». На этом открытом уроке:
– поговорим про общие принципы построения бизнес-логики приложения,
– рассмотрим фреймворки для разработки бизнес-логики,
– узнаем про такие шаблоны проектирования, как фасад и цепочка обязанностей,
– разработаем библиотеку для шаблона "Цепочка обязанностей" с использованием DSL и coroutines.

Регистрация на урок открыта на странице онлайн-курса "Kotlin Backend Developer. Professional".

Теги:
Хабы:
+2
Комментарии 7
Комментарии Комментарии 7

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS