Kotlin Bootcamp для программистов 4: Объектно-ориентированное программирование

Эта практическая работа является частью курса Kotlin Bootcamp for Programmers . Вы получите максимальную пользу от этого курса, если будете выполнять задания последовательно. В зависимости от вашего уровня знаний, вы можете пропустить некоторые разделы. Этот курс ориентирован на программистов, владеющих объектно-ориентированным языком программирования и желающих изучить Kotlin .

Введение

В этой лабораторной работе вы создадите программу на Kotlin и узнаете о классах и объектах в Kotlin. Большая часть материала будет вам знакома, если вы знаете другой объектно-ориентированный язык программирования, но в Kotlin есть несколько важных отличий, которые позволяют сократить объём кода. Вы также узнаете об абстрактных классах и делегировании интерфейсов.

Вместо создания одного примера приложения уроки этого курса направлены на углубление ваших знаний, но при этом они частично независимы друг от друга, чтобы вы могли бегло просмотреть знакомые разделы. Чтобы связать их воедино, многие примеры используют тему аквариума. А если вы хотите узнать всё об аквариуме, ознакомьтесь с курсом Kotlin Bootcamp for Programmers на Udacity.

Что вам уже следует знать

  • Основы Kotlin, включая типы, операторы и циклы
  • Синтаксис функций Kotlin
  • Основы объектно-ориентированного программирования
  • Основы IDE, такие как IntelliJ IDEA или Android Studio

Чему вы научитесь

  • Как создавать классы и получать доступ к свойствам в Kotlin
  • Как создавать и использовать конструкторы классов в Kotlin
  • Как создать подкласс и как работает наследование
  • Об абстрактных классах, интерфейсах и делегировании интерфейсов
  • Как создавать и использовать классы данных
  • Как использовать синглтоны, перечисления и запечатанные классы

Что ты будешь делать?

  • Создать класс со свойствами
  • Создать конструктор для класса
  • Создать подкласс
  • Изучите примеры абстрактных классов и интерфейсов.
  • Создайте простой класс данных
  • Узнайте больше о синглтонах, перечислениях и запечатанных классах

Следующие термины программирования должны быть вам уже знакомы:

  • Классы — это чертежи объектов. Например, класс Aquarium — это чертеж для создания объекта «Аквариум».
  • Объекты являются экземплярами классов; объект аквариума — это один настоящий Aquarium .
  • Свойства — это характеристики классов, такие как длина, ширина и высота Aquarium .
  • Методы , также называемые функциями-членами , представляют собой функциональность класса. Методы — это то, что можно «делать» с объектом. Например, можно применить fillWithWater() к объекту Aquarium .
  • Интерфейс — это спецификация, которую может реализовать класс. Например, очистка свойственна не только аквариумам, но и другим объектам, и для разных объектов она обычно выполняется схожим образом. Таким образом, можно создать интерфейс Clean , определяющий метод clean() . Класс Aquarium может реализовать интерфейс Clean для очистки аквариума мягкой губкой.
  • Пакеты — это способ группировки связанного кода для его упорядочивания или создания библиотеки кода. После создания пакета вы можете импортировать его содержимое в другой файл и повторно использовать код и классы из него.

В этом задании вы создадите новый пакет и класс с некоторыми свойствами и методом.

Шаг 1: Создайте пакет

Пакеты помогут вам упорядочить свой код.

  1. На панели «Проект» в разделе «Проект Hello Kotlin» щелкните правой кнопкой мыши папку src .
  2. Выберите «Создать» > «Пакет» и назовите его example.myapp .

Шаг 2: Создайте класс со свойствами

Классы определяются с помощью ключевого слова class , а имена классов по соглашению начинаются с заглавной буквы.

  1. Щелкните правой кнопкой мыши по пакету example.myapp .
  2. Выберите Создать > Файл/Класс Kotlin .
  3. В разделе Kind выберите Class и назовите класс Aquarium . IntelliJ IDEA добавит имя пакета в файл и создаст для вас пустой класс Aquarium .
  4. В классе Aquarium определите и инициализируйте var свойства ширины, высоты и длины (в сантиметрах). Инициализируйте свойства значениями по умолчанию.
package example.myapp

class Aquarium {
    var width: Int = 20
    var height: Int = 40
    var length: Int = 100
}

