Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос нашим менторам. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете
JS: Полиморфизм

Объектная композиция

В этом уроке описывается система, которая помогает правильно организовывать код, построенный на классах.

В языках, где ООП построено без инкапсуляции, подобные проблемы решаются проще и возникают реже. Если хочется узнать как это бывает, попробуйте пописать код на Clojure или Elixir.

Предположим, что мы делаем сайт, имеющий механизм аутентификации. После её выполнения пользователю выводится приветствие, которое строится по-разному в зависимости от возраста пользователя. Если пользователю не исполнилось 18, то пишется одно, всем остальным — другое.

В данном случае, реализация в лоб, через if, будет лучшим решением задачи. Но в этом уроке мы отрабатываем использование полиморфизма в рамках классовой модели, поэтому пойдём другим путём. Сама задача специально упрощена, чтобы не тратить время на её анализ

Первый порыв у многих разработчиков ввести два класса: Under18 и Above18. Дальше в каждом из классов добавить по методу getMessage(). В итоге мы получили полиморфизм подтипов:

class Under18 extends User {
  getMessage() {
    // Hi Sam
    return `Hi ${this.firstName}`;
  }
}

class Above18 extends User {
  getMessage() {
    // Hello Mr Smith
    // Hello Mrs Tomson
    return `Hello ${this.appeal} ${this.firstName}`;
  }
}
// Где-то в шаблоне
// Правильный класс для пользователя выбирается на момент начала обработки http-запроса -.
= user.getMessage()

Это решение хоть и работает, но ведёт не по тому пути. Сегодня у нас до 18 и после, потом появится отдельное поведение для тех кто старше 65. Всё станет ещё хуже, когда кроме этих разделений появится дополнительное разделение по полу. В таком случае мы получим большое число комбинаций, под каждую из которых придётся создать отдельный класс пользователя:

  • девушки старше 18
  • девушки младше 18
  • парни старше 18
  • парни младше 18
  • ...

В книжках по паттернам любят приводить пример с разделением средств передвижения по типам: плавающие, летающие и ездящие. А потом, внезапно оказывается, что некоторые одновременно и плавают и ездят.

Теперь попробуем ответить на вопрос, почему эту задачу не надо решать подтипами в любом случае. Сам по себе пользователь — это сущность, взятая из нашей предметной области. Предметная область и вывод текста на экран — это совершенно разные вещи. Второе относится к логике приложения, но не бизнес-логике. Если об этом не задумываться, то в конце концов настанет момент, когда внутри пользователя окажется вообще всё, что только происходит на сайте, ведь оно всё так или иначе связано с самим пользователем. И мы получим божественный объект.

Правильное решение в таких ситуациях построено на композиции — подходе, основанном на взаимодействии объектов, а не на иерархии классов. Начнём сначала. В нашей задаче есть две ситуации: пользователи до 18 лет и пользователи старше. Это два разных варианта поведения, которые будут описываться двумя разными классами. Назовем их: GreetingForAbove18 и GreetingForUnder18. В каждом из классов реализуем метод getMessage. В каждом из классов этот метод будет возвращать именно то приветствие, которое требуется для этой категории пользователей.

class GreetingForUnder18 {
  getMessage(user) {
    return `Hi ${user.firstName}`;
  }
}

class GreetingForAbove18 {
  getMessage(user) {
    return `Hello ${user.appeal} ${user.firstName}`;
  }
}

Как пользователь будет взаимодействовать с объектами этих классов? Варианта два, либо мы передаём его в конструктор, либо в сам метод getMessage(user). Что правильнее? Всегда пытайтесь понять, имеем ли мы дело с абстракцией данных или нет. С самим пользователем всё понятно. Пользователь это абстракция данных, у него есть уникальность (все пользователи отличаются) и время жизни. А вот вывод сообщения — это операция без состояния. Само наличие класса и объекта для него обусловлено желанием получить полиморфизм подтипов и ничем более. Поэтому в данном примере лучше передавать пользователя через метод:

// Где-то в шаблоне
= greeting.getMessage(user)

За кадром остался вопрос выбора и создания соответствующего объекта. За это отвечает фабрика, которая вызывается где-то до формирования вывода из шаблона.

const buildGreetingObject = (user) => {
  if (user.getAge() < 18) {
    return new GreetingForUnder18();
  } else {
    return new GreetingForAbove18();
  }
}

Главное в этой схеме то, что пользователь остался пользователем. Он по-прежнему отвечает только за логику ядра приложения. Даже если добавятся новые условия вывода сообщения и наши два класса превратятся в 10 классов (потому что 10 вариантов вывода в зависимости от разных параметров), то это никак не повлияет на пользователя.

Что ещё более важно, при появлении новых задач, не связанных с выводом сообщения, пользователь по-прежнему не будет затронут. Например, мы захотим отправлять письма разным пользователям после регистрации. В зависимости от количества видов писем, будет создано такое же количество классов. Принцип работы останется таким же. Фабрика, выбор нужного типа в начале процесса регистрации и полиморфное поведение при отправке письма.

Внимательный читатель заметит, что результат подозрительно похож на стратегию. Как ни странно, это и есть стратегия.

В итоге, в коде появляется большое количество небольших классов. Количество этих классов равно количеству возможных вариантов поведения. Большинство объектов этих классов не имеют своего состояния и нужны для организации полиморфного кода.


Дополнительные материалы

  1. Архитектура и ООП
  2. Принцип разделения интерфейса

<span class="translation_missing" title="translation missing: ru.web.courses.lessons.mentors.mentor_avatars">Mentor Avatars</span>

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят менторы из команды Хекслета или другие студенты.

Для полного доступа к курсу нужна профессиональная подписка

Профессиональная подписка откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, даст возможность обращаться за помощью к менторам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Зарегистрироваться

или войти в аккаунт

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг».