Найти тему
Сделай игру

Как обобщённые программные решения ведут проблемам

Оглавление

Программисты, не секрет, любят обобщать, но иногда, это превращается в источник постоянной головной боли. Желание решить задачу в общем виде, нередко, воспринимается как уровень мастерства (чем меньше кода и больше абстракций тем он, вроде как, выше). Но "мастерство" и решение, понятное и поддерживаемое хуже обфусцированного кода - это не одно и то же.

Когда выбранное решение себя не оправдало
Когда выбранное решение себя не оправдало

Есть и у меня подходящая история.

В чём суть обобщения

Бывает условно-типовая задача. Скажем, открытие модального окна и, когда становится понятно, что она используется довольно часто - возникает соблазн сделать её решение в виде отдельного модуля. Например, написать функцию с параметрами, открывающую и настраивающую модальное окно.

И это, в принципе, довольно хороший подход: сильно сокращает время на повторное использование кода.

Однако бывает и иначе

А бывают случаи, когда задача весьма объёмна и попытка решить её в общем виде предполагает построение некоторой архитектуры, эдакой иерархии, в которой есть определённый вид правил, которым подчиняются все внутренние компоненты. Например, строго определённый способ вывода данных, описанный самом высоком уровне.

Так в чём проблема?

Проблема возникает не сразу, проблема возникает потом. В какой-то момент обязательно потребуется выполнить это общее действие немного по-другому (скажем, вывести на строку, а несколько строк, сделать не GET, а POST запрос и всё в таком роде) и тут же выясняется, что это сделать крайне не просто, так как существующий механизм на это не рассчитан, а менять его долго, дорого и опасно.

Расскажу, как было у нас.

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

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

И, хотя с точки зрения UX это не особо хорошо, всех всё устраивало. До какого-то момента, пока не появился запрос для одной из таблиц не сбрасывать номер страницы.

И тут выяснилось, что обобщающее решение не позволяет внести изменения таким образом, чтобы не затронуть все таблицы компонента, где изменений никто не просил.

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

Разбираем проблему

Итак, мы решаем некоторую задачу и видим, что определённые действия повторяются раз за разом и выглядят одинаково - появляется соблазн их обобщить и написать меньше кода.

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

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

Наверное, появится соблазн написать функцию, в которую передаётся параметр, уточняющий, какие именно данные надо предоставить. Однако такой подход идёт в разрез с рекомендацией принципа SOLID - S - единой ответственности.

Одна функция должна отвечать за одно действие. Если следовать рекомендации, то должно быть 3 функции: по одной для каждого из отделов.

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

Так, опять-таки, делать нельзя! И вот почему: в какой-то момент бухгалтерия может изменить формат используемых данных и начать получать данные в ином формате (например, изменится формат выходного значения).

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

Более широкий случай

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

Так как же было необходимо сделать?

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

Разрабатывая какой-то функционал, который будет широко использоваться внутри приложения - всегда стоит заложить некоторый простор на возможные изменения:

  • В рамках модуля (или приложения) может быть одна главная (обобщающая) функция, но вся прочая функциональность должна быть разделяемой;
  • Использование методов должно быть явным, то есть любое действие должно проходить через явный вызов функции, просмотр которой должен приводить к пониманию всей цепочки вызовов;
  • Неявного исполнения следует избегать (например, пользоваться библиотекой, в которую в одном месте устанавливаются обработчики, в другом вызываются триггеры, не позволяющие получить ассоциацию с обработчиками);
  • Делать вызовы явными (вместо вызова функции, которая каскадно обновляет структуру данных приложения, через это, неявно, изменяя текущие данные - лучше вызывать функцию, исполнение которой возвращает обновлённые данные).

Заключение

Как известно, техника безопасности пишется кровью; техника же эффективной работы - набором допущенных просчётов и ошибок.

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

Использование "теневого функционала", то есть вызов функций, использование которых не очевидно в контексте задачи (например, управление состоянием относительно большого приложения через внешнюю библиотеку) приводит не только к сложности поддержки, но и созданию сильной связанности, препятствующей развитию приложения.

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