Под капотом Kotlin автоматически создает геттеры и сеттеры для свойств, определенных вами в классе Aquarium , поэтому вы можете получить к свойствам прямой доступ, например, myAquarium.length .

Шаг 3: Создайте функцию main()

Создайте новый файл с именем main.kt для хранения функции main() .

  1. На панели «Проект» слева щелкните правой кнопкой мыши пакет example.myapp .
  2. Выберите Создать > Файл/Класс Kotlin .
  3. В раскрывающемся списке «Вид» выберите « Файл » и назовите файл main.kt IntelliJ IDEA включает имя пакета, но не включает определение класса для файла.
  4. Определите функцию buildAquarium() и внутри неё создайте экземпляр класса Aquarium . Чтобы создать экземпляр, обратитесь к классу, как к функции Aquarium() . Это вызовет конструктор класса и создаст экземпляр класса Aquarium , аналогично использованию new в других языках программирования.
  5. Определите функцию main() и вызовите buildAquarium() .
package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium()
}

fun main() {
    buildAquarium()
}

Шаг 4: Добавьте метод

  1. В классе Aquarium добавьте метод для печати свойств размеров аквариума.
    fun printSize() {
        println("Width: $width cm " +
                "Length: $length cm " +
                "Height: $height cm ")
    }
  1. В main.kt , в buildAquarium() , вызовите метод printSize() для myAquarium .
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. Запустите программу, щёлкнув по зелёному треугольнику рядом с функцией main() . Наблюдайте за результатом.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. В buildAquarium() добавьте код для установки высоты 60 и выведите измененные свойства размеров.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. Запустите программу и посмотрите на результат.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

В этом задании вы создадите конструктор для класса и продолжите работу со свойствами.

Шаг 1: Создание конструктора

На этом этапе вы добавляете конструктор к классу Aquarium , созданному в первом задании. В предыдущем примере каждый экземпляр Aquarium создаётся с одинаковыми размерами. Вы можете изменить размеры после создания, настроив свойства, но проще изначально задать правильный размер.

В некоторых языках программирования конструктор определяется путём создания метода внутри класса с тем же именем, что и у класса. В Kotlin конструктор определяется непосредственно в объявлении класса, указывая параметры в скобках, как если бы класс был методом. Как и функции в Kotlin, эти параметры могут включать значения по умолчанию.

  1. В созданном ранее классе Aquarium измените определение класса, включив в него три параметра конструктора со значениями по умолчанию для length , width и height , и назначьте их соответствующим свойствам.
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
   // Dimensions in cm
   var length: Int = length
   var width: Int = width
   var height: Int = height
...
}
  1. Более компактный подход в Kotlin — определение свойств непосредственно в конструкторе с помощью var или val , при этом Kotlin также автоматически создаёт геттеры и сеттеры. В этом случае определения свойств можно удалить из тела класса.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
  1. При создании объекта Aquarium с помощью этого конструктора вы можете не указывать аргументы и получить значения по умолчанию, либо указать только некоторые из них, либо указать все аргументы и создать Aquarium совершенно произвольного размера. В функции buildAquarium() попробуйте различные способы создания объекта Aquarium с использованием именованных параметров.
fun buildAquarium() {
    val aquarium1 = Aquarium()
    aquarium1.printSize()
    // default height and length
    val aquarium2 = Aquarium(width = 25)
    aquarium2.printSize()
    // default width
    val aquarium3 = Aquarium(height = 35, length = 110)
    aquarium3.printSize()
    // everything custom
    val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
    aquarium4.printSize()
}
  1. Запустите программу и посмотрите на результат.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 25 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 110 cm Height: 35 cm 
Width: 25 cm Length: 110 cm Height: 35 cm 

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

Шаг 2: Добавьте блоки инициализации

В примерах конструкторов, представленных выше, просто объявляются свойства и присваиваются им значения выражений. Если вашему конструктору требуется больше кода инициализации, его можно разместить в одном или нескольких блоках init . На этом этапе вы добавляете несколько блоков init в класс Aquarium .

  1. В классе Aquarium добавьте блок init для вывода информации об инициализации объекта и второй блок для вывода объема в литрах.
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
    init {
        println("aquarium initializing")
    }
    init {
        // 1 liter = 1000 cm^3
        println("Volume: ${width * length * height / 1000} l")
    }
}
  1. Запустите программу и посмотрите на результат.
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm 
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm 

Обратите внимание, что блоки init выполняются в том порядке, в котором они указаны в определении класса, и все они выполняются при вызове конструктора.

Шаг 3: Узнайте о вторичных конструкторах

На этом этапе вы узнаете о вторичных конструкторах и добавите один в свой класс. Помимо основного конструктора, который может содержать один или несколько блоков init , класс Kotlin также может иметь один или несколько вторичных конструкторов, что позволяет перегружать конструкторы, то есть конструкторы с разными аргументами.

  1. В классе Aquarium добавьте вторичный конструктор, принимающий в качестве аргумента количество рыб, используя ключевое слово constructor . Создайте свойство val tank для расчётного объёма аквариума в литрах, исходя из количества рыб. Предположим, что на каждую рыбу приходится 2 литра (2000 см³) воды, плюс небольшое дополнительное пространство, чтобы вода не проливалась.
constructor(numberOfFish: Int) : this() {
    // 2,000 cm^3 per fish + extra room so water doesn't spill
    val tank = numberOfFish * 2000 * 1.1
}
  1. Внутри вторичного конструктора сохраните длину и ширину (которые были заданы в первичном конструкторе) такими же и рассчитайте высоту, необходимую для того, чтобы резервуар получил заданный объем.
    // calculate the height needed
    height = (tank / (length * width)).toInt()
  1. В функцию buildAquarium() добавьте вызов для создания Aquarium с помощью нового вторичного конструктора. Выведите размер и объём.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
  1. Запустите программу и посмотрите на результат.
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

Обратите внимание, что объем выводится дважды: один раз блоком init в первичном конструкторе перед выполнением вторичного конструктора и один раз кодом в buildAquarium() .

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

Шаг 4: Добавьте новый получатель свойств

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

  1. В классе Aquarium определите свойство Int с именем volume и определите метод get() , который вычисляет объем в следующей строке.
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. Удалите блок init , который печатает громкость.
  2. Удалите код в buildAquarium() , который выводит объем.
  3. В методе printSize() добавьте строку для печати объема.
fun printSize() {
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm "
    )
    // 1 l = 1000 cm^3
    println("Volume: $volume l")
}
  1. Запустите программу и посмотрите на результат.
⇒ aquarium initializing
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

Размеры и объем те же, что и раньше, но объем выводится только один раз после полной инициализации объекта как первичным, так и вторичным конструктором.

Шаг 5: Добавьте установщик свойств

На этом этапе вы создаете новый установщик свойств для тома.

  1. В классе Aquarium измените volume на var , чтобы ее можно было задать более одного раза.
  2. Добавьте сеттер для свойства volume , добавив метод set() под геттером, который пересчитывает высоту на основе указанного количества воды. По соглашению, имя сеттер-параметра — value , но вы можете изменить его при желании.
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. В buildAquarium() добавьте код для установки объёма аквариума 70 литров. Выведите новый размер.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. Запустите программу еще раз и посмотрите, как изменилась высота и объем.
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm 
Volume: 70 l

До сих пор в коде не было модификаторов видимости, таких как public или private . Это связано с тем, что по умолчанию всё в Kotlin является public, а это означает, что ко всему можно получить доступ отовсюду, включая классы, методы, свойства и переменные-члены.

В Kotlin классы, объекты, интерфейсы, конструкторы, функции, свойства и их сеттеры могут иметь модификаторы видимости :

  • public означает «видимо за пределами класса». По умолчанию всё открыто, включая переменные и методы класса.
  • internal означает, что он будет виден только внутри этого модуля. Модуль — это набор файлов Kotlin, скомпилированных вместе, например, библиотека или приложение.
  • private означает, что он будет виден только в этом классе (или исходном файле, если вы работаете с функциями).
  • protected — это то же самое, что и private , но он также будет виден всем подклассам.

Дополнительную информацию см. в разделе Модификаторы видимости в документации Kotlin.

Переменные-члены

Свойства внутри класса, или переменные-члены, по умолчанию являются public . Если они определены с помощью var , они становятся изменяемыми, то есть доступными для чтения и записи. Если они определены с помощью val , после инициализации они доступны только для чтения.

Если вам нужно свойство, которое ваш код может читать или записывать, а внешний код может только читать, вы можете оставить свойство и его геттер как публичные и объявить сеттер закрытым, как показано ниже.

var volume: Int
    get() = width * height * length / 1000
    private set(value) {
        height = (value * 1000) / (width * length)
    }

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

В Kotlin по умолчанию классы не могут быть подклассифицированы. Аналогично, свойства и переменные-члены не могут быть переопределены подклассами (хотя к ним можно получить доступ).

Чтобы разрешить создание подклассов, необходимо пометить класс как open . Аналогично, необходимо пометить свойства и переменные-члены как open , чтобы переопределить их в подклассе. Ключевое слово open необходимо для предотвращения случайной утечки деталей реализации в рамках интерфейса класса.

Шаг 1: Откройте класс «Аквариум».

На этом этапе вы делаете класс Aquarium open , чтобы иметь возможность переопределить его на следующем этапе.

  1. Пометьте класс Aquarium и все его свойства ключевым словом open .
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
    open var volume: Int
        get() = width * height * length / 1000
        set(value) {
            height = (value * 1000) / (width * length)
        }
  1. Добавьте свойство открытой shape со значением "rectangle" .
   open val shape = "rectangle"
  1. Добавьте свойство открытой water с геттером, который возвращает 90% объема Aquarium .
    open var water: Double = 0.0
        get() = volume * 0.9
  1. Добавьте код в метод printSize() , чтобы вывести форму и количество воды в процентах от объема.
fun printSize() {
    println(shape)
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm ")
    // 1 l = 1000 cm^3
    println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
  1. В buildAquarium() измените код, чтобы создать Aquarium с width = 25 , length = 25 и height = 40 .
fun buildAquarium() {
    val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
    aquarium6.printSize()
}
  1. Запустите программу и посмотрите на новый результат.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)

Шаг 2: Создайте подкласс

  1. Создайте подкласс Aquarium с именем TowerTank , который реализует резервуар с закруглённой цилиндрической формой вместо прямоугольного. Вы можете добавить TowerTank ниже Aquarium , поскольку вы можете добавить другой класс в том же файле, что и класс Aquarium .
  2. В TowerTank переопределите свойство height , определенное в конструкторе. Чтобы переопределить свойство, используйте ключевое слово override в подклассе.
  1. Сделайте так, чтобы конструктор TowerTank принимал значение типа diameter . Используйте значение diameter как для length , так и для width при вызове конструктора в суперклассе Aquarium .
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. Переопределите свойство объёма для вычисления цилиндра. Формула для цилиндра: пи, умноженное на квадрат радиуса, умноженный на высоту. Вам необходимо импортировать константу PI из java.lang.Math .
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }
  1. В TowerTank измените свойство water на 80% от объема.
override var water = volume * 0.8
  1. Измените shape на "cylinder" .
override val shape = "cylinder"
  1. Ваш окончательный класс TowerTank должен выглядеть примерно так, как показано в коде ниже.

Aquarium.kt :

package example.myapp

import java.lang.Math.PI

... // existing Aquarium class

class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }

    override var water = volume * 0.8
    override val shape = "cylinder"
}
  1. В buildAquarium() создайте TowerTank диаметром 25 см и высотой 45 см. Выведите размеры.

main.kt:

package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium(width = 25, length = 25, height = 40)
    myAquarium.printSize()
    val myTower = TowerTank(diameter = 25, height = 40)
    myTower.printSize()
}
  1. Запустите программу и посмотрите на результат.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)
aquarium initializing
cylinder
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 18 l Water: 14.4 l (80.0% full)

Иногда требуется определить общее поведение или свойства, общие для нескольких связанных классов. Kotlin предлагает два способа сделать это: интерфейсы и абстрактные классы. В этой задаче вы создадите абстрактный класс AquariumFish для свойств, общих для всех рыб. Вы создадите интерфейс FishAction для определения поведения, общего для всех рыб.

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

Шаг 1. Создание абстрактного класса

  1. В example.myapp создайте новый файл AquariumFish.kt .
  2. Создайте класс, также называемый AquariumFish , и обозначьте его как abstract .
  3. Добавьте одно String свойство, color и пометьте его как abstract .
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. Создайте два подкласса AquariumFish : Shark и Plecostomus .
  2. Поскольку color абстрактный, подклассы должны его реализовывать. Сделайте Shark серым, а Plecostomus золотым.
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. В файле main.kt создайте функцию makeFish() для тестирования классов. Создайте экземпляры Shark и Plecostomus , а затем выведите цвет каждого из них.
  2. Удалите предыдущий тестовый код в main() и добавьте вызов makeFish() . Ваш код должен выглядеть примерно так, как показано ниже.

main.kt :

package example.myapp

fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()

    println("Shark: ${shark.color}")
    println("Plecostomus: ${pleco.color}")
}

fun main () {
    makeFish()
}
  1. Запустите программу и посмотрите на результат.
⇒ Shark: gray 
Plecostomus: gold

На следующей диаграмме представлены классы Shark и Plecostomus , которые являются подклассами абстрактного класса AquariumFish .

A diagram showing the abstract class, AquariumFish, and two subclasses, Shark and Plecostumus.

Шаг 2. Создание интерфейса

  1. В AquariumFish.kt создайте интерфейс FishAction с методом eat() .
interface FishAction  {
    fun eat()
}
  1. Добавьте FishAction к каждому подклассу и реализуйте eat() , заставив его выводить на экран то, что делает рыба.
class Shark: AquariumFish(), FishAction {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

class Plecostomus: AquariumFish(), FishAction {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. В функции makeFish() заставьте каждую созданную вами рыбу что-нибудь съесть, вызвав eat() .
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. Запустите программу и посмотрите на результат.
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

На следующей диаграмме представлены классы Shark и Plecostomus , оба из которых состоят из интерфейса FishAction и реализуют его.

Когда следует использовать абстрактные классы, а когда интерфейсы

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

Как отмечалось выше, абстрактные классы могут иметь конструкторы, а интерфейсы — нет, но в остальном они очень похожи. Итак, когда следует использовать каждый из них?

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

Композиция часто приводит к лучшей инкапсуляции , меньшей связанности (взаимозависимости), более чистым интерфейсам и более удобному коду. По этим причинам композиция с интерфейсами является предпочтительным решением. С другой стороны, наследование от абстрактного класса, как правило, естественным образом подходит для решения некоторых задач. Поэтому следует отдавать предпочтение композиции, но когда наследование имеет смысл, Kotlin позволяет делать и это!

  • Используйте интерфейс, если у вас много методов и одна или две реализации по умолчанию, например, как в AquariumAction ниже.
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • Используйте абстрактный класс всякий раз, когда не удаётся завершить класс. Например, возвращаясь к классу AquariumFish , можно реализовать все FishAction AquariumFish и предоставить реализацию по умолчанию для eat , оставив color абстрактным, поскольку для рыбы цвета по умолчанию не существует.
interface FishAction  {
    fun eat()
}

abstract class AquariumFish: FishAction {
   abstract val color: String
   override fun eat() = println("yum")
}

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

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

Шаг 1: Создайте новый интерфейс

  1. В AquariumFish.kt удалите класс AquariumFish . Вместо наследования от класса AquariumFish , Plecostomus и Shark реализуют интерфейсы как для действий рыб, так и для их цвета.
  2. Создайте новый интерфейс FishColor , который определяет цвет как строку.
interface FishColor {
    val color: String
}
  1. Измените Plecostomus так, чтобы он реализовал два интерфейса: FishAction и FishColor . Вам нужно переопределить color из FishColor и eat() из FishAction .
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Измените класс Shark так, чтобы он также реализовал два интерфейса, FishAction и FishColor , вместо наследования от AquariumFish .
class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}
  1. Ваш готовый код должен выглядеть примерно так:
package example.myapp

interface FishAction {
    fun eat()
}

interface FishColor {
    val color: String
}

class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}

class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

Шаг 2: Создайте класс-синглтон

Затем вы реализуете настройку для делегирования, создав вспомогательный класс, реализующий FishColor . Вы создаёте базовый класс GoldColor , реализующий FishColor — он просто сообщает, что его цвет — золотой.

