Composition over inheritance (and Kotlin) — небольшая заметка, хорошо иллюстрирующая принцип композиции объектов и его преимущества перед наследованием.
Взгляни на следующий код:
1 2 3 4 5 6 7 8 9 10 11 |
open class Parent { fun parentFunctionality() {} } open class Child(): Parent() { fun childFunctionality() {} } class Grandchild constructor() : Child() { fun grandchildFunctionality() {} } |
Это канонический пример наследования в объектно ориентированном программировании. Объект класса Grandchild сможет вызывать методы parentFunctionality() и childFunctionality(). Код красив и замечателен. Но представь себе, что будет, если сильно усложнить этот пример, добавив в каждый класс множество новых открытых методов и связав их между собой. В какой-то момент может оказаться, что ты переопределяешь метод, который используется другим методом, и таким образом ломаешь функциональность всего объекта.
Разумеется, грамотный дизайн поможет избежать этой проблемы, но что, если команда разработчиков состоит не только из тебя одного и в коде есть множество классов с незнакомым тебе кодом?
РЕКОМЕНДУЕМ:
Хорошие и плохие приемы программирования на Kotlin
На самом деле в современном мире наследование уже не считается единственно верным способом проектирования приложения. Во многих случаях более предпочтительным будет принцип композиции, когда вместо класса-наследника создается новый класс, который не переопределяет методы предка, а вызывает их напрямую.
Предыдущий код, переписанный с использованием принципа композиции, будет выглядеть так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Parent { fun parentFunctionality() {} } class Child() { private val parent = Parent() fun parentFunctionality() { parent.parentFunctionality() } fun childFunctionality() {} } class Grandchild { private val parent = Parent() private val child = Child() fun parentFunctionality() { parent.parentFunctionality() } fun childFunctionality() { child.childFunctionality() } fun grandchildFunctionality() {} } |
Принцип композиции позволяет не только избежать трудно уловимых багов, но и упрощает тестирование (класс-предок легко заменить на фейковую реализацию) и сопровождение приложения (код становится более очевидным и понятным).
Kotlin содержит несколько инструментов, которые могут упростить композицию классов и даже принудить тебя использовать ее вместо наследования. Например, именно по причине возможных багов Kotlin делает классы не наследуемыми по умолчанию. Также здесь есть поддержка синглтонов на уровне языка, так что многие классы можно быстро оформить в виде синглтонов и напрямую вызывать их методы без необходимости создавать класс и хранить на него ссылку.
РЕКОМЕНДУЕМ:
Полезные советы разработчику на языке Kotlin
Функция-делегат lazy также помогает создавать композиции, а точнее минимизировать возможный оверхед. В следующем коде объект parent будет создан только в момент первого обращения к нему, то есть не будет занимать дополнительную память, если вообще не используется:
1 2 3 4 5 6 7 8 9 |
open class Parent { fun parentFunctionality() {} } open class Child() { val parent by lazy { Parent() } ... fun childFunctionality() {} } |
Ну и последнее — функции-расширения, которые позволяют добавить новые методы к существующему классу без необходимости наследоваться от него:
1 2 3 4 5 6 7 |
class SystemClass { ... } fun SystemClass.newFunctionality() {} SystemClass().newFunctionality() |