Закрепим пройденную теорию на одном практическом примере, показывающем типичное применение полиморфизма подтипов.
Представьте себе задачу расчета стоимости туристической страховки. Это страховка, которую желательно купить при выезде за границу, на случай внезапных болезней или травм.
Сумма страховки зависит от большого числа факторов. Некоторые из факторов могут влиять на сам процесс подсчета стоимости, то есть они изменяют не значения в формуле подсчета, а саму формулу.
Конкретно в случае страховок, скорее всего есть одна большая формула, куда подставляются значения и вычисление происходит за один заход. Для нас сейчас главное сама концепция, а не точное знание внутренностей страхового бизнеса.
Если решать эту задачу в лоб, то она будет выглядеть как большое месиво вычислений со множеством условных конструкций. Со временем такой код становится крайне тяжелым для восприятия из-за большого числа состояний, которые надо удерживать в голове.
// Примеры с потолка
if (age < 18) {
let cost = salary * age;
if (country === 'uganda') {
cost = cost / 2;
}
} else if (age >= 18 && age < 24) {
// ...
}
Можно ли как-то сделать код понятнее и проще для восприятия? Иногда да. Из всех факторов, участвующих в расчете, нужно попытаться найти те, которые влияют на вычисление глобально. Они проявляются как глобальный if
на верхнем уровне. Предположим, что в случае страховки это возраст. То есть считаем, что возраст определяет формулу расчета стоимости страховки. Следующим шагом смотрим на ветки в этой условной конструкции и то, какие диапазоны там указаны. Допустим такую картину:
- До 18
- От 18 до 24
- От 24 до 65
- Старше 65
Обратите внимание на важную деталь. Выше мы договорились, что каждая возрастная группа определяет алгоритм расчета стоимости страховки. То есть они между собой независимы, хотя и сам процесс вычисления местами может быть схож (и будет скорее всего).
Теперь делаем переход от логической структуры к коду. Каждая возрастная группа — класс, отвечающий за вычисления стоимости для этой группы:
class LessThan18 {
// Параметры, это те самые факторы, по которым строится вычисление
calculate(params) {
// тут считаем и возвращаем результат
}
}
// Имя, конечно, так себе, в реальном коде стоит придумать что-нибудь говорящее
class MoreThan18AndLessThan24 {
// Структура параметров должна 100% совпадать с остальными классами,
// так как только в этом случае возможен полиморфизм
calculate(params) {
// тут считаем и возвращаем результат
}
}
// Остальные классы
Главное, что мы получили — разделили процесс вычисления на независимые блоки кода, которые проще для восприятия. Каждый такой класс называется стратегией (вычисления). Очень важно то, что стратегия не является абстракцией, объектом с состоянием и временем жизни объекта. Поэтому данные передаются не в конструктор, а в сам метод. По сути, это обычная функция (вычисление), которая упакована в класс только с одной целью — получить полиморфизм подтипов. Все то же самое можно сделать, используя диспетчеризацию функций по ключам, при этом код будет проще.
Дальше возникает вопрос, а каким образом и где выбрать правильную реализацию, с которой нужно работать? Вариантов здесь несколько. Выбор реализации может быть делегирован внешнему коду, то есть если мы применяем инверсию зависимостей, то работаем уже с готовой стратегией:
calculateCost(strategy, params) {
strategy.calculate(params);
}
Пока мы только ушли от проблемы, но не решили ее. В любом случае, где-то будет код, который содержит либо условную конструкцию, либо реализует один из способов диспетчеризации, которые мы разобрали в предыдущих уроках. В самом простом случае этот код будет выглядеть так:
chooseCostInsuranceStrategy(user) {
if (user.getAge() < 18) {
return new LessThan18();
} else if (/* ... */) {
// some code
}
}
strategy = chooseCostInsuranceStrategy(user);
strategy.calculate(params);
Как видно по примерам выше, кода с использованием стратегии будет больше, но не так много, если используется диспетчеризация функций по ключам в ассоциативном массиве. Это касается фактически всех ситуаций, в которых задействован полиморфизм подтипов в JavaScript. Это цена, которую придется заплатить за разделение, упрощающее расширение кода и уменьшающее его сложность. С другой стороны, очень легко попасть в ловушку и, наоборот, сделать сложность кода выше, чем она была до внедрения полиморфизма подтипов. Этот полиморфизм делает код многословным и излишне абстрактным, если применять его налево и направо. А расширение нужно не так часто, как об этом говорят. Более того, инвертировать зависимости можно по ходу действия, когда в этом появляется необходимость.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.