Для тих, хто почитав визначення поліморфізму і реалізував кілька прикладів з інтерфейсами, але не зрозумів, навіщо він потрібен.
Є звички погані, є — хороші, є звички індивідуальні, є — розповсюджені. Але яка б розповсюджена звичка не була, кожна людина реалізує її зі своїми нюансами.
Наприклад, моя улюблена звичка — спати. Усі люди сплять по-різному, і в родині Іванових — це теж має місце бути.
Тато спить на спині та хропе, а Мама спить на правому боці і відштовхується.
Перенесемо сонне царство у світ Java. Знаючи сенс інтерфейсів, звичка спати буде такою:
Застосуємо поліморфізм і подивимося, що змінилося:
Дядечко Боб каже: щоб краще зрозуміти, перепишіть код в IDEA і позапускайте.
Поліморфізм автоматично визначає конкретний тип об'єктів із загальним предком. Нам не потрібно писати перевірки на те - яким типом є об'єкт.
Резюме для трьох статей:
Тут описані найочевидніші (для мене) приклади використання інтерфейсів, успадкування і поліморфізму. Існують й інші світи.
Загалом моя основна думка: «Додаток – це реалізація (абстракція) реального світу, а оскільки світ постійно змінюється, то і додаток постійно піддається змінам, неможливо написати раз і назавжди. Процес внесення змін у додаток може бути довгим і незрозумілим чи швидким і зрозумілим. Це багато в чому залежить від організації коду, від організації класів, від дисциплінованого слідування правилам.»
Поняття розширюваності та внесення змін піднімають уявлення про програмування на новий рівень. Можливо, якщо розглядати ООП через розширюваність та внесення змін, можна швидше зрозуміти це саме ООП.
Наступними вершинами після ООП стануть: SOLID, чистий код, архітектура додатків і патерни проєктування. Їх також можна розуміти через розширюваність та внесення змін.
Тато спить на спині та хропе, а Мама спить на правому боці і відштовхується.
Перенесемо сонне царство у світ Java. Знаючи сенс інтерфейсів, звичка спати буде такою:
public interface ЗвичкаСпати {
String якСпить();
}
public class Тато implements ЗвичкаСпати {
@Override
public String якСпить() {
return "Тато спить на спині та хропе";
}
}
public class Мама implements ЗвичкаСпати {
@Override
public String якСпить() {
return "Мама спить на правому боці і відштовхується";
}
}
Щоб точно сказати, хто як спить, треба підійти до нього і подивитися, хто це тут спить — і потім ми точно зможемо дізнатися, чи почуємо ми храп, чи отримаємо поштовх ногою. Підходити будемо у випадковому порядку, то до Тата, то до Мами.
У класі з методом main створимо метод, який буде у випадковому порядку повертати нам то Тата, то Маму.
public class Спальня {
public static void main(String[] args) {
}
public static Object подивитисяХтоСпить() {
int a = 1 + (int) (Math.random() * 2);
if (a == 1) {
return new Мама();
}
if (a == 2) {
return new Тато();
}
return null;
}
}
Ми не знаємо наперед, кого саме поверне метод подивитисяХтоСпить(), тому тип об'єкта, що повертається, буде загальним для всіх — Object.
Щоб перевірити, як метод працює, у main запишемо конструкцію, яка викличе перевірений метод 10 разів і виведе клас отриманого об'єкта:
for (int i = 0; i < 10; i++) {
Object випадковий = подивитисяХтоСпить();
System.out.println(випадковий.getClass());
}
При запуску у консоль виведеться випадкова кількість Мам і Тат.
class Тато
class Тато
class Мама
class Тато
class Тато
class Мама
class Мама
class Мама
class Тато
class Мама
Але повернемося до сну. Нам потрібно розуміти, хто як спить.
Метод подивитисяХтоСпить() поверне випадковий об'єкт, і записати ось так:
Object випадковий = подивитисяХтоСпить();
System.out.println(випадковий.якСпить());
У нас не вийде.
Тому що змінна випадковий має тип Object, а у Object немає такого методу. Він є лише у Тата або Мами, але кого це зупиняє? Зараз зробимо приведення типу Object до Тата або Мами, в залежності від отриманого класу, і все готово.
Пишемо (хочете switch, хочете if, я вибрав if):
for (int i = 0; i < 10; i++) {
Object випадковий = подивитисяХтоСпить();
if (випадковий.getClass().equals(Мама.class)) {
Мама мама = (Мама) випадковий;
System.out.println(мама.якСпить());
}
if (випадковий.getClass().equals(Тато.class)) {
Тато тато = (Тато) випадковий;
System.out.println(тато.якСпить());
}
}
На виході отримаємо відмінний результат зі сплячими Мамами і Татами, справа закрита!
Тато спить на спині та хропе
Тато спить на спині та хропе
Мама спить на правому боці і відштовхується
Тато спить на спині та хропе
Тато спить на спині та хропе
Тато спить на спині та хропе
Тато спить на спині та хропе
Мама спить на правому боці і відштовхується
Тато спить на спині та хропе
Мама спить на правому боці і відштовхується
І так би воно й було, якби не постійно мінливий світ. У родині Іванових є ще двоє дітей, вони теж сплять кожен по-своєму, їх треба перенести у Java світ. А якщо в гості до Іванових приїдуть Петрови зі своїми трійнятами, і вони також сплять кожен по-своєму? З розширенням програми новими людьми-класами, які вміють спати, метод main перетвориться на вавилонську вежу зі сотень умов.
for (int i = 0; i < 10; i++) {
Object випадковий = подивитисяХтоСпить();
if (випадковий.getClass().equals(Мама.class)) {
Мама мама = (Мама) випадковий;
System.out.println(мама.якСпить());
}
if (випадковий.getClass().equals(Тато.class)) {
Тато тато = (Тато) випадковий;
System.out.println(тато.якСпить());
}
//тут ще мільйон рядків коду
}
Тобто для розширення програми така організація класів зовсім не підходить. Вона змушує нас писати багато однотипного, переважно повторюваного коду.
Допоможе нам лише батько всього ООП, найсвітліший князь гнучкості — Поліморфізм.
Застосуємо поліморфізм і подивимося, що змінилося:
public class Спальня {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
ЗвичкаСпати випадковий = подивитисяХтоСпить();
System.out.println(випадковий.якСпить());
}
}
public static ЗвичкаСпати подивитисяХтоСпить() {
//тут все без змін
}
}
У методі подивитисяХтоСпить() змінився тип, що повертається, був загальний для всіх клас Object, став загальний лише для Тата і Мами інтерфейс ЗвичкаСпати.
Це значить, що явно змінювати типи з Object на Мама або Тато уже не треба, перевіряти тип вхідного класу також уже не треба. Можна додати хоч +100500 людей-класів із звичкою спати, але метод main залишиться незмінним.
Відволічемося.
Особисто у мене є думка, що об'єктно-орієнтоване програмування дуже сильно схоже на складання текстів. Іменники більше підходять для класів, дієслова для методів, прикметники для полів класів.
Наприклад, речення: «Червоний автомобіль їде.» можна переписати в код:
public class Автомобіль {
String колір = «Червоний»;
public void їхати() {
System.out.println(колір + « автомобіль їде.»)
}
}
Можна й код переписати як речення, наприклад:
ЗвичкаСпати випадковий = подивитисяХтоСпить();
System.out.println(випадковий.якСпить());
На людському буде: «Виведи в консоль, як спить випадковий ЗвичкаСпати».
Не надто по-людськи, правильніше було б «ЗвичкаСпати» замінити на «Людина».
Можна змінити назву інтерфейсу ЗвичкаСпати на Людина, тоді логіка з'явиться: є загальна Людина, і у неї є метод якСпить().. Але знаючи про проблеми з різними звичками з попередньої статті про успадкування, правильніше створити інтерфейс Людина і успадкуватися від інтерфейсу ЗвичкаСпати. А класам Тато і Мама імплементувати інтерфейс Людина. Тоді все виглядатиме логічно:
Звичка спати оголошує метод як спить
public interface ЗвичкаСпати {
String якСпить();
}
Людина теж виглядає як людина зі звичкою спати.
public interface Людина extends ЗвичкаСпати {
}
Тато і Мама — люди
public class Тато implements Людина {
@Override
public String якСпить() {
return "Тато спить на спині і хропе";
}
}
public class Мама implements Людина {
@Override
public String якСпить() {
return "Мама спить на правому боці і штовхається";
}
}
І в методі main теж все норм, в консоль виводиться "Як спить випадкова людина":
public class Спальня {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Людина випадкова = подивитисяХтоСпить ();
System.out.println(випадкова.якСпить());
}
}
public static Людина подивитисяХтоСпить() {
//тут все без змін
}
}
Резюмує за мене доктор Боб Келсо.
Дядечко Боб каже: щоб краще зрозуміти, перепишіть код в IDEA і позапускайте.
Поліморфізм автоматично визначає конкретний тип об'єктів із загальним предком. Нам не потрібно писати перевірки на те - яким типом є об'єкт.
Резюме для трьох статей:
Тут описані найочевидніші (для мене) приклади використання інтерфейсів, успадкування і поліморфізму. Існують й інші світи.
Загалом моя основна думка: «Додаток – це реалізація (абстракція) реального світу, а оскільки світ постійно змінюється, то і додаток постійно піддається змінам, неможливо написати раз і назавжди. Процес внесення змін у додаток може бути довгим і незрозумілим чи швидким і зрозумілим. Це багато в чому залежить від організації коду, від організації класів, від дисциплінованого слідування правилам.»
Поняття розширюваності та внесення змін піднімають уявлення про програмування на новий рівень. Можливо, якщо розглядати ООП через розширюваність та внесення змін, можна швидше зрозуміти це саме ООП.
Наступними вершинами після ООП стануть: SOLID, чистий код, архітектура додатків і патерни проєктування. Їх також можна розуміти через розширюваність та внесення змін.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