В данной статье я планирую развить тему важности умения "Программировать на уровне интерфейсов", а именно обсудить направление зависимостей. Это достаточно важная тема, так как только осознавая направленность зависимостей, можно спроектировать действительно гибкое и масштабируемое приложение (Данная статья является расшифровкой видео).
// a.js
import b from './b'
Здесь мы видим, что файл a.js зависит от файла b.js. A может ли теперь b.js зависеть от a.js?
Большинство из вас сразу же ответят конечно же нет. У этой проблемы даже есть название Circular Dependencies или круговые зависимости. И есть даже webpack плагин, который помогает вам найти Circular Dependency в вашем проекте. Зачастую они могут быть не совсем очевидными, круг замыкается через 5-6 импортов.
А теперь представьте, что у вас таких 5 файлов и все друг от друга зависят. Не важно в какой из файлов понадобится вносить изменения, в любом случае, вам придется вносить изменения или хотя бы проверить, что ничего не сломалось во всех 5 файлах. А если таких файлов не 5, а 100 или 1000 и большинство из них друг от друга зависимы.
Технически такая структура файлов многих устраивает, webpack не жалуется на круговые зависимости, разработчикам такой подход более понятен. Если отредактировал какой-то файл, тогда посмотри кто его импортирует и поправь. Очень просто для понимания.
Для решения этой проблемы, в прошлой статье мы создали абстракцию. Что это значит физически. Мы добавили несколько файлов перед библиотекой и объединили их в так называемый модуль, который единственный взаимодействует с 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 человек с менее эффективными направлениями зависимостей.
Конечно все эти цифры максимально утрированы и не отображают реальную картину, так как в реальной разработке на скорость разработки фичи влияет огромное количество факторов. Эти расчеты предназначены лишь помочь вам задуматься, о том, что такая вещь как направление зависимостей, так же очень сильно может повлиять на ваш проект.
Я попробую подытожить мысль, которую я пытался донести: "Направление зависимостей должно строиться так, чтобы файл, который обновляется чаще зависел от файла, который обновляется реже." Т.е. вам в вашем проекте нужно все время анализировать, какой код чаще подвержен модификациям. Если вы нашли такой код, то от него никто не должен зависеть и наоборот.
А если в некоторых модулях вы с трудом можете определить, какой код меняется чаще. Тогда перепишите свой модуль так, чтобы отделить неизменяемый код модуля в отдельный файл или модуль, а часто изменяемый код в другой файл. В таком случае ответ кто от кого должен зависеть будет крайне очевидным.
Что такое зависимость?
Но давайте все по порядку. Начнем с того как выглядят в наших проектах зависимости - это обычный 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 человек с менее эффективными направлениями зависимостей.
Конечно все эти цифры максимально утрированы и не отображают реальную картину, так как в реальной разработке на скорость разработки фичи влияет огромное количество факторов. Эти расчеты предназначены лишь помочь вам задуматься, о том, что такая вещь как направление зависимостей, так же очень сильно может повлиять на ваш проект.
Я попробую подытожить мысль, которую я пытался донести: "Направление зависимостей должно строиться так, чтобы файл, который обновляется чаще зависел от файла, который обновляется реже." Т.е. вам в вашем проекте нужно все время анализировать, какой код чаще подвержен модификациям. Если вы нашли такой код, то от него никто не должен зависеть и наоборот.
А если в некоторых модулях вы с трудом можете определить, какой код меняется чаще. Тогда перепишите свой модуль так, чтобы отделить неизменяемый код модуля в отдельный файл или модуль, а часто изменяемый код в другой файл. В таком случае ответ кто от кого должен зависеть будет крайне очевидным.
Как сэкономить годы разработки при правильном направлении зависимостей
Привет Хабр! В данной статье я планирую развить тему важности умения "Программировать на уровне интерфейсов", а именно обсудить направление зависимостей . Это достаточно важная тема, так как только...
habr.com