НМЦ

ОБ’ЄКТНО ОРІЄНТОВАНЕ ПРОГРАМУВАННЯ

Електронний посібник

 

ВФПО

Модуль 3

 

3. Інкапсуляція. успадкування та повторне використання коду

3.1. Інкапсуляція та конструктори

3.2. Створення та використання масивів

3.3. Реалізація успадкування

 

 

Відеозапис лекції:

 

browser does not support the video tag.

 

Частина 1: ООП. Наслідування Методи. Конструктори.

Проєктування ієрархії класів (UML)

https://youtu.be/ybQk1G1lsBo

 

Одною із переваг ООП є те, що користувачі класів можуть використовувати їх, майже не знаючи, як вони реалізовані, використовуючи лише кілька методів для роботи з конкретним об’єктом. Таке приховування реалізації класу слугує також і певним захистом від неправильної роботи з даними. Такий принцип реалізації називається "інкапсуляцією". Якщо візьмемо реальний світ, то, наприклад, ви не знаєте як влаштований телевізор, проте ви можете його увімкнути та за допомогою кнопок переключати канали. Сама ж схемотехнічна реалізація прикрита корпусом телевізора і переважно невідома глядачам ТВ. Аналогічно і класи розробляються за схожим принципом. Створюються певні методи (інтерфейс класу), через які відбувається доступ до полів класу (змінних) та його методів. Всі інші класи, методи, поля, які слугують лише для обслуговування внутрішнього функціонування класу, намагаються захистити, щоб до них не було доступу без крайньої на те потреби. Це в свою чергу зменшує кількість помилок в програмі через невмілі дії користувача цього класу.

Інкапсуляція – один з основних принципів об'єктно орієнтованого програмування

 

Йдеться про те, що об’єкт вміщує не тільки дані, але і правила їх обробки, оформлені у вигляді виконуваних фрагментів (методів). Доступ до стану об'єкта напряму заборонено і ззовні з ним можна взаємодіяти виключно через заданий інтерфейс, що дозволяє контролювати правильність звернення до полів та їхню ініціалізацію. Також інкапсулюються методи, які виконують допоміжну роль і не бажано, щоб користувач-програміст мав доступ до них. Оскільки користувачі-програмісти працюють лише через відкриті елементи класів, то розробники класу можуть як завгодно змінювати всі закриті елементи і навіть перейменовувати та видаляти їх, не турбуючись, що десь хтось їх використовує у своїх програмах.

Наприклад, вам потрібно зробити певну програму по роботі зі списком автомобілів.

 

Для цього потрібний відповідний клас Car:

package org.wikibooks.uk.osvjava;

public class Car {

    public static int count;

    public int id;

    public String _maker;

    public double _price;

    public String _year;

    public String _color;

    //конструктор без параметрів

    public Car() {

        count++;

        id = count;

    }

    //конструктор з параметрами, який ініціалізує всі поля класу

    public Car(String maker, String color, double price, String year) {

        _maker = maker;

        _price = price;

        _year = year;

        _color = color;

        count++;

        id = count;

    }

    //заміщення (перевизначення) методу toString() класу Object

    //замість дескриптора об'єкта, він виводитиме інформацію по автомобілю

    @Override

    public String toString() {

return "Авто " + id + " " + _maker + " " + _color + " " + _price + " " + _year + " ";

    }

    //тестовий метод main

    public static void main(String[] args) {

       //створюємо об'єкт car1 конструктором без параметрів 

        Car car1 = new Car();

        car1._maker = "Audi";

        car1._price = 10000;

        car1._year = "2000";

        car1._color = "red";

       //створюємо об'єкт car2 конструктором з параметрами

        Car car2 = new Car("BMW", "black", 12000, "2001");

        //виведення інформації про автомобілі

        //при цьому застосовуватиметься заміщений в цьому класі метод toString

        System.out.println(car1);

        System.out.println(car2);

    }

}

Результат виконання:

Авто 1: Audi red 10000.0 2000

Авто 2: BMW black 12000.0 2001

 

