Урок 18: ООП. Полиморфизм в Kotlin, 3 типа (Ad hoc, Subtyping, Parametric)

3 типа Ad hoc Subtyping Parametric 1 - Android [Kotlin] для начинающих

Суть полиморфизма

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

Название “Полиморф” говорит о том, что это что-то многообразное, а именно имеет множество форм. “Поли” — много, “морф” — форма. Полиморфизм часто путают с наследованием. Из примеров позже станет понятно почему. Напомню, что наследование, это про то, когда экземпляр класса наследника включает в себя поля и функциональность родителя. В полиморфизме больший акцент делается на типах, чем на классах. Полиморфизм – это когда один интерфейс используется для разных типов. Помните как выглядит функция, принимающая некий параметр? В такую функцию мы обязаны поставлять только определенный тип. Полиморфизм – это про то, что мы можем поставлять в одну условную функцию параметры разных типов.

3 типа полиморфизма в Kotlin

  • Ad hoc (по случаю) – одна функция определяется для различных типов данных. В классе прописывается несколько функций, которые принимают разные параметры. При вызове этой функции, компилятор определяет какая функция сработает по количеству и типам передаваемых параметров. Минус такого подхода, в том, что нужно наделать множество реализаций этой функции.
  • Subtyping (полиморфизм включения) – это реализация через принцип подстановки Барбары Лисков. Это один из принципов объектно-ориентированного программирования из аббревиатуры SOLID. Почитайте об этом. А звучит он так: функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом. То есть объект более узкого типа всегда может использоваться там, где может использоваться объект более широкого типа. Здесь, кстати и может сбить с толку факт отношений наследования родителя и потомка, о чем я упоминал вначале.
  • Parametric (параметрический) – программа может быть реализована через обобщенные типы. То есть без ориентации на конкретный тип. Касаемо Kotlin – это история про дженерики (или обобщенное программирование). О них поговорим в другой раз.

Полиморфизм по случаю

Перейдем к примерам и воспроизведем сначала полиморфизм по случаю. Нам нужен класс и метод внутри него, который будет принимать параметры определенного типа. Представим, что мы разрабатываем гипотетический модуль приложения для заметок, в котором есть базовый класс заметки – NotesAppItem. Внутри этого класса будет метод, который добавляет какую-то информацию в виде элемента заметки в ячейку календаря.

За гипотетическое добавление в ячейку будет отвечать метод addItemToCell(). Поля будут такими:

  • title – строка, заголовок ячейки,
  • creationDate – дата создания ячейки, тип Date() из стандартной библиотеки. Провалившись в этот тип, увидим, что объект вернет текущее время вашей системы.
  • type – тип сообщения. Представим, что при создании заметки, программа будет валидировать контент и присваивать соответствующий тип. Например, текстовая заметка, список дел или некий номер телефона. Сейчас это реализовывать не будем, поэтому это будет просто строка с типом.
  • data – данные. То, что будет содержаться в теле заметки. В данном случае это будет какой-то текст сообщения.

Хорошо. В теле метода печатаем информационное сообщение. Элемент “такой-то” добавлен в ячейку. Дата добавления, тип и содержимое. Для красивого вывода будем разделять данные символами переноса строки.

class NotesAppItem {

   fun addItemToCell(
			title: String, 
			creationDate: Date, 
			type: String, 
			data: String,
		) {
				println("Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n")

   }
}
Реализация функций с разными параметрами

Далее создадим экземпляр в рабочем файле и вызовем метод добавления, якобы наполнив его данными. Сначала создаем экземпляр текущего времени и даты. Текущее время запишется в переменную в момент создания, поэтому можно будет обойтись потом без переменной и проставлять Date() прямо в вызове метода добавления ячейки. Далее создаем объект и вызываем у него метод. Это будет заметка-напоминание позвонить сестре, чтобы ее поздравить. В поле тип пропишем строку message.

fun main() {

	val creationDate = Date()
	val notes = NotesAppItem()

	notes.addItemToCell(
		"call sister",
		creationDate,
		"message",
		"call sister to congratulate",
	)
}

Хорошо. В гипотетическом техническом задании сказано, что приложение может сохранять заметки с разным типом контента. А именно это будет возможность сохранять, помимо сообщения, номер телефона и какой нибудь список (типа TODO-list). И согласно описания Ad hoc полиморфизма, нужно создать новые функции, которые будут принимать соответствующие параметры. Для этого в классе нужно создать дополнительные функции с точно таким же названием, но с другими параметрами. Создадим одну функцию, которая будет принимать, например, только номера телефонов. С данными типа Long. И еще одну, чтобы принимать список типа String.

class NotesItem {

   fun addItemToCell(
			title: String, 
			creationDate: Date, 
			type: String, 
			data: String,
	 ) {
			  println("Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n")
   }

   fun addItemToCell(
			title: String, 
			creationDate: Date, 
			type: String, 
			data: Int,
	 ) {
			  println("Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n")
   }

   fun addItemToCell(
			title: String, 
			creationDate: Date, 
			type: String, 
			data: List<String>,
	 ) {
			  println("Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n")
   }

}

Отлично. Вызываем новые функции с сответствующими разнообразными данными. Создадим заметку с номером телефона сестры. Тип будет phone и сам телефон в качестве данных. Также сделаем вызов функции создания заметки со списком дел. Тип прописываем list. Список пусть будет таким: помыть собаку, сделать уборку, купить новую обувь.

