User Павел
Павел
11 уровень

Зачем нужен полиморфизм?

Статья из группы Java Developer
Зачем нужны интерфейсы? Зачем нужно наследование интерфейсов? Зачем нужен полиморфизм? Для тех, кто почитал определение полиморфизма и реализовал несколько примеров с интерфейсами, но не понял, зачем он нужен. Есть привычки плохие, есть – хорошие, есть привычки индивидуальные, есть — распространенные. Но какая бы распространённая привычка не была, каждый человек ее делает со своими нюансами. Например, моя любимая привычка - спать. Все люди спят по-разному, и семье Ивановых – это тоже имеет место быть. Зачем нужен полиморфизм? - 1Папа спит на спине и похрапывает, а Мама спит на правом боку и пинается. Перенесем сонное царство в мир 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)) {
        Мама мама = (Мама) random;
        System.out.println(мама. какСпит());
    }

    if (неизвестный.getClass().equals(Папа.class)) {
        Папа папа = (Папа) random;
        System.out.println(папа. какСпит());
    }

//тут еще миллион строк кода

}
То есть для расширения программы, такая организация классов совсем не подходит. Она заставляет нас писать много однообразного, в большей части повторяющегося кода. Поможет нам только отец всея ООП, наисветлейший князь гибкости — Полиморфизм. Зачем нужен полиморфизм? - 2Применим полиморфизм и посмотрим, что изменилось:

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 Человек посмотретьКтоСпит() {
   //тут все без изменений
  }
}
Резюмирует за меня доктор Боб Келсо. Зачем нужен полиморфизм? - 3Дядюшка Боб говорит: что для лучшего понимания перепишите код в IDEA и по запускайте. Полиморфизм автоматически определяет конкретный тип объектов с общим предком. Нам не нужно писать проверки на то - каким типом является объект. Резюме для трех статей: Тут описаны самые очевидные (для меня) примеры использования интерфейсов, наследования и полиморфизма. Существуют и другие миры. Вообще моя основная мысль: «Приложение – это реализация (абстракция) реального мира, а так как мир постоянно меняется, то и приложение постоянно подвержено изменениям, нельзя написать раз и на всегда. Процесс внесения изменений в приложение может быть долгим и не понятным или быстрым и понятным. Это во многом зависит от организации кода, от организации классов, от дисциплинированного следования правилам.» Понятия расширяемости и внесения изменений, поднимают представление о программировании на новый уровень. Возможно, если рассматривать ООП через расширяемость и внесение изменений, можно быстрее понять это самое ООП. Следующими вершинами после ООП станут: SOLID, чистый код, архитектура приложений и паттерны проектирования. Их так же можно понимать через расширяемость и внесение изменений.
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Дмитрий Уровень 11, Екатеринбург, Россия
23 ноября 2021
сначала не понял а потом каааак понял =)