Как стать автором
Обновить
827.91
OTUS
Цифровые навыки от ведущих экспертов

If-else-switch

Время на прочтение 4 мин
Количество просмотров 4.3K
Автор оригинала: Robert C. Martin

Несколько дней назад кто-то задал вопрос в твиттере, о том, какой из PHP-сниппетов  лучше других, и есть ли подход лучше.

Я написал свой ответ в Твиттере в следующем абзаце.

Поместите if/else в фабрику объектов (factory object), которая создает полиморфный объект для каждого варианта. Создайте фабрику в 'main' и перенесите ее в ваше приложение. Тогда цепочка if/else будет повторяться только один раз.

После этого меня попросили привести пример. Твиттер — не лучшее средство для этого, поэтому...

Во-первых, если единственное намерение программиста — это перевести:

0->'male', 
1->'female' 
otherwise -> 'unknown'

...тогда рефакторинг №2 будет моим предпочтением.

Однако мне трудно поверить, что бизнес-правила системы не используют этот гендерный код для принятия решений. Я боюсь, что цепочка if/else/switch, о которой спрашивали, повторяется во многих других местах кода. Некоторые из операторов if/else/switch могут включать целое число, а другие — строку. Не исключено, что вы найдете if/else/switch, которые в одном случае используют целое число, а в другом — строку!

Быстрое распространение операторов if/else/switch является распространенной проблемой в программных системах. Тот факт, что они повторяются во многих местах, является проблематичным, поскольку при неизбежном изменении таких операторов легко пропустить некоторые из них. Это приводит к хрупкости систем (fragile systems).

Но есть и более серьезная проблема с операторами if/else/switch. Это структура зависимостей.

Такие операторы, как правило, имеют случаи, указывающие на модули более низкого уровня. Это часто означает, что модуль, содержащий if/else/switch, будет иметь зависимости исходного кода от модулей более низкого уровня.

Это достаточно плохо. Нам не нравятся зависимости, идущие от модулей высокого уровня к модулям низкого уровня. Они препятствуют нашему желанию создавать архитектуры, состоящие из независимо развертываемых компонентов (deployable).

Однако приведенная выше диаграмма показывает, что дело обстоит еще хуже. Другие модули более высокого уровня, как правило, зависят от модулей, содержащих операторы if/else/switch. Таким образом, модули более высокого уровня имеют транзитивные зависимости от модулей более низкого уровня. Это превращает операторы if/else/switch в магниты зависимостей, которые охватывают большие участки исходного кода системы, превращая систему в жесткую единую монолитную архитектуру без гибкой структуры компонентов.

Решение этой проблемы заключается в том, чтобы разорвать эти внешние зависимости на модулях нижнего уровня. Это можно сделать с помощью простого полиморфизма.

На приведенной выше диаграмме можно увидеть, что модули высокого уровня используют интерфейс базового класса, который полиморфно распространяется на детали низкого уровня. Немного поразмыслив, можно понять, что поведенчески это идентично if/else/switch, но с некоторыми особенностями. Решение о том, какому случаю следовать, должно быть принято до того, как высокоуровневые модули задействуют интерфейс базового класса.

Мы вернемся к when через некоторое время, когда будет принято решение. Пока же просто посмотрите на направление зависимостей. Больше нет никакой транзитивной зависимости исходного кода от модулей высокого уровня к модулям низкого уровня. Мы можем легко создать границу компонента, разделяющую их. Мы даже можем развернуть (deploy) модули высокого уровня независимо от модулей низкого уровня. Это обеспечивает приятную гибкость архитектуры.

Еще один момент, который следует учитывать, заключается в том, что реализация if/else/switch и полиморфная реализация используют поиск в таблице для выполнения своей работы. В случае if/else поиск в таблице является процедурным. В случае switch большинство компиляторов создают небольшую таблицу поиска. В случае полиморфной диспетчеризации векторная таблица встроена в интерфейс базового класса, таким образом, что все три имеют очень похожие характеристики времени выполнения и памяти, при этом один из них не намного быстрее другого.

Так где же принимается решение? Решение принимается, когда создается экземпляр базового класса. Надеюсь, это происходит в хорошем безопасном месте, например в main. Обычно мы управляем этим с помощью класса фабрики.

На приведенной выше диаграмме видно, что модуль высокого уровня использует базовый класс для выполнения своей работы. Каждое бизнес-правило, которое раньше зависело от оператора if/else/switch, теперь имеет свой собственный метод вызова в базовом классе. Когда бизнес-правило вызывает этот метод, оно развертывается (deploy) до соответствующего модуля низкого уровня. Модуль низкого уровня создается Factory. Модуль высокого уровня вызывает метод make(x) Factory, передавая некий маркер x, который представляет решение. FactoryImpl содержит единственный оператор if/else/switch, который создает соответствующий экземпляр и передает его обратно в модуль высокого уровня, который затем вызывает его.

Еще раз обратите внимание на направление зависимостей. Видите эту красную линию? Это удобная граница компонента. Все зависимости пересекают ее, указывая на модули более высокого уровня.

Будьте осторожны с маркером x. Не пытайтесь сделать его enum или чем-то, что требует объявления над красной линией. Лучше выбрать целое число или строку. Это может быть небезопасно для типов. Действительно, он не может быть безопасным для типов. Но это позволит вам сохранить компонентную структуру вашей архитектуры.

Возможно, вас больше волнует другой вопрос. Этот базовый класс должен иметь метод для каждого бизнес-правила, которое когда-то зависело от решения if/else/switch. По мере появления новых бизнес-правил вам придется добавлять все больше методов в базовый класс. А поскольку многие бизнес-правила уже зависят от базового класса, их придется перекомпилировать/переразвернуть, даже если ничего в них не изменилось.

Существует множество способов решения этой проблемы. Я мог бы продолжать этот блог еще на 2000 слов или около того, описывая их. Чтобы избежать этого, я предлагаю вам ознакомиться с принципом разделения интерфейсов (The Interface Segregation Principle) и паттерном Acyclic Visitor.


Материал подготовлен в рамках курса «Архитектура и шаблоны проектирования». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн.

Регистрация здесь.

Теги:
Хабы:
0
Комментарии 8
Комментарии Комментарии 8

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS