В этом уроке описывается система, которая помогает правильно организовывать код, построенный на классах.
В языках, где ООП построено без инкапсуляции, подобные проблемы решаются проще и возникают реже. Если хочется узнать как это бывает, попробуйте пописать код на Clojure или Elixir.
Предположим, что мы делаем сайт, имеющий механизм аутентификации. После ее выполнения пользователю выводится приветствие, которое строится по-разному в зависимости от возраста пользователя. Если пользователю не исполнилось 18, то пишется одно, всем остальным — другое.
В данном случае реализация в лоб через if будет лучшим решением задачи. Но в этом уроке мы отрабатываем использование полиморфизма в рамках классовой модели, поэтому пойдем другим путем. Сама задача специально упрощена, чтобы не тратить время на ее анализ.
Первый порыв у многих разработчиков — ввести два класса: Under18
и Above18
. Дальше в каждом из классов добавить по методу getMessage()
. В итоге мы получили полиморфизм подтипов:
// Классы Under18 и Above18 наследуют свойства и методы User
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.lastName}`;
}
}
// Где-то в шаблоне
// Правильный класс для пользователя выбирается на момент начала обработки 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.lastName}`;
}
}
Как пользователь будет взаимодействовать с объектами этих классов? Варианта два: либо мы передаем его в конструктор, либо в сам метод getMessage(user)
. Что правильнее? Всегда пытайтесь понять, имеем ли мы дело с абстракцией данных или нет. С самим пользователем все понятно. Пользователь — это абстракция данных, у него есть уникальность (все пользователи отличаются) и время жизни. А вот вывод сообщения — это операция без состояния. Само наличие класса и объекта для него обусловлено желанием получить полиморфизм подтипов и ничем более. Поэтому в данном примере лучше передавать пользователя через метод:
// Где-то в шаблоне
= greeting.getMessage(user)
За кадром остался вопрос выбора и создания соответствующего объекта. За это отвечает фабрика, которая вызывается где-то до формирования вывода из шаблона.
const buildGreetingObject = (user) => {
if (user.getAge() < 18) {
return new GreetingForUnder18();
} else {
return new GreetingForAbove18();
}
}
Главное в этой схеме то, что пользователь остался пользователем. Он по-прежнему отвечает только за логику ядра приложения. Даже если добавятся новые условия вывода сообщения и наши два класса превратятся в 10 классов (потому что 10 вариантов вывода в зависимости от разных параметров), то это никак не повлияет на пользователя.
Что еще более важно, при появлении новых задач, не связанных с выводом сообщения, пользователь по-прежнему не будет затронут. Например, мы захотим отправлять письма разным пользователям после регистрации. В зависимости от количества видов писем, будет создано такое же количество классов. Принцип работы останется таким же. Фабрика, выбор нужного типа в начале процесса регистрации и полиморфное поведение при отправке письма.
Внимательный читатель заметит, что результат подозрительно похож на стратегию. Как ни странно, это и есть стратегия.
В итоге, в коде появляется большое количество небольших классов. Количество этих классов равно количеству возможных вариантов поведения. Большинство объектов этих классов не имеют своего состояния и нужны для организации полиморфного кода.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.