У вищенаведеному прикладі для того, щоб рахувати кількість об'єктів, ми створюємо статичну змінну count, яка буде спільною для всіх об'єктів car. В методі main ми створюємо екземпляри класу Car двома способами. Спочатку вручну ініціалізуємо кожне поле car1, а поля car2 ініціалізуємо через конструктор. Крім того, що другий спосіб є простішим, перший спосіб може слугувати ще й джерелом низки помилок. Наприклад, ми можемо забути ініціалізувати певне поле. Крім того, ініціалізуючи через конструктор, ми можемо здійснити попередню перевірку на правильність введення даних. Наприклад, на правильність введення назви виробника тощо. Також маємо два службових поля: count та id, які ініціалізуються в конструкторах, проте ми можемо задати значення і напряму, що може зашкодити логіці функціонування об'єкту класу. Ми запросто можемо вказати, що є 100 автомобілів, хоча насправді їх 66. І якщо будемо десь використовувати цикл з перебору автомобілів з врахування змінної count, це викличе помилку. Тому в ООП і придумано інкапсуляцію – приховування внутрішньої реалізації класу. Рекомендується оголошувати поля та методи з самого початку закритими і лише за необхідності надавати до них більший доступ.

 

Модифікуємо дещо нашу програму:

package org.wikibooks.uk.osvjava;

public class Car {

    private static int count=0;

    private int id;

    private String _maker;

    private double _price;

    private String _year;

    private String _color;

    //конструктор з параметрами, який ініціалізує всі поля класу

    public Car(String maker, String color, double price, String year) {

        _maker = maker;

        _price = price;

        _year = year;

        _color = color;

        count++;

        id = count;

    }

    //заміщення (перевизначення) методу toString() класу Object

    //замість дескриптора об'єкта, він виводитиме інформацію по автомобілю

    @Override

    public String toString() {

        return id+". "+ _maker + " " + _color + " " + _price + " " + _year + " ";

    }

    //метод для отримання значення поля id

    public int getId() {

        return id;

    }

    //метод для отримання кількості автомобілів

    public static int getCount() {

        return count;

    }  

    //тестовий метод main

    public static void main(String[] args) {

        Car car[]=new Car[5];       

        car[0] = new Car("Audi","red",10000,"2000" );

        car[1] = new Car("BMW", "black", 12000, "2001");    

        car[2] = new Car("Daewoo", "white", 8000, "2001");

        car[3] = new Car("Reno", "black", 12000, "2001");   

        for (int i = 0; i < Car.getCount(); i++) {

            System.out.println(car[i]);

        }

    }

}

Результат виконання:

1. Audi red 10000.0 2000

2. BMW black 12000.0 2001

3. Daewoo white 8000.0 2001

4. Reno black 12000.0 2001

 

Як бачимо, усі поля у нас тепер приватні. Робота з закритими полями можлива лише через відповідні методи. Для доступу до полів count та id створено методи getCount та getId. За необхідності можна створити методи для доступу до інших полів. Також можна створити певні методи модифікації окремих полів (setId та ін.) та передбачити в них попередню перевірку значень, що вводяться. Тож ми усунули можливість появи помилок у програмуванні, пов'язаних з неправильним використанням полів та методів класу. Іншим програмістам буде значно легше використовувати клас Car, їм не потрібно вникати в особливості реалізації цього класу.

Необхідно зазначити, що методи, які читають значення полів, прийнято називати з використанням префіксу get з наступним вказанням назви змінної, а для тих, що модифікують значення, використовують префікс set. В англомовній термінології можна зустріти терміни getter та setter методи або метод accesor та метод mutator.

Зверніть увагу, як здійснюється звернення до змінної count. Насправді до методу можна звернутися з використанням об'єкта car[i].getCount(), але оскільки цей метод та змінна є статичними, тобто вона та метод спільно використовуються усіма об'єктами, то логічнішим є зверненням до неї через назву клас Car.getCount(). Цей метод можна викликати до створення будь-яких об'єктів. В такому разі ми просто отримаємо 0. Якщо б змінна не була приватною, то доступ до неї можна було б робити без застосування методу, безпосередньо: Car.count.

