Привет Хабр!
В данной статье я планирую развить тему важности умения "Программировать на уровне интерфейсов", а именно обсудить направление зависимостей. Это достаточно важная тема, так как только осознавая направленность зависимостей, можно спроектировать действительно гибкое и масштабируемое приложение (Данная статья является расшифровкой видео).
Что такое зависимость?
Но давайте все по порядку. Начнем с того как выглядят в наших проектах зависимости - это обычный import
какого-то модуля.
// a.js
import b from './b'
Здесь мы видим, что файл a.js
зависит от файла b.js
. A может ли теперь b.js
зависеть от a.js
?
Большинство из вас сразу же ответят конечно же нет. У этой проблемы даже есть название Circular Dependencies
или круговые зависимости. И есть даже webpack
плагин, который помогает вам найти Circular Dependency
в вашем проекте. Зачастую они могут быть не совсем очевидными, круг замыкается через 5-6 импортов.
Почему это проблема для архитектуры?
Но это все проблемы для webpack
, а почему это создает проблемы для архитектуры проекта? Допустим вам понадобилось внести изменения в файл a.js
, а так как b.js
зависит от a.js
, значит изменения повлияют и на него. Соответственно оба файла придется редактировать. С другой стороны, если вы захотите внести изменения в файл b.js
, тогда вам придется так же вносить изменения и в файл a.js
, так как они оба друг от друга зависимы. Согласитесь, звучит не очень приятно даже при использовании всего 2-ух файлов.
А теперь представьте, что у вас таких 5 файлов и все друг от друга зависят. Не важно в какой из файлов понадобится вносить изменения, в любом случае, вам придется вносить изменения или хотя бы проверить, что ничего не сломалось во всех 5 файлах. А если таких файлов не 5, а 100 или 1000 и большинство из них друг от друга зависимы.
Как эта проблема решена в большинстве проектов
Чтобы решить такого рода проблему, структура вашего проекта чаще всего напоминает дерево файлов. Во главе стоит какой-нибудь сборщик типа webpack
, который импортирует стартовый файл и дальше как ветки импорты расползаются по всему проекту, главная особенность этого дерева, что направление зависимостей идет строго в одну сторону. А на самом нижнем уровне чаще всего находятся сторонние библиотеки.
Технически такая структура файлов многих устраивает, webpack
не жалуется на круговые зависимости, разработчикам такой подход более понятен. Если отредактировал какой-то файл, тогда посмотри кто его импортирует и поправь. Очень просто для понимания.
Недостатки такого решения
Но хорошо ли это для масштабируемости? Давайте вспомним пример, который мы обсуждали в предыдущей статье "[js] Программируйте на уровне интерфейсов" (не обязательно читать, чтобы понять нижесказанное). Одной из подключаемых к тому проекту библиотек является socket.io
. И если, в такого рода дереве, мы обновим версию socket.io
или мигрируем на SSE
, в этом случае возможно и не радикально, но это все же повлияет на половину проекта. С точки зрения архитектуры, это конечно звучит не очень хорошо.
Для решения этой проблемы, в прошлой статье мы создали абстракцию. Что это значит физически. Мы добавили несколько файлов перед библиотекой и объединили их в так называемый модуль, который единственный взаимодействует с SSE
. После этого мы выставили перед этим модулем интерфейс, а именно, то что наш модуль принимает лишь 1 метод onMessage
. И теперь наш модуль обязан подстраиваться под нужды интерфейса, а это значит, что направление зависимостей изменилось. И теперь модуль зависит от интерфейса, а не наоборот.
Соответственно если мы решим заменить снова SSE
на socket.io
все, что нам придется перепроверять, это наш модуль, так как интерфейс от модуля не зависит. Такой процесс называется инверсия зависимостей, 5-ый принцип SOLID
, Dependency Inversion
, если вы не до конца понимаете, как это работает, у нас есть отдельное видео, где мы очень подробно рассказываем как это работает на практике, и еще не раз воспользуемся этим принципом в будущем
А кто от кого должен зависеть?
Мы поняли, что в проекте с помощью инверсии зависимостей мы можем менять направление зависимостей. И результатом этого является дополнительная гибкость проекта, для замены одного кода на другой, с минимизированными затратами. Тогда остается вопрос: "а как правильно организовать направление зависимостей?" Проще говоря кто от кого должен зависеть?
Давайте рассуждать логически. Вернемся к файлам a.js
и b.js
. Допустим нам приходится в a.js
вносить изменения в 2 раза чаще, чем в b.js
. И например, за месяц при доработке фич вам пришлось внести 10 раз изменения в a.js
и соответственно 5 раз в b.js
. Если между ними нет никаких зависимостей, тогда конечное количество файлов которых нам пришлось бы редактировать равно 15.
А если, например a.js
будет зависеть от b.js
. Тогда при каждом из пяти редактирований файла b.js
, скорей всего нам придется вносить правки и в a.js
, это значит, что при такой зависимости нам уже придется редактировать 20 файлов.
Осталось рассмотреть последний вариант, когда b.js
зависит от a.js
. Тогда при каждом из 10 редактирований файла a.js
, скорей всего нам придется вносить правки и в b.js
, это значит, что при такой зависимости нам уже придется редактировать целых 25 файлов.
Возможно на первый взгляд, кажется это небольшой разницей 15, 20 и 25 редактирований. Но давайте масштабируем ситуацию. Допустим в месяц разработчик в среднем выдает 5 таких фич. А в команде 5 таких фронтенд разработчиков. В году 12 месяцев разработки, отбросим отпуска. И на дистанции в 5 лет разработки получим, что при одном направлении зависимостей такой команде придется отредактировать 30 000 файлов, а при другом направлении зависимостей 37 500 тыс файлов.
Разница составляет 7 500 редактирований файлов. А я напомню мы закладывали, что один разработчик в месяц редактирует 100 файлов. Это значит, что если такая команда менее эффективно выставила направление зависимостей в их проекте, тогда они переплатили 75 месяцев разработки одного человека, если в годах, то это более 6 лет разработки. Т.е. команда в 4 человека с более эффективными направлениями зависимостей, сделала бы больше фич, чем команда в 5 человек с менее эффективными направлениями зависимостей.
Конечно все эти цифры максимально утрированы и не отображают реальную картину, так как в реальной разработке на скорость разработки фичи влияет огромное количество факторов. Эти расчеты предназначены лишь помочь вам задуматься, о том, что такая вещь как направление зависимостей, так же очень сильно может повлиять на ваш проект.
Я попробую подытожить мысль, которую я пытался донести: "Направление зависимостей должно строиться так, чтобы файл, который обновляется чаще зависел от файла, который обновляется реже." Т.е. вам в вашем проекте нужно все время анализировать, какой код чаще подвержен модификациям. Если вы нашли такой код, то от него никто не должен зависеть и наоборот.
А если в некоторых модулях вы с трудом можете определить, какой код меняется чаще. Тогда перепишите свой модуль так, чтобы отделить неизменяемый код модуля в отдельный файл или модуль, а часто изменяемый код в другой файл. В таком случае ответ кто от кого должен зависеть будет крайне очевидным.
Викторина
Для тех кому понравилась данная тема, я оставлю ссылочку на мини викторину
Комментарии (8)
AlexSpaizNet
03.08.2021 10:25По мне так влияние количество правок на направление зависимостей выглядит странно. ИМХО контекст намного важнее. А то по вашей логике, если у меня есть http клиент который инжектится но изменяется часто, я должен инжектить бизнес логику в него?
В любом нормально языке Circular Dependency решается 2мя способами.
1. Объединение 2ух файлов/классов вотевер в одине файл/класс/пекедж
2. ИнтерфейсамиSin9k Автор
03.08.2021 10:55А то по вашей логике, если у меня есть http клиент который инжектится но изменяется часто, я должен инжектить бизнес логику в него?
а что значит инжектить бизнес логику? вы имеете ввиду создавать адаптеры, чтобы АПИшка готовила данные в нужном формате бизнес логике?
В любом нормально языке Circular Dependency решается 2мя способами.
1. Объединение 2ух файлов/классов вотевер в одине файл/класс/пекедж
2. ИнтерфейсамиСпособ плох тем, что файл разрастается. И в одном файле, могут быть уже функции, которые крайне не связаны друг с другом. Я подразумеваю вы имели ввиду, то что работает вместе выделить из обоих файлов и поместить в третий файл
Сомнительный путь. Если я буду импортить только эту функцию из того файла, а тот файл будет импортить другую функцию из моего файла. Звучит как крайне скользкая дорожка. Если я правильно конечно понял идею
keksmen
03.08.2021 12:34Простите мне мой тон, но мне кажется, что вы заново изобрели Принцип инверсии зависимостей.
nin-jin
03.08.2021 12:47+1Он скорее изобрёл паттерн "фасад". Инверсия зависимостей тут совершенно ни при чём.
Sin9k Автор
03.08.2021 12:55Да, тут используется инверсия зависимостей) Но это лишь инструмент, как развернуть направление зависимостей. Статья была направлена на то чтобы люди немного больше уделяли внимания направлениям зависимостей в их проектах)
ReDev1L
03.08.2021 23:30Подскажите пожалуйста контакты дилера)
SOLID. Читаем и не думаем о 6 годах разработки)
Alexandroppolus
b.js придется менять, только если поменялось что-то в интерфейсе кода из a.js. Если только реализация, то не придется. В случае следования принципу DIP будет то же самое.
Sin9k Автор
Естественно вы правы. Но идею, которую я пытался передать, думаю вы уловили