Kotlin Bootcamp for Programmers 4: Объектно-ориентированное программирование

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

Введение

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

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

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

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

Что вы узнаете

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

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

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

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

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

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

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

Пакеты могут помочь вам организовать ваш код.

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

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

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

  1. Щелкните правой кнопкой мыши пакет example.myapp .
  2. Выберите « Создать» > «Файл/класс Kotlin» .
  3. В разделе « Тип » выберите « Класс » и назовите класс « 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 см^3) воды плюс небольшое дополнительное пространство, чтобы вода не проливалась.
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() под получателем, который пересчитывает высоту на основе предоставленного volume воды. По соглашению имя параметра установки — 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 общедоступно, а это означает, что все доступно везде, включая классы, методы, свойства и переменные-члены.

В 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 , вы можете заставить все AquariumFish реализовывать FishAction и предоставить реализацию по умолчанию для 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")
   }
}

С таким классом все Plecos будут золотыми, но на самом деле эти рыбы бывают разных цветов. Вы можете решить эту проблему, добавив параметр конструктора для цвета с 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() добавьте оператор печати, который выводит результат сравнения 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, в том числе о следующих:

  • Одноэлементные классы
  • перечисления
  • Запечатанные классы

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

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

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

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

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

Kotlin также поддерживает перечисления, которые позволяют вам перечислять что-то и ссылаться на это по имени, как и в других языках. Объявите перечисление, поставив перед объявлением ключевое слово enum . Базовое объявление 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 .

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

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

Котлин документация

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

Kotlin tutorials

The https://try.kotlinlang.org website includes rich tutorials called Kotlin Koans, a web-based interpreter , and a complete set of reference documentation with examples.

Udacity course

To view the Udacity course on this topic, see Kotlin Bootcamp for Programmers .

IntelliJ IDEA

Documentation for the IntelliJ IDEA can be found on the JetBrains website.

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

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

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

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

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

Вопрос 1

Classes have a special method that serves as a blueprint for creating objects of that class. What is the method called?

▢ A builder

▢ An instantiator

▢ A constructor

▢ A blueprint

вопрос 2

Which of the following statements about interfaces and abstract classes is NOT correct?

▢ Abstract classes can have constructors.

▢ Interfaces can't have constructors.

▢ Interfaces and abstract classes can be instantiated directly.

▢ Abstract properties must be implemented by subclasses of the abstract class.

Вопрос 3

Which of the following is NOT a Kotlin visibility modifier for properties, methods, etc.?

internal

nosubclass

protected

private

Question 4

Consider this data class:
data class Fish(val name: String, val species:String, val colors:String)
Which of the following is NOT valid code to create and destructure a Fish object?

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")

Question 5

Let's say you own a zoo with lots of animals that all need to be taken care of. Which of the following would NOT be part of implementing caretaking?

▢ An interface for different types of foods animals eat.

▢ An abstract Caretaker class from which you can create different types of caretakers.

▢ An interface for giving clean water to an animal.

▢ A data class for an entry in a feeding schedule.

Proceed to the next lesson: 5.1 Extensions

For an overview of the course, including links to other codelabs, see "Kotlin Bootcamp for Programmers: Welcome to the course."