Skip to content
This repository was archived by the owner on Jul 23, 2025. It is now read-only.

Commit 5828cf4

Browse files
authored
Added data structures block (not finished) (#58)
* feat: added objects, classes and abstractions * feat: added data structure block (not finished yet)
1 parent 15241fe commit 5828cf4

14 files changed

+692
-382
lines changed

docs/block-2/abstractions.md

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# Абстракції (Спадкування)
2+
3+
## Завдання
4+
5+
Ускладнимо завдання: у нас є притулок з домашніми тваринами, і нам потрібно
6+
зберігати уніфіковану інформацію про кожну тварину.
7+
Якщо ви раніше створювали таблиці в тому ж Excel, ви, швидше за все, розумієте,
8+
як ви можете виділити параметри кожного вихованця.
9+
10+
Що ж таке спадкування? **Спадкування** - це властивість об'єкта набувати
11+
риси *іншого об'єкта* за допомогою абстракції.
12+
13+
Як же вирішуватимемо завдання? Давайте спочатку виділимо, які вихованці у нас
14+
взагалі є:
15+
16+
- собаки
17+
- коти
18+
- папуги
19+
20+
Тепер нашим завданням є знайти загальні властивості даних об'єктів (вихованців)
21+
щоб виділити їх у більш загальну сутність.
22+
Перше, що швидше за все, спало на думку — це імена. У всіх домашніх
23+
тварин є якісь імена. Відразу ж після цього, буде неважко додумати
24+
деякі інші властивості: скільки їм років, їх опис (може, наприклад, це говорящий папуга або кіт)
25+
та інші їх атрибути (властивості).
26+
27+
Також, кожен з них, має власні особливості, які існують тільки в них – собаки бігають,
28+
папуги літають, а коти просто ліниві. Візуалізуймо:
29+
30+
![Наслідування](images/pet_object.png#invert)
31+
32+
Зі структурою розібрались, але як це реалізувати на Kotlin?
33+
34+
## Interface
35+
36+
Одним з варіантів рішення є `interface`. Цей тип опису структури припускає тільки опис контракту того, як
37+
клас, що його наслідує (у відношені інтерфейсів ще часто говорять «імплеменує»), буде поводитись та які саме дані
38+
буде мати.
39+
:::info Термінологія
40+
**Контракт** – формальний опис того, що робить будь-яка сутність (починаючи з функцій до класу або інтерфесу).
41+
:::
42+
Створюється інтерфейс наступним чином:
43+
44+
```kotlin
45+
interface Foo {...}
46+
```
47+
48+
Варто враховувати, що на відміну від класів чи об'єктів — інтерфейси stateless (тобто, не можуть зберігати ніяких
49+
даних). Також вони не є самостійною структурною одиницею й існують тільки за допомогою об'єктів, що їх реалізують
50+
(імплементують, наслідують).
51+
Тобто, ви не можете зробити наступне:
52+
53+
```kotlin
54+
interface Foo {
55+
// This will error
56+
val name: String = "" // Error: Property initializers are not allowed in interfaces
57+
}
58+
```
59+
60+
Тому зробити можна тільки так:
61+
62+
```kotlin
63+
interface Foo {
64+
val name: String
65+
}
66+
```
67+
68+
Спробуймо віднаслідувати даний інтерфейс:
69+
70+
```kotlin
71+
class Bar : Foo {
72+
override val name: String = "Bar"
73+
}
74+
```
75+
76+
:::info Інформація
77+
Ключове слово `override` використовується для того, щоб ініціалізовувати те, що не було ініціалізовано до цього або
78+
для того, щоб змінити те, що вже ініціалізовано, якщо можливо (розглянемо це питання нижче).
79+
:::
80+
Для всіх ситуацій, окрім тих, де вам не потрібно (дійсно потрібно) зберігати якійсь дані у своїй абстракції
81+
краще використовувати цей вид абстракцій. Але, розгляньмо й інший варіант того, як це можна зробити:
82+
83+
## Abstract class
84+
85+
Іншим варіантом реалізації абстракції – є абстрактний класс. Він може все що й звичайний клас, але може мати
86+
не ініціалізованих членів класу (функції або властивості) та не може бути зконструйований викликом конструктора. Може
87+
бути тільки батьківським классом (тобто, реалізується через спадкоємця через спадкування).
88+
89+
Розгляньмо на прикладі.
90+
91+
```kotlin
92+
abstract class Foo(var name: String) { // він також має конструктор
93+
abstract val someNumber: Int
94+
95+
abstract fun isEarthRound(): Boolean
96+
}
97+
98+
class Bar : Foo("Bar") { // викликаємо конструктор при наслідуванні
99+
// залишимо не ініціалізованими члени класу, який наслідуємо.
100+
}
101+
```
102+
103+
Ми не ініціалізували члени класу, який наслідуємо тому, при спробі запуску, отримаємо помилку:
104+
105+
```
106+
Class 'Bar' is not abstract and does not implement abstract base class member public abstract val someNumber:
107+
Int defined in Foo
108+
Class 'Bar' is not abstract and does not implement abstract base class member public abstract fun isEarchRound():
109+
Boolean defined in Foo
110+
```
111+
112+
:::tip Цікаво знати
113+
До речі, абстрактний клас може наслідувати абстрактний клас (і також інтерфейс може наслідувати інтерфейс).
114+
:::
115+
Тому нам потрібно реалізувати наш клас:
116+
117+
```kotlin
118+
class Bar : Foo("Bar") { // викликаємо конструктор при наслідуванні
119+
override val someNumber: Int = 1000
120+
override fun isEarthRound() = false // а ви шо думали?
121+
}
122+
```
123+
124+
Але, що якщо ми хочемо зробити абстракцію можливою до використання без спадкоємця (наслідника)?
125+
126+
## Open class
127+
128+
Цей вид класів може бути як віднаслідуваним, так і просто створеним:
129+
130+
```kotlin
131+
open class Foo {
132+
fun isEarthRound(): Boolean = false
133+
}
134+
```
135+
136+
Цей клас може бути створеним:
137+
138+
```kotlin
139+
fun main() {
140+
val foo = Foo()
141+
println("Is Earth round? ${foo.isEarthRound()}")
142+
}
143+
```
144+
145+
І також може мати спадкоємця:
146+
147+
```kotlin
148+
class Bar : Foo() {
149+
override fun isEarthRound() = true
150+
}
151+
```
152+
153+
Начебто, все окей, але Kotlin нам скаже наступне:
154+
155+
```
156+
'isEarthRound' in 'Foo' is final and cannot be overridden
157+
```
158+
159+
Насправді таке ж би було, якщо ми б захотіли ініціалізувати в абстрактному класі не абстрактного члена.
160+
Тому, за аналогією абстрактних членів, додамо до функції модифікатор `open`.
161+
162+
```kotlin
163+
open class Foo {
164+
open fun isEarthRound(): Boolean = false
165+
}
166+
```
167+
168+
Після чого ми вже зможемо переназначити (ініціалізувати) функцію:
169+
170+
```kotlin
171+
class Bar : Foo() {
172+
override fun isEarthRound() = true // тепер все ок
173+
}
174+
```
175+
176+
:::tip Чому все так?
177+
Для того, щоб дізнатись більше, чому всі члени в Kotlin за замовчуванням `final` (фінальні, тобто їх вже не можна
178+
змінювати), можна прочитати про [кризис базового класу](https://en.wikipedia.org/wiki/Fragile_base_class).
179+
:::
180+
181+
## Рішення
182+
183+
Але перейдім все ж таки до того, як ми розв'яжемо нашу задачу.
184+
185+
Насправді нам мало чим підходить `open class`, бо в нас немає ніякого окремого 'Pet', а є конкретна тварина.
186+
Абстрактний клас нам не підходить, бо в нас немає ніяких початкових значень та взагалі чогось, що було
187+
б визначено початково (у нас все має визначати спадкоємець). Тому напишим варіантом буде interface:
188+
189+
```kotlin
190+
// до речі всі члени interface за замовчуванням `open`
191+
interface Pet {
192+
val name: String
193+
val age: Int
194+
195+
fun sound(): String
196+
}
197+
```
198+
199+
І віднаслідуємо:
200+
201+
```kotlin
202+
class Cat(override val name: String, override val age: String) : Pet {
203+
override fun sound(): String = "meow<3"
204+
}
205+
206+
class Dog(override val name: String, override val age: String) : Pet {
207+
override fun sound(): String = "aww!"
208+
override fun run() {
209+
println("pretend dog is running..")
210+
}
211+
}
212+
213+
class Perrot(override val name: String, override val age: String) : Pet {
214+
override fun sound(): String = "squawk!"
215+
override fun fly() {
216+
println("pretend perrot is flying..")
217+
}
218+
}
219+
```
220+
221+
І створім функцію, що буде використовувати нашу абстракцію:
222+
223+
```kotlin
224+
fun printSound(pet: Pet) {
225+
println(pet.sound())
226+
}
227+
```
228+
229+
І викличемо цю функцію:
230+
231+
```kotlin
232+
fun main() {
233+
val cat = Cat("Мася", 4)
234+
val dog = Dog("Мопс", 1)
235+
val perrot = Perrot("Жан", 2)
236+
237+
printSound(cat)
238+
printSound(dog)
239+
printSound(perrot)
240+
}
241+
```
242+
243+
Для прикладу поки зробили так.
244+
До речі, а як нам в подібній функції перевірити, яка сама реалізація була передана аргументом?
245+
Наприклад, для того, щоб виконати унікальну дію нашого об'єкта (`fly()` або `run()`).
246+
Для цього існують два оператори:
247+
248+
- `is`: оператор, який говорить, чи є екземпляр вказаним об'єктом:
249+
```kotlin
250+
if(pet is Dog)
251+
pet.run()
252+
```
253+
- `as`: оператор приведення типа до якогось іншого:
254+
```kotlin
255+
(pet as Dog).run() // може бути помилка, бо перевірка типа не відбувається
256+
```
257+
258+
Рекомендую використовувати перший оператор завжди, коли ви не впевнені в тому, що параметр не є конкретно
259+
переданим типом об'єкта.
260+
261+
Зробім же нашу функцію повноцінно:
262+
263+
```kotlin
264+
fun doUniqueAction(pet: Pet) {
265+
when {
266+
pet is Cat -> println("я просто лінивий")
267+
pet is Dog -> pet.run()
268+
pet is Perrot -> pet.fly()
269+
}
270+
}
271+
```
272+
І у нас все готово, але, до речі, це можна спростити:
273+
```kotlin
274+
fun doUniqueAction(pet: Pet) {
275+
when(pet) {
276+
is Cat -> println("я просто лінивий")
277+
is Dog -> pet.run()
278+
is Perrot -> pet.fly()
279+
}
280+
}
281+
```
282+
Викличемо нашу функцію:
283+
```kotlin
284+
fun main() {
285+
val cat = Cat("Мася", 4)
286+
val dog = Dog("Мопс", 1)
287+
val perrot = Perrot("Жан", 2)
288+
289+
doUniqueAction(cat)
290+
doUniqueAction(dog)
291+
doUniqueAction(perrot)
292+
}
293+
```
294+
Ось ми й зробили нашу абстракцію!
295+
:::tip Цікаво знати
296+
До речі, будь-який об'єкт за замовчуванням спадкує клас `Any`. Наприклад, за допомогою цього,
297+
у будь-якого об'єкта є `toString()`.
298+
299+
Але ніякої магії в цій функції немає: кожен спадкоємець, якщо хоче мати `toString()`, має його реалізувати:
300+
```kotlin
301+
class Foo(..) {
302+
...
303+
304+
override fun toString() = "my awesome string representation of object"
305+
}
306+
```
307+
Раніше ми розглядали базові типи Kotlin, що вже за замовчуванням мають реалізацію цих функцій. Але,
308+
з нашими об'єктами нам знадобиться робити це вручну (але насправді рідко коли це потрібно) або
309+
використовувати інші типи класів, про які ми поговоримо згодом.
310+
:::

0 commit comments

Comments
 (0)