Згадаємо тепер приклад з сейфами з попередньої лекції. Замість методу safeValue(), який в нас заповнює змінні об’єкта значеннями, ми можемо створити метод, який буде мати назву таку ж, як і клас, тобто Safe(pWidth, pHeight, pDepth). Це дасть нам можливість ще скоротити програму, оскільки при створенні об’єкта ми зможемо зразу ж задавати розміри сейфу.

 

Safe mySafe1 = new Safe(10.0, 15.0, 20.0)

 

Такі методи носять назву конструктор класу. Коли ми писали просто new Safe(), то віртуальна машина використовувала конструктор за замовчуванням без параметрів, який практично нічого корисного для нас не робив. Тепер же ми можемо використовувати новий створений нами конструктор.

 

Таким чином, новий варіант програми:

class Safe {

    double width;

    double height;

    double depth;

    // конструктор

    Safe(double pWidth, double pHeight, double pDepth) {

        width = pWidth;

        height = pHeight;

        depth = pDepth;

    }

    // обчислюємо об'єм сейфу

    double getVolume() {

        return width * height * depth;

    }

}

public class CoinsVolume {

    public static void main(String[] args) {

        double width1 = 10, height1 = 20, depth1 = 40;

        Safe safe1 = new Safe(width1, height1, depth1); // створюємо 1-й сейф

        Safe safe2 = new Safe(10.0, 15.0, 20.0); // створюємо 2-й сейф

        Safe safe3 = new Safe(10.3, 15.4, 20.5); // створюємо 3-й сейф

        Safe safe4 = new Safe(20.0, 30.0, 20.0); // створюємо 4-й сейф

        printSafeVolume(safe1, 1); // виводимо об'єм 1-го сейфу

        printSafeVolume(safe2, 2); // виводимо об'єм 2-го сейфу

        printSafeVolume(safe3, 3); // виводимо об'єм 3-го сейфу

        printSafeVolume(safe4, 4); // виводимо об'єм 4-го сейфу

    }

    // виведення об'єму сейфу

    // safe – сейф

    // number – номер сейфу

    static void printSafeVolume(Safe safe, int number) {

// викликаємо метод getVolume(), що обчислює об’єм сейфу і результат виводимо на екран

        System.out.println("Об'єм " + number + "-го сейфу = " + safe.getVolume());

    }

}

Результат виконання:

Об'єм 1-го сейфу = 8000.0

Об'єм 2-го сейфу = 3000.0

Об'єм 3-го сейфу = 3251.71

Об'єм 4-го сейфу = 12000.0

 

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

Тема класів та методів значно комплексніша, тому основне, що ви маєте винести з цього заняття, – це розуміння того, як використовуються методи (зокрема конструктори) класів. Java надає великий набір уже готових класів та методів, які значно спрощують роботу програміста.

 

 

Відеозапис лекції:

 

 

Частина 1: Повторення: масиви в JAVA

https://youtu.be/_CIHMRxUzak

 

Масив – це впорядкований набір однотипних елементів, на які посилаються за спільним іменем.

 

Це доволі зручний засіб групування інформації. Масиви можна створювати з елементів будь-якого типу. До конкретного елемента в масиві звертаються за індексом (номером). Вони можуть бути як одновимірні, так і багатовимірні.

 

Одновимірні масиви – це список однотипних елементів. Загальний формат оголошення такого масиву:

тип-елементів назва-масиву[];

 

Наприклад:

int month_days[]; // масив цілих чисел

 

Існує також інша форма оголошення масиву:

int[] month_days;

 

Проте для того, щоб масив почав існувати, необхідно виділити під нього пам'ять за допомогою операції new.

 

назва-масиву = new тип-елементів [розмір];

 

де розмір – планована кількість елементів у масиві.

month_days = new int[12];

 

або зразу ж:

int month_days[] = new int[12];

 

Таким чином відбувається виділення пам'яті під масив і ініціалізація елементів масиву нулями. В подальшому можна напряму звертатися до елементів масиву, вказуючи індекс у квадратних дужках. Нумерація елементів у масиві в Java відбувається з нуля. Тобто в наведеному прикладі звернення до першого (нульового) елемента – month_days[0], а до останнього – month_days[11]. Java не дозволить програмі звернутися поза межі масиву, щоправда помилка буде вказана лише на етапі виконання програми через викидання винятку (виключення, exception).