Создавать несколько экземпляров GoldColor не имеет смысла, поскольку все они будут делать одно и то же. Поэтому Kotlin позволяет объявить класс, в котором можно создать только один экземпляр, используя ключевое слово object вместо class . Kotlin создаст этот экземпляр, и на него будет ссылаться имя класса. После этого все остальные объекты смогут использовать только этот экземпляр — создать другие экземпляры этого класса невозможно. Если вы знакомы с шаблоном Singleton , то именно так реализуются синглтоны в Kotlin.

  1. В AquariumFish.kt создайте объект GoldColor и переопределите цвет.
object GoldColor : FishColor {
   override val color = "gold"
}

Шаг 3: Добавьте делегирование интерфейса для FishColor

Теперь вы готовы использовать делегирование интерфейса.

  1. В AquariumFish.kt удалите переопределение color у Plecostomus .
  2. Измените класс Plecostomus так, чтобы его цвет получался из GoldColor . Это делается путём добавления by GoldColor к объявлению класса, создавая делегирование. Это означает, что вместо реализации FishColor следует использовать реализацию, предоставляемую GoldColor . Таким образом, каждый раз при обращении к color он делегируется GoldColor .
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

При заданном классе все рыбы рода Pleco будут золотистыми, но на самом деле эти рыбы бывают самых разных цветов. Решить эту проблему можно, добавив параметр конструктора для цвета, указав GoldColor в качестве цвета по умолчанию для Plecostomus .

  1. Измените класс Plecostomus так, чтобы он принимал fishColor в конструкторе, и установите значение по умолчанию GoldColor . Измените делегирование с by GoldColor на by fishColor .
class Plecostomus(fishColor: FishColor = GoldColor):  FishAction,
       FishColor by fishColor {
   override fun eat() {
       println("eat algae")
   }
}

Шаг 4: Добавьте делегирование интерфейса для FishAction

Таким же образом можно использовать делегирование интерфейса для FishAction .

  1. В AquariumFish.kt создайте класс PrintingFishAction , который реализует FishAction , который принимает String , food , а затем печатает то, что ест рыба.
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. В классе Plecostomus удалите переопределяющую функцию eat() , поскольку вы замените ее делегированием.
  2. В объявлении Plecostomus делегируйте FishAction функции PrintingFishAction , передав "eat algae" .
  3. При всем этом делегировании в теле класса Plecostomus нет кода, поэтому удалите {} , поскольку все переопределения обрабатываются делегированием интерфейса.
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

На следующей диаграмме представлены классы Shark и Plecostomus , оба состоящие из интерфейсов PrintingFishAction и FishColor , но с делегированной им реализацией.

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

Класс данных похож на struct в некоторых других языках программирования — он существует в основном для хранения данных, но объект класса данных всё равно остаётся объектом. Объекты классов данных в Kotlin обладают рядом дополнительных преимуществ, например, функциями печати и копирования. В этом задании вы создадите простой класс данных и узнаете о поддержке классов данных в Kotlin.

Шаг 1: Создайте класс данных

  1. Добавьте новый пакет decor в пакет example.myapp для хранения нового кода. Щёлкните правой кнопкой мыши по example.myapp на панели «Проект» и выберите «Файл» > «Создать» > «Пакет» .
  2. В пакете создайте новый класс с названием Decoration .
package example.myapp.decor

class Decoration {
}
  1. Чтобы сделать Decoration классом данных, добавьте к объявлению класса ключевое слово data .
  2. Добавьте String свойство с именем rocks , чтобы предоставить классу некоторые данные.
data class Decoration(val rocks: String) {
}
  1. В файле, за пределами класса, добавьте функцию makeDecorations() для создания и печати экземпляра Decoration с "granite" .
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. Добавьте функцию main() для вызова makeDecorations() и запустите программу. Обратите внимание на логичный вывод, поскольку это класс данных.
⇒ Decoration(rocks=granite)
  1. В makeDecorations() создайте еще два объекта Decoration , которые оба являются «сланцами», и распечатайте их.
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

    val decoration2 = Decoration("slate")
    println(decoration2)

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. В makeDecorations() добавьте оператор print, который выводит результат сравнения decoration1 с decoration2 , а также второй оператор сравнения decoration3 с decoration2 . Используйте метод equals(), предоставляемый классами данных.
    println (decoration1.equals(decoration2))
    println (decoration3.equals(decoration2))
  1. Запустите свой код.
⇒ Decoration(rocks=granite)
Decoration(rocks=slate)
Decoration(rocks=slate)
false
true

Шаг 2. Используйте деструктуризацию

Чтобы получить свойства объекта данных и назначить их переменным, вы можете назначать их по одному, вот так.

val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver

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

val (rock, wood, diver) = decoration

Это называется деструктуризацией и является полезным сокращённым вариантом. Количество переменных должно соответствовать количеству свойств, а переменные назначаются в том порядке, в котором они объявлены в классе. Вот полный пример, который вы можете попробовать в Decoration.kt .

// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}

fun makeDecorations() {
    val d5 = Decoration2("crystal", "wood", "diver")
    println(d5)

// Assign all properties to variables.
    val (rock, wood, diver) = d5
    println(rock)
    println(wood)
    println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver)
crystal
wood
diver

Если вам не нужны одно или несколько свойств, вы можете пропустить их, используя _ вместо имени переменной, как показано в коде ниже.

    val (rock, _, diver) = d5

В этом задании вы узнаете о некоторых специализированных классах в Kotlin, включая следующие:

  • Классы Singleton
  • Перечисления
  • Запечатанные классы

Шаг 1: Вспомните классы-одиночки

Вспомните предыдущий пример с классом GoldColor .

object GoldColor : FishColor {
   override val color = "gold"
}

Поскольку каждый экземпляр GoldColor выполняет одно и то же действие, он объявлен как object , а не как class , чтобы сделать его синглтоном. Может существовать только один экземпляр.

Шаг 2: Создайте перечисление

Kotlin также поддерживает перечисления, которые позволяют перечислять объекты и ссылаться на них по имени, как в других языках. Чтобы объявить перечисление, добавьте к объявлению ключевое слово enum . Для простого объявления перечисления требуется только список имён, но вы также можете определить одно или несколько полей, связанных с каждым именем.

  1. В Decoration.kt попробуйте пример перечисления.
enum class Color(val rgb: Int) {
   RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}

Перечисления немного похожи на одиночные элементы — в перечислении может быть только одно значение каждого типа, причем только одно. Например, может быть только одно Color.RED , одно Color.GREEN и одно Color.BLUE . В этом примере значения RGB присваиваются свойству rgb для представления компонентов цвета. Вы также можете получить порядковый номер перечисления, используя свойство ordinal , и его имя, используя свойство name .

  1. Попробуйте другой пример перечисления.
enum class Direction(val degrees: Int) {
    NORTH(0), SOUTH(180), EAST(90), WEST(270)
}

fun main() {
    println(Direction.EAST.name)
    println(Direction.EAST.ordinal)
    println(Direction.EAST.degrees)
}
⇒ EAST
2
90

Шаг 3: Создайте запечатанный класс

Запечатанный класс — это класс, подклассы которого можно создать только внутри файла, в котором он объявлен. При попытке создать подкласс этого класса в другом файле возникнет ошибка.

Поскольку классы и подклассы находятся в одном файле, Kotlin статически распознаёт все подклассы. То есть, во время компиляции компилятор видит все классы и подклассы и понимает, что это всё, поэтому он может выполнить дополнительные проверки.

  1. В AquariumFish.kt попробуйте пример запечатанного класса, придерживаясь водной тематики.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()

fun matchSeal(seal: Seal): String {
   return when(seal) {
       is Walrus -> "walrus"
       is SeaLion -> "sea lion"
   }
}

Класс Seal не может быть подклассифицирован в другом файле. Если вы хотите добавить больше типов Seal , их необходимо добавить в тот же файл. Это делает запечатанные классы безопасным способом представления фиксированного количества типов. Например, запечатанные классы отлично подходят для возврата успешного или ошибочного выполнения из сетевого API .

В этом уроке мы рассмотрели много материала. Хотя многое из этого вам знакомо по другим объектно-ориентированным языкам программирования, Kotlin добавляет ряд функций, которые делают код лаконичным и удобочитаемым.

