9. Основы Kotlin. Классы и интерфейсы

Функции-создатели

Функции-создатели в некотором смысле являются заменой конструкторам. Они предназначены для создания объекта, реализующего тот или иной интерфейс; при этом, естественно, для их написания должна существовать реализация данного интерфейса и функция-создатель обычно вызывает внутри себя тот или иной конструктор, иногда делая выбор из нескольких вариантов. Известными примерами функций-создателей являются listOf(…​)mutableListOf(…​)setOf(…​)mutableSetOf(…​). Для матрицы, подобная функция может быть определена так:

fun <E> createMatrix(height: Int, width: Int, e: E): Matrix<E> = TODO()

Здесь fun <E> говорит о том, что функция использует настраиваемый тип E — тип элементов матрицы. Первый и второй параметр задают высоту и ширину матрицы, а третий — значение элемента, который при создании матрицы будет записан во все ячейки. Результатом функции должна стать вновь созданная матрица, но, поскольку реализации интерфейса Matrix<E> ещё нет, вместо вызова конструктора в теле фигурирует TODO() — специальная функция, бросающая исключение UnsupportedOperationException.

Имея функцию-создатель, мы можем описывать более сложные операции с матрицами, которые не только изменяют существующие матрицы, но и создают новые — по-прежнему не зная ничего о реализации. Например, транспонирование меняет местами ряды и колонки в матрице:

fun <E> transpose(matrix: Matrix<E>): Matrix<E> {
    if (matrix.width < 1 || matrix.height < 1) return matrix
    val result = createMatrix(height = matrix.width, width = matrix.height, e = matrix[0, 0])
    for (i in 0 until matrix.width) {
        for (j in 0 until matrix.height) {
            result[i, j] = matrix[j, i]
        }
    }
    return result
}

Так мы создаём новую матрицу, меняя местами ширину и высоту старой, а затем в цикле переписываем элементы из старой матрицы в новую — с учётом того, что ряды стали колонками и наоборот.

При попытке протестировать эту функцию мы получим исключение UnsupportedOperationException при создании матрицы — до тех пор, пока не сделаем её реализацию и не используем её в функции-создателе.

Скелет реализации интерфейса

Для того, чтобы создать реализацию интерфейса — то есть класс, который умеет делать все описанные в интерфейсе операции — необходимо для начала написать примерно следующий “скелет”.

class MatrixImpl<E> : Matrix<E> {
    override val height: Int = TODO()
    override val width: Int = TODO()

    override fun get(row: Int, column: Int): E  = TODO()
    override fun get(cell: Cell): E  = TODO()

    override fun set(row: Int, column: Int, value: E) {
        TODO()
    }
    override fun set(cell: Cell, value: E) {
        TODO()
    }

    override fun equals(other: Any?) = TODO()
    override fun toString(): String = TODO()
}

Заголовок class MatrixImpl<E> : Matrix<E> говорит о том, что мы определяем класс MatrixImpl<E>, который является реализацией интерфейса Matrix<E> и использует настраиваемый тип E. Далее перечисляются все свойства и функции, имеющиеся в Matrix<E>; перед каждым из них добавляется модификатор override — он сигнализирует об определении свойства / функции, уже имеющихся в интерфейсе. Класс, в отличие от интерфейса, должен содержать реальные тела функций и реальные значения свойств — но в скелете они заменяются на TODO(). В конце класса перечисляются две упоминавшиеся ранее функции equals и toString — первая для сравнения (матриц) на равенство, а вторая для представления матрицы в виде строки.

Здесь въедливый читатель, заметив перед equals и toString модификатор override, может задаться вопросом — а две этих функции тоже определены в каком-нибудь интерфейсе? Это предположение не вполне верно. Определения двух этих функций имеются в специальном классе Any, определяющем тип “любой”. Напомним, что в Котлине любой тип является разновидностью типа Any?, то есть множество значений Any? — вообще все значения, которые могут существовать в программе на Котлине. Any без вопроса имеет то же множество значений, за вычетом специального null. Это, в частности, значит, что сравнение на равенство и представление в виде строки в Котлине можно выполнить для чего угодно.

Варианты реализации интерфейса

Теперь поговорим о том, как можно скелет реализации заменить на настоящую реализацию. Почти всегда, когда речь идёт о более-менее сложных понятиях, это можно сделать несколькими способами, какой из них лучше подходит для конкретной задачи — решает программист.

Начать нужно всегда с ответа на вопрос — какие данные описывают интересующий нас объект (матрицу) и как их можно представить на данном языке программирования? Для матрицы первая часть ответа такова — высота и ширина матрицы (целые числа) и набор элементов матрицы (типа E). Поскольку имеющиеся у матрицы функции не предполагают изменения её высоты и ширины, их лучше всего объявить как свойства в конструкторе матрицы:

class MatrixImpl<E>(override val height: Int, override val width: Int
                    //, something other?
                    ) : Matrix<E> {
    // Attention: no more height / width here

    override fun get(row: Int, column: Int): E  = TODO()
    // Other functions...
}

Обратите внимание, что определения свойств высоты и ширины исчезли из тела класса и переехали в конструктор — при этом сохранив необходимый модификатор override.

Что касается набора элементов, то здесь актуальна вторая часть вопроса — как представить этот набор? Для этого нужен некоторый контейнер, ссылка на который хранилась бы в ещё одном свойстве матрицы. Лучше, чтобы это свойство было закрытым, чтобы возможные действия с матрицей ограничивались лишь свойствами и функциями из интерфейса Matrix<E>. Существует несколько вариантов такого контейнера. Рассмотрим некоторые из них.

Додати коментар