month_days[5] = 30;

System.out.println(month_days[5]);

Масиви також можна ініціалізувати зразу ж при їхньому оголошенні, не використовуючи операції new, аналогічно як це відбувається під час роботи з простими типами даних.

Наступний приклад зразу ж при оголошенні ініціалізує масив month_days[] кількістю днів у місяцях.

public class DaysOfMonth {

    public static void main(String[] args) {

    int month_days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

    //оголошуємо та ініціалізуємо масив

    System.out.println("Травень має " + month_days[4] + " день"); // вивід на консоль

    }

}

Результат виконання на екрані:

Травень має 31 день

Наступний приклад демонструє знаходження максимального числа в одновимірному масиві.

public class ArrayMax {

    public static void main(String[] args) {

        double array[] = {1.1, 2.2, 1.1, 3.2, 1.2, 2.1};

        double max = array[0];

        for (int i = 0; i < 6; i++) {

            if (max < array[i])

                max = array[i];

        }

        System.out.println("Максимальне число в масиві: " + max);

    }

}

Як бачимо, спочатку змінній max присвоюється значення нульового елемента масиву, після чого в циклі іде послідовне порівняння з кожним наступним числом до останнього. Якщо при порівнянні чергове значення в масиві більше за максимальне в змінній max, то змінній max присвоюється це значення. Як ви уже зрозуміли, після закінчення циклу у змінній max міститиметься максимальне значення, яке і буде виведене на консоль:

Максимальне число в масиві: 3.2