Классы и конструкторы

  • Определите класс в Kotlin с помощью class .
  • Kotlin автоматически создает сеттеры и геттеры для свойств.
  • Определите основной конструктор непосредственно в определении класса. Например:
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • Если основному конструктору требуется дополнительный код, напишите его в одном или нескольких блоках init .
  • Класс может определить один или несколько вторичных конструкторов с помощью constructor , но в стиле Kotlin вместо этого используется фабричная функция.

Модификаторы видимости и подклассы

  • Все классы и функции в Kotlin по умолчанию являются public , но вы можете использовать модификаторы, чтобы изменить видимость на internal , private или protected .
  • Чтобы создать подкласс, родительский класс должен быть помечен как open .
  • Чтобы переопределить методы и свойства в подклассе, методы и свойства должны быть помечены как open в родительском классе.
  • Запечатанный класс можно создать только в том же файле, где он определён. Чтобы создать запечатанный класс, добавьте к объявлению префикс sealed .

Классы данных, одиночные объекты и перечисления

  • Создайте класс данных, добавив к объявлению префикс data .
  • Деструктуризация — это сокращенная форма присвоения свойств объекта data отдельным переменным.
  • Создайте класс-синглтон, используя object вместо class .
  • Определите перечисление с помощью enum class .

Абстрактные классы, интерфейсы и делегирование

  • Абстрактные классы и интерфейсы — это два способа обеспечения общего поведения между классами.
  • Абстрактный класс определяет свойства и поведение, но оставляет реализацию подклассам.
  • Интерфейс определяет поведение и может предоставлять реализации по умолчанию для некоторых или всех поведений.
  • При использовании интерфейсов для создания класса функциональность класса расширяется за счет экземпляров классов, которые он содержит.
  • Делегирование интерфейса использует композицию, но также делегирует реализацию классам интерфейса.
  • Композиция — это мощный способ добавить функциональность классу с помощью делегирования интерфейса. В целом композиция предпочтительнее, но для некоторых задач лучше подходит наследование от абстрактного класса.

Документация Kotlin

Если вам нужна дополнительная информация по какой -либо теме в этом курсе, или если вы застряли, https://kotlinlang.org - ваша лучшая отправная точка.

Учебники Котлина

Веб-сайт https://try.kotlinlang.org включает в себя богатые учебники под названием Kotlin Koans, веб-интерпретатор и полный набор справочной документации с примерами.

Курс Udacity

Чтобы просмотреть курс Udacity по этой теме, см. Kotlin Bootcamp для программистов .

IntelliJ IDEA

Документация для идеи IntelliJ можно найти на веб -сайте Jetbrains.

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

  • При необходимости задавайте домашнее задание.
  • Объясните учащимся, как следует сдавать домашние задания.
  • Оцените домашние задания.

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

Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.

Ответьте на эти вопросы

Вопрос 1

У классов есть особый метод, который служит планом для создания объектов этого класса. Как называется метод?

▢ Строитель

▢ Желатель

▢ Конструктор

▢ План

Вопрос 2

Какое из следующих утверждений о интерфейсах и абстрактных классах неверно?

▢ Абстрактные классы могут иметь конструкторы.

▢ Интерфейсы не могут иметь конструкторы.

▢ Интерфейсы и абстрактные классы могут быть созданы напрямую.

▢ Абстрактные свойства должны быть реализованы подклассами абстрактного класса.

Вопрос 3

Что из следующего не является модификатором видимости котлина для свойств, методов и т. Д.?

internal

nosubclass

protected

private

Вопрос 4

Рассмотрим этот класс данных:
data class Fish(val name: String, val species:String, val colors:String)
Что из следующего не является действительным кодом для создания и разрушения объекта Fish ?

val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")

val (name2, _, colors2) = Fish("Bitey", "shark", "gray")

val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")

val (name4, species4, colors4) = Fish("Harry", "halibut")

Вопрос 5

Допустим, у вас есть зоопарк с большим количеством животных, о которых нужно позаботиться. Что из следующего не будет частью реализации заботы?

interface для различных видов пищевых продуктов, которые едят животные.

▢ Класс abstract Caretaker , из которого вы можете создавать различные типы опекунов.

interface для подачи чистой воды животному.

▢ Класс data для записи в графике кормления.

Перейдите к следующему уроку: 5.1 Расширения

Для обзора курса, включая ссылки на другие коделаб, см. «Kotlin Bootcamp для программистов: добро пожаловать на курс».