Skip to content
This repository was archived by the owner on Jul 23, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
310 changes: 310 additions & 0 deletions docs/block-2/abstractions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
# Абстракції (Спадкування)

## Завдання

Ускладнимо завдання: у нас є притулок з домашніми тваринами, і нам потрібно
зберігати уніфіковану інформацію про кожну тварину.
Якщо ви раніше створювали таблиці в тому ж Excel, ви, швидше за все, розумієте,
як ви можете виділити параметри кожного вихованця.

Що ж таке спадкування? **Спадкування** - це властивість об'єкта набувати
риси *іншого об'єкта* за допомогою абстракції.

Як же вирішуватимемо завдання? Давайте спочатку виділимо, які вихованці у нас
взагалі є:

- собаки
- коти
- папуги

Тепер нашим завданням є знайти загальні властивості даних об'єктів (вихованців)
щоб виділити їх у більш загальну сутність.
Перше, що швидше за все, спало на думку — це імена. У всіх домашніх
тварин є якісь імена. Відразу ж після цього, буде неважко додумати
деякі інші властивості: скільки їм років, їх опис (може, наприклад, це говорящий папуга або кіт)
та інші їх атрибути (властивості).

Також, кожен з них, має власні особливості, які існують тільки в них – собаки бігають,
папуги літають, а коти просто ліниві. Візуалізуймо:

![Наслідування](images/pet_object.png#invert)

Зі структурою розібрались, але як це реалізувати на Kotlin?

## Interface

Одним з варіантів рішення є `interface`. Цей тип опису структури припускає тільки опис контракту того, як
клас, що його наслідує (у відношені інтерфейсів ще часто говорять «імплеменує»), буде поводитись та які саме дані
буде мати.
:::info Термінологія
**Контракт** – формальний опис того, що робить будь-яка сутність (починаючи з функцій до класу або інтерфесу).
:::
Створюється інтерфейс наступним чином:

```kotlin
interface Foo {...}
```

Варто враховувати, що на відміну від класів чи об'єктів — інтерфейси stateless (тобто, не можуть зберігати ніяких
даних). Також вони не є самостійною структурною одиницею й існують тільки за допомогою об'єктів, що їх реалізують
(імплементують, наслідують).
Тобто, ви не можете зробити наступне:

```kotlin
interface Foo {
// This will error
val name: String = "" // Error: Property initializers are not allowed in interfaces
}
```

Тому зробити можна тільки так:

```kotlin
interface Foo {
val name: String
}
```

Спробуймо віднаслідувати даний інтерфейс:

```kotlin
class Bar : Foo {
override val name: String = "Bar"
}
```

:::info Інформація
Ключове слово `override` використовується для того, щоб ініціалізовувати те, що не було ініціалізовано до цього або
для того, щоб змінити те, що вже ініціалізовано, якщо можливо (розглянемо це питання нижче).
:::
Для всіх ситуацій, окрім тих, де вам не потрібно (дійсно потрібно) зберігати якійсь дані у своїй абстракції
краще використовувати цей вид абстракцій. Але, розгляньмо й інший варіант того, як це можна зробити:

## Abstract class

Іншим варіантом реалізації абстракції – є абстрактний класс. Він може все що й звичайний клас, але може мати
не ініціалізованих членів класу (функції або властивості) та не може бути зконструйований викликом конструктора. Може
бути тільки батьківським классом (тобто, реалізується через спадкоємця через спадкування).

Розгляньмо на прикладі.

```kotlin
abstract class Foo(var name: String) { // він також має конструктор
abstract val someNumber: Int

abstract fun isEarthRound(): Boolean
}

class Bar : Foo("Bar") { // викликаємо конструктор при наслідуванні
// залишимо не ініціалізованими члени класу, який наслідуємо.
}
```

Ми не ініціалізували члени класу, який наслідуємо тому, при спробі запуску, отримаємо помилку:

```
Class 'Bar' is not abstract and does not implement abstract base class member public abstract val someNumber:
Int defined in Foo
Class 'Bar' is not abstract and does not implement abstract base class member public abstract fun isEarchRound():
Boolean defined in Foo
```

:::tip Цікаво знати
До речі, абстрактний клас може наслідувати абстрактний клас (і також інтерфейс може наслідувати інтерфейс).
:::
Тому нам потрібно реалізувати наш клас:

```kotlin
class Bar : Foo("Bar") { // викликаємо конструктор при наслідуванні
override val someNumber: Int = 1000
override fun isEarthRound() = false // а ви шо думали?
}
```

Але, що якщо ми хочемо зробити абстракцію можливою до використання без спадкоємця (наслідника)?

## Open class

Цей вид класів може бути як віднаслідуваним, так і просто створеним:

```kotlin
open class Foo {
fun isEarthRound(): Boolean = false
}
```

Цей клас може бути створеним:

```kotlin
fun main() {
val foo = Foo()
println("Is Earth round? ${foo.isEarthRound()}")
}
```

І також може мати спадкоємця:

```kotlin
class Bar : Foo() {
override fun isEarthRound() = true
}
```

Начебто, все окей, але Kotlin нам скаже наступне:

```
'isEarthRound' in 'Foo' is final and cannot be overridden
```

Насправді таке ж би було, якщо ми б захотіли ініціалізувати в абстрактному класі не абстрактного члена.
Тому, за аналогією абстрактних членів, додамо до функції модифікатор `open`.

```kotlin
open class Foo {
open fun isEarthRound(): Boolean = false
}
```

Після чого ми вже зможемо переназначити (ініціалізувати) функцію:

```kotlin
class Bar : Foo() {
override fun isEarthRound() = true // тепер все ок
}
```

:::tip Чому все так?
Для того, щоб дізнатись більше, чому всі члени в Kotlin за замовчуванням `final` (фінальні, тобто їх вже не можна
змінювати), можна прочитати про [кризис базового класу](https://en.wikipedia.org/wiki/Fragile_base_class).
:::

## Рішення

Але перейдім все ж таки до того, як ми розв'яжемо нашу задачу.

Насправді нам мало чим підходить `open class`, бо в нас немає ніякого окремого 'Pet', а є конкретна тварина.
Абстрактний клас нам не підходить, бо в нас немає ніяких початкових значень та взагалі чогось, що було
б визначено початково (у нас все має визначати спадкоємець). Тому напишим варіантом буде interface:

```kotlin
// до речі всі члени interface за замовчуванням `open`
interface Pet {
val name: String
val age: Int

fun sound(): String
}
```

І віднаслідуємо:

```kotlin
class Cat(override val name: String, override val age: String) : Pet {
override fun sound(): String = "meow<3"
}

class Dog(override val name: String, override val age: String) : Pet {
override fun sound(): String = "aww!"
override fun run() {
println("pretend dog is running..")
}
}

class Perrot(override val name: String, override val age: String) : Pet {
override fun sound(): String = "squawk!"
override fun fly() {
println("pretend perrot is flying..")
}
}
```

І створім функцію, що буде використовувати нашу абстракцію:

```kotlin
fun printSound(pet: Pet) {
println(pet.sound())
}
```

І викличемо цю функцію:

```kotlin
fun main() {
val cat = Cat("Мася", 4)
val dog = Dog("Мопс", 1)
val perrot = Perrot("Жан", 2)

printSound(cat)
printSound(dog)
printSound(perrot)
}
```

Для прикладу поки зробили так.
До речі, а як нам в подібній функції перевірити, яка сама реалізація була передана аргументом?
Наприклад, для того, щоб виконати унікальну дію нашого об'єкта (`fly()` або `run()`).
Для цього існують два оператори:

- `is`: оператор, який говорить, чи є екземпляр вказаним об'єктом:
```kotlin
if(pet is Dog)
pet.run()
```
- `as`: оператор приведення типа до якогось іншого:
```kotlin
(pet as Dog).run() // може бути помилка, бо перевірка типа не відбувається
```

Рекомендую використовувати перший оператор завжди, коли ви не впевнені в тому, що параметр не є конкретно
переданим типом об'єкта.

Зробім же нашу функцію повноцінно:

```kotlin
fun doUniqueAction(pet: Pet) {
when {
pet is Cat -> println("я просто лінивий")
pet is Dog -> pet.run()
pet is Perrot -> pet.fly()
}
}
```
І у нас все готово, але, до речі, це можна спростити:
```kotlin
fun doUniqueAction(pet: Pet) {
when(pet) {
is Cat -> println("я просто лінивий")
is Dog -> pet.run()
is Perrot -> pet.fly()
}
}
```
Викличемо нашу функцію:
```kotlin
fun main() {
val cat = Cat("Мася", 4)
val dog = Dog("Мопс", 1)
val perrot = Perrot("Жан", 2)

doUniqueAction(cat)
doUniqueAction(dog)
doUniqueAction(perrot)
}
```
Ось ми й зробили нашу абстракцію!
:::tip Цікаво знати
До речі, будь-який об'єкт за замовчуванням спадкує клас `Any`. Наприклад, за допомогою цього,
у будь-якого об'єкта є `toString()`.

Але ніякої магії в цій функції немає: кожен спадкоємець, якщо хоче мати `toString()`, має його реалізувати:
```kotlin
class Foo(..) {
...

override fun toString() = "my awesome string representation of object"
}
```
Раніше ми розглядали базові типи Kotlin, що вже за замовчуванням мають реалізацію цих функцій. Але,
з нашими об'єктами нам знадобиться робити це вручну (але насправді рідко коли це потрібно) або
використовувати інші типи класів, про які ми поговоримо згодом.
:::
Loading