В Java масиви є об’єктами (про об’єкти ми вже говорили раніше), що забезпечує деяку додаткову функціональність масивам. Зокрема можна дізнатися довжину масиву наступним чином array.length. Для вищенаведеного прикладу можна замінити рядок з циклом таким чином:

        for (int i =0; i < array.length; i++){

 

Багатовимірні масиви по суті – це масив масивів.

 

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

int twoD[][] = new int [4][5]; //створення масиву 4x5

int threeD[][][] = new int[5][5][5]; //створення масиву 5х5х5

Для двовимірного лівий індекс означає номер рядка, а правий – номер стовпця. Це можна уявити наступним чином:

[0,0][0,1][0,2][0,3][0,4]

[1,0][1,1][1,2][1,3][1,4]

[2,0][2,1][2,2][2,3][2,4]

[3,0][3,1][3,2][3,3][3,4]

Тривимірний масив можна уявити у вигляді куба. Крім номера рядка і номера стовпця, додається ще індекс елемента вглибину.

 

Наступна програма створює масив 5 на 4, заповнює його випадковими числами і виводить на екран.

import java.util.Random;                    // імпортуємо клас Random

public class RandomArray {

    public static void main(String[] args) {

        int m = 5, n = 4; //оголошуємо та ініцілізуємо змінні з розмірами масиву

        int Array[][] = new int[m][n];    //оголошуємо та ініціалізуємо масив

        Random generator = new Random();  // створюємо генератор випадкових чисел

        int gn; //змінна, в яку буде записуватися згенероване генератором число

        /* заповнюємо масив випадковими числами */

        for (int i = 0; i < m; i++)             //проходимось по стовпцях

            for (int j = 0; j < n; j++) {        //проходимось по рядках

                gn = generator.nextInt(100);  //генерація випадкового числа від 0 до 100;

                Array[i][j] = gn;             //записуємо згенероване випадкове число

            }

        /* Виводимо результат */

        for (int i = 0; i < m; i++) {

            for (int j = 0; j < n; j++)         // зверніть увагу на відсутність фігурної дужки

                System.out.print(Array[i][j] + "    ");// рядок відноситься до масиву по j

            System.out.println();         //виводимо символи переводу каретки і нового рядка

            //після кожного проходження стовпцевих елементів рядка

        }

    }

}

В результаті на екрані одержимо:

94    47    65    0

99    20    60    69   

80    33    63    73   

35    50    48    81   

39    19    4    85

 

У наведеному прикладі в кожному рядку однакова кількість елементів (стовпців). У Java можна створити двовимірні масиви з різною кількістю елементів у рядках (зазубрені або зубчасті масиви, jagged arrays).

int twoD[][] = new int[5][]; //створюємо двовимірний масив з 5-ма рядками

twoD[0] = new int[5]; // виділяємо пам’ять для 5-ти елементів нульового рядка

twoD[1] = new int[4]; // перший рядок матиме 4 елементи

twoD[2] = new int[3]; // другий - 3

twoD[3] = new int[2]; // третій - 2

twoD[4] = new int[1]; // четвертий – 1

Використання таких нерівних (нерегулярних) масивів не рекомендується, оскільки з ними важче працювати і можна припуститися низки помилок, але в деяких ситуаціях вони можуть бути доволі корисними.

 

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

public class Array2 {

    public static void main(String[] args) {

        int[][] Array= {

            {5, 6, 1, 3},

            {3, 4, 2, 1},

            {1, 2, 2, 2}

         };

         for (int i = 0; i < 3; i++){

            for (int j = 0; j < 4; j++)

                 System.out.print (Array[i][j]+"    ");

            System.out.println();

        }

    }

}

Результат виконання:

5    6    1    3   

3    4    2    1   

1    2    2    2

 

3.3. Реалізація успадкування

 

Успадкування або наслідування (англ. inheritance) – це ще один важливий механізм об'єктно орієнтованого програмування, який дозволяє створювати нові класи на основі вже існуючих.

 

Клас, на базі якого створюється підклас, називають надкласом, суперкласом або ж батьківським класом. Новий клас, що розширює (extends) батьківський клас називають підкласом або дочірнім класом. На відміну від С++, де клас може мати кілька батьківських класів, мова програмування Java підтримує лише одинарне успадкування, тобто може бути лише один безпосередній надклас. У надкласу звичайно може бути свій надклас, проте також лише один безпосередній. Множинне успадкування доволі складне у застосуванні і вимагає обережного підходу, тому творці Java вирішили відмовитися від нього.

 

Припустимо, у вас є клас SimpleRoom, який містить поля width (ширина) та length (довжина) кімнати та методи виведення інформації про кімнату.

package ua.wikibooks.oj;

public class SimpleRoom {

    protected double width=0.0;

    protected double length=0.0;

    public SimpleRoom(double width, double length) {

        this.width=width;

        this.length=length;

        System.out.println("SimpleRoom створено");

    }   

    public void info (){

        System.out.println("Кімната: ширина = "+width+", довжина = "+length);

        System.out.println("Площа кімнати: "+width*length);

    }   

    public static void main(String[] args) {

        SimpleRoom s=new SimpleRoom(5, 5);

        s.info();

    }

}

Результат виконання:

SimpleRoom створено

Кімната: ширина = 5.0, довжина = 5.0

Площа кімнати: 25.0

 

Тепер нехай вам необхідний клас, який би містив ще й інформацію про висоту кімнати й обчислював ще й об'єм кімнати. Можна створити повністю новий клас, можна модифікувати вже існуючий клас, а можна створити клас, розширивши базовий клас SimpleRoom. Якщо клас SimpleRoom вже використовується в інших програмах, то змінювати його потрібно обережно. Для цього прикладу доведеться створити ще один конструктор, а не модифікувати існуючий. У складніших випадках може знадобитися набагато більше дій. Крім того, може бути, що клас SimpleRoom вже стандартизований, задокументований, його використовує чимало інших розробників і змінювати його просто так ви не маєте права. Тож виходом є створення нового класу під ваші потреби. Завдяки ж можливості успадкування, нам не потрібно повністю переписувати клас. На основі класу SimpleRoom можна створити новий клас SimpleRoom2. Для цього достатньо вказати ключове слово extends (що означає "розширює") і вказати назву батьківського класу.

 

Новий, дочірній клас отримує доступ до публічних і захищених полів та методів батьківського класу.

package ua.wikibooks.oj;

public class SimpleRoom2 extends SimpleRoom {

    protected double height;

    public SimpleRoom2(double w, double l, double h) {

        super(w, l);

        height=h;

        System.out.println("SimpleRoom2 створено");       

    }   

    public void info2(){

 System.out.println("Кімната: ширина = "+super.width+", довжина = "+super.length+", висота= "+this.height);

System.out.println("Площа кімнати: "+width*length); // якщо немає конфлікту з іменами, то можна і пропустити super

        System.out.println("Об'єм кімнати: "+width*length*height);

    }

    public static void main(String[] args) {

        SimpleRoom2 s2 = new SimpleRoom2(5, 5, 3);

        System.out.println("Метод info SimpleRoom");

        s2.info();

        System.out.println();

        System.out.println("Метод info2 SimpleRoom2");

        s2.info2();

    }

}

 

Розберемо вищенаведений приклад. При створенні класу ми зазначили, який клас ми розширюємо. В класі введене нове поле height. Далі ми створили конструктор, в якому іде звернення до батьківського конструктора. Якщо б не було конструкторів з параметрами, то неявні виклики конструкторів викликалися б у такій же послідовності. Спочатку викликається конструктор дочірнього класу, з нього викликається батьківський конструктор, створюється батьківський об'єкт, далі іде завершення конструктора дочірнього класу і створюється дочірній об'єкт. Для виклику конструктора суперкласу ми скористалися методом super з відповідними аргументами для батьківського конструктора:

super(w, l);

Також зверніть увагу як відбувається звернення до полів батьківського класу. Якщо поля не приватні, то вони доступні з дочірнього класу. Тож до них можна звертатися безпосередньо за іменем, або ж скористатися для доступу ключовим словом super (замість об'єктної змінної). Якщо б у нашому дочірньому класі існували б однойменні поля з батьківським класом (наприклад, і там, і там width), то поля батьківського класу були б доступні лише з допомогою super.

Результат виконання SimpleRoom2:

SimpleRoom створено

SimpleRoom2 створено

Метод info SimpleRoom

Кімната: ширина = 5.0, довжина = 5.0

Площа кімнати: 25.0

 

Метод info2 SimpleRoom2

Кімната: ширина = 5.0, довжина = 5.0, висота= 3.0

Площа кімнати: 25.0

Об'єм кімнати: 75.0

Зверніть увагу, що ми без проблем через об'єктну змінну класу SimpleRoom2 викликаємо метод info класу SimpleRoom:

s2.info();

Тож, крім полів, дочірньому класу також доступні неприватні методи батьківського класу.

 

Насправді у дочірньому класі (SimpleRoom2) можна було б створити однойменний клас info і він би замістив відповідний метод батьківського класу. Такий механізм так і називається – заміщення або перевизначення методів (англ. method overriding).

 

Інколи може виникнути необхідність заборонити можливість здійснення успадкування. Тобто можливість створення нових класів на базі певного класу. Тоді клас можна оголосити як final. Також можна заборонити заміщення методу у класах-нащадках, використавши при оголошенні методу той же модифікатор final. Як уже мабуть ви знаєте, цей же модифікатор final також застосовується для оголошення констант – змінних, які ініціалізуються лише раз.

Наприклад, у Java фінальним оголошено клас String.

 

Додаткові матеріали

1. Основи програмування на Java – безкоштовний відеокурс (Prometheus): https://courses.prometheus.org.ua/courses/EPAM/JAVA101/2016_T2/about

2. Java Professional – авторський відеокурс (Бабич О.В, ITVDN): https://www.youtube.com/watch?v=H77pBuf582M&list=PLvItDmb0sZw9DXLBDs4IBcalvA1Nx56o9

3. Основи програмування на Java – безкоштовний відеокурс (Prometheus): https://courses.prometheus.org.ua/courses/EPAM/JAVA101/2016_T2/about

4. Java Professional – авторський відеокурс (Бабич О.В, ITVDN): https://www.youtube.com/watch?v=H77pBuf582M&list=PLvItDmb0sZw9DXLBDs4IBcalvA1Nx56o9

5. Основи програмування на Java – безкоштовний відеокурс (Prometheus): https://courses.prometheus.org.ua/courses/EPAM/JAVA101/2016_T2/about

6. Java Professional – авторський відеокурс (Бабич О.В, ITVDN): https://www.youtube.com/watch?v=H77pBuf582M&list=PLvItDmb0sZw9DXLBDs4IBcalvA1Nx56o9

 

 

Попередня тема

На початок

Наступна тема