Запускаем и видим в консоли как отрабатывают методы addItemToCell(). Что тут происходит еще раз. Мы написали несколько функций для разных кейсов. В рабочем файле вызываем метод и по типам параметров и их количеству срабатывает конкретная реализация. Так работает тип полиморфизма “по случаю”.

Полиморфизм включения

Второй тип полиморфизма реализуется с помощью наследования. Если что, эту тему мы разбирали в 14 уроке. Но не смотря на то, что инструментарий очень похож, есть фундаментальные отличия. Еще раз. Полиморфизм – это способность объекта вести себя множеством способов. То есть с разными реализациями. Наследование – это создание нового класса с использованием свойств и методов родителя.

Воспроизведем этот пример, немного изменив логику, чтобы сделать интересные наблюдения.

Реализация с помощью наследования

Сначала под каждый тип заметки создадим класс и сделаем их наследниками от NotesAppItem. Не забываем сделать класс родитель open. Функцию addItemToCell() изменим и ее мы будем переопределять. Теперь пусть функция будет без принимающих параметров, а возвращать будет пустую строку. И так как логика уже изменена, корректнее теперь назвать ее как-нибудь со словом get – например, getItemData().

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

Строку оставляем такую же специфичную для каждого типа. Итого будет 3 наследника с переопределенными функциями с индивидуальной реализацией для каждого объекта. Если вдруг еще непонятно, реализация разная, потому, что параметр data везде разный. Мы по прежнему можем сделать и более сложную кастомизацию. Изменить количество параметров для какого-то типа или прописать какие-то вычисления в теле переопределенных функций.

open class NotesAppItem {

   open fun addItemToCell() = ""

}

class MessageItem(
   private val title: String,
   private val creationDate: Date,
   private val type: String,
   private val data: String,
) : NotesAppItem() {
   override fun getItemData(): String {
      return "Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n"
   }
}

class PhoneItem(
   private val title: String,
   private val creationDate: Date,
   private val type: String,
   private val data: Long,
) : NotesAppItem() {
   override fun getItemData(): String {
      return "Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n"
   }
}

class ListItem(
   private val title: String,
   private val creationDate: Date,
   private val type: String,
   private val data: List<String>,
) : NotesAppItem() {
   override fun getItemData(): String {
      return "Item \"$title\" added to cell – $creationDate\nType: $type\nData: $data\n"
   }
}

Окей. Классы готовы, давайте создавать их экземпляры с какими-то данными и вызывать методы получения информации о заметках. Тип будем проставлять принудительно для последующей демонстрации. Данные возьмем из предыдущего примера. И еще нужно будет сделать форматирование кода для красоты. Хорошо, вызываем методы у созданных объектов и запускаем программу.

val creationDate = Date()
val notes = NotesItem()
val messageItem = MessageItem(
   "call sister",
   creationDate,
   "message",
   "call sister to congratulate",
)
val phoneItem = PhoneItem(
   "sister's number",
   creationDate,
   "phone",
   89914424242,
)
val toDoListItem = ListItem(
   "todolist",
   creationDate,
   "list",
	listOf("wash dog", "do the cleaning", "buy new shoes"),
)

println(messageItem.getItemData())
println(phoneItem.getItemData())
println(toDoListItem.getItemData())

Вывод в консоль не отличается от предыдущей реализации и мы все сделали правильно. Но это промежуточная точка и пока еще не полиморфизм. Обратите внимание, метод getItemData() вызван три раза. По сути этот код будет работать даже если функции будут иметь разное название и, более того, даже без наследования. Но наследование в полиморфизме начнет играть роль, когда появляется общий код для этих типов объектов.

Роль наследования в полиморфизме

И эти возможности я сейчас продемонстрирую. Во-первых меняем все типы дочерних экземпляров на тип базового класса. И все будет работать также в обоих случаях.

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

Далее. Обладая этой информацией имеем возможность сделать такие манипуляции. Создаем массив с типом NotesAppItem и наполняем его созданными выше объектами. Ниже сделаем функцию showAllNotes(). Внутри которой через forEach вызываем метод getItemData() у всех элементов массива. В реальности эта функция может передавать объекты далее для сохранения в базу или их дополнительной обработки. Запускаем и все отрабатывает корректно. Таким образом объекты разных типов, но с общим родителем демонстрируют свое полиморфное свойство.


   val arrayOfNotes = arrayOf<NotesAppItem>(messageItem, phoneItem, listItem)

}

fun showAllNotes(notes: Array<NotesAppItem>) {
   notes.forEach {
	 println(it.getItemData())
	}
}

С помощью полиморфизма легко можно поддерживать и расширять ваши сущности, внедряя в действующую логику без последствий. Если мы создадим объект нового типа, его легко будет внедрить в текущую реализацию. То есть, как на текущем примере, мы объединили их в список для дальнейшего с ним взаимодействия.

Это все о чем я хотел рассказать в рамках изучения полиморфизма.

Для тех, кто собрался стать Android-разработчиком

Пошаговаясхема
Пошаговая
схема

Описание процесса обучения от основ Kotlin до Android-разработчика

Бесплатныеуроки
Бесплатные
уроки

Авторский бесплатный курс по основам языка программирования Kotlin

Обучающийбот
Обучающий
бот

Тренажер и самоучитель по Котлин – бесплатные тесты и практика

Поделиться уроком

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *