Эта статья расскажет о рисках, связанных с наследованием классов. Здесь будет показана альтернатива наследованию классов – композиция. После прочтения вы поймете, почему 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».
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/713608/
Добавить комментарий