Вот что-то мне показалось, что в чём-то автор лукавит. "Если вы не понимаете, как работает Нода, то сядьте и разберитесь в ней." (с) Ага, и с Пайтоном так же? И с JS? И с TS? А есть ещё Java, PHP, C#, Go тот же. RDBMS - сядьте и разберитесь с Oracle/MS SQL/PostgreSQL. Изучите отличия MariaDB от MySQL. А потом разберите отличия всего это многообразия от NoSQL DBs, объектно-ориентированных БД, БД документов и иерархических БД. Чем отличается Tailwind от CSS3. Когда лучше применять React, когда Angular, а когда Vue? Для чего нужен Svelte? Нужен ли вообще $mol? Можно изучить ещё различия PHP'шных фреймворков друг от друга - Laravel, Symfony. Java'вский Spring. Занырнуть в архитектуру и особенности функционирования WordPress, Magento, Joomla, Django, Odoo, 1C, Liferay. Понять чем CMS отличаются от CRM, а всё это от ERP и BPMS.
Мне кажется, что тут зарождается (или уже родилась?) новая религия "Сядь и разберись, если не понятно". Я вас уверяю, что все, кто писал, с точки зрения автора, говно-проекты, именно так и сделали - сели и разобрались. А получилось, что получилось. Кто-то разбирается лучше, кто-то хуже. Автор может всем ответственно заявить, что он уже разобрался с Нодой? Или хотя бы хоть с чем-то разобрался до конца?
"Я знаю только то, что ничего не знаю, но другие не знают и этого" (с) Сократ
Я не пробрасываю типы (вернее, классы/объекты/функции), я использую их напрямую там, где в них есть необходимость. Контейнер знает, каким образом получить доступ к каждому es-модулю проекта и его содержимому (экспорту). Он может напрямую создавать требуемые объекты, без посредников. В данном примере это описано в пункте "7. Карта зависимостей" в виде переменной map, где зависимости между идентификатором зависимости и местом расположения его исходников прописано вручную.
В реальных проектах идентификаторы зависимостей выглядят более кучеряво и, как правило, основаны на полном имени класса, который создаёт нужную для внедрения зависимость (например com.vendor.project.module.BaseClass - в Java или /Vendor/Project/Module/BaseClass в PHP). Так как идентификатор зависимости - это строка, то лишь от фантазии разработчика зависит, какие правила он использует для преобразования строки в:
путь к файлу с исходниками для импорта;
определение нужного экспорта внутри es6-модуля;
флаг создания нового экземпляра зависимости (transient) или передачи уже существующего (singleton);
флаг добавления адаптора (wrapper'а) к создаваемой зависимости;
Зависимость - это не просто тип (уровень исходного кода), это объект, существующий в runtime. Тут не столько в терминах исходного кода нужно мыслить, сколько в терминах работающей программы (те же синглтоны).
Спасибо за вопрос, очень полезный оказался, как минимум для меня.
Ну и вы сами виноваты в своих неоправдавшихся ожиданиях - статья изначально называлась "Внедрение зависимостей в ES6+ 'на пальцах'". В ES6+, а не в TS. В TS, как и в Java, для этой цели зачастую используются аннотации. Ибо транспиляция позволяет.
Вполне возможно. Но на практике я пока что не сталкивался с необходимостью протягивать имя класса через статические импорты. IDE прекрасно подобное разруливает. PhpStorm, как минимум. Даже с опечатками.
Спасибо за пояснение. Но примерно такие же проблемы могут возникнуть и без default'а:
// ./bar.js
export class Foo {}
import {Foo} from './bar.js';
import {Foo as Bar} from './bar.js';
const f1 = new Foo();
const b1 = new Bar();
IMHO, иметь один класс в одном модуле - это очень хорошая практика для бэка, судя по Java/PHP. Да и вообще SOLID'но (single-responsibility principle). А раз так, то больше одного экспорта и не особо-то нужно.
По рукам, на мой взгляд, нужно бить не за то, что кто-то что-то по default'у экспортирует, а как раз вот за такое использование (если оно ни чем не обусловлено):
import Foo from './bar'; // Валидно.
import Bar from './bar'; // Также валидно.
впрочем, как и за:
import {Foo} from './bar.js';
import {Foo as Bar} from './bar.js';
Да, думаю, неплохо может получиться. Это без спецификации (spec) возможно переименование аргументов при минификации, а если все зависимости идут как ключи объекта, то можно и регуляркой доставать.
ещё один пример от Артёма, без промежуточного блока:
<div class="container">
<div class="text">
Этот текст должен быть отцентрирован, но его распидарасило. Очень обескураживает такая иррациональная логика!
</div>
</div>
Это и было сделано в изначальном примере. Только там из-за длинных слов создавалось впечатление, что блок сдвинут влево, а так-то он был вписан в родителя:
Спасибо добрым людям (конкретно - Артёму), объяснили, что вы, возможно, имеете в виду под "выключен и отцентрирован" - текст в блоке выровнен по левой границе, ширина блока определяется самым длинным словом текста, сам блок отцентрирован относительно родительского блока (в красной рамке).
Вполне достижимо средствами CSS, как видно в примере. Возможно, $mol просто не очень подходящий инструмент для изысканной вёрстки. Пока через тернии Tree продерёшься до основ...
У вас исключительно скромные навыки социального взаимодействия и в силу этого вы весьма забавный персонаж. Так факапиться, как вы - тут определённо нужен талант!
Ваш текст изначально был "выключен" по левому краю. Размер родительского элемента в вашем примере 300px. Слова "отцентрирован", "распидарасило", "обескураживает" и "иррациональная" просто длиннее, чем свободное место на предыдущей строке. Я изменил размер родительского элемента, поставив 325px, и стало вот так:
Поставьте word-break: break-all; и будем вам и центровка, и выключка, и полное обалдевание пользователей от переносов:
Если добавить `text-align: center;` напрямую в DOM через DevTools, то вполне себе центруется. Всё-таки HTML/CSS/JS - это базовые технологии для веба. Остальное, включая Tree - преобразовывается к этим трём. Наверное где-то преобразование споткнулось.
Каждый подключаемый модуль пройдет в начале через импорт или его аналог (например, прямое внедрение на страницу через тег script). Это часть жизненного цикла. Ибо откуда он сам по себе возьмется? А это означает, что адрес модуля, его физическое расположение любом случае нужно будет учитывать (даже хотя бы один раз).
Точно. В ES6+ модули загружаются через импорт - статический или динамический. Других вариантов нет.
Причем эта концепция полностью отражена в моем простом коде service.js, где все модули грузятся только всего один раз и будут доступны в неймспейсе модуля service.js.
Это и есть прямой контроль - когда разработчик сервиса должен знать, где находятся его зависимости, чтобы подключить их через import.
Если какой-то из модулей будет перемещен (кроме базового, разумеется), то изменения придется внести один раз
Именно. Изменения придётся внести. Если у вас зависимость (logger) используется в одном модуле (service), то придётся делать одно изменение. А если у вас таких сервисов десять тысяч, то придётся делать десять тысяч изменений. Вы всего лишь передвинули один модуль (logger), а менять пути импорта придётся в десяти тысячах скриптов, которые этот модуль используют.
как бы не пришлось в коде без js-импортов каждый раз указывать jsdoc-импорт типа service.
Именно это и приходится делать - использовать JSDoc для описания типов зависимостей.
Вот смотрите, довольно распространённая ситуация. Вы пишите сервис, который должен логировать данные в ходе выполнения своей работы. У вас есть два логгера - консольный (для разработки) и файловый (для прода). Во "взрослых" языках программирования вы можете определить интерфейс логгера. Например так:
затем использовать этот интерфейс при разработке сервиса:
class Service {
/** @type {ILogger} */
logger;
exec(opts) {
this.logger.info('Executing...');
}
}
Всё, для разработки сервиса вам не нужно знать, где собственно находятся имплементации логгера и какие (консольные, файловые, сетевый, базы данных).
Но. Чтобы это дело заработало, каким-то образом нужно внедрить зависимость logger в класс service. Обычно это делают через конструктор:
class Service {
/** @type {ILogger} */
logger;
/** @param {ILogger} logger */
constructor(logger) {
this.logger = logger;
}
}
Смотрите, вот валидный JavaScript код, который соответствует поставленной задаче (сервис с логгированием) и в котором нет ни одного импорта. Мы просто резанули задачу по месту склейки (интерфейсу) и можем распараллелить процесс разработки: один разработчик делает сервис, второй - файловый логгер, третий - логгер для перенаправления логов на сервис Sentry.
Вы правы, что без импортов ничего не закрутится. Как разработчик сервиса вы можете поднять окружение (привет TDD!), которое мокирует зависимости в соответствии с заданным интерфейсом и проверяет корректность имплементации:
import assert from 'assert';
import {describe, it} from 'mocha';
import {Service} from './Service.js';
/** @implements ILogger */
const logger = {
error(mg) {},
info(msg) {}
};
describe('Service', () => {
it('does the job', () => {
const service = new Service(logger);
service.exec({});
assert(true);
});
});
Вот здесь, в тестовом окружении, вы import'ы и используете. Вот это уже инверсия управления. Разраб сервиса ничего не знает о том, где находится код логгера и какой из логгеров (консольный или файловый) будет использоваться - это вне рамок поставленной ему задачи. Он лишь знает, что каким-то образом его сервис получит логгер при создании. Для разработки сервиса разраб мокирует зависимости, имплементируя нужное ему поведение зависимостей. Это в данном случае логгер просто делает что-то и не возвращает ничего. Зачастую зависимости возвращают какой-то результат и разработчик сервиса может запрограммировать этот результат (или набор результатов) в своей имплементации зависимости. Более того, для разных тестов можно создавать разные имплементации одних и тех же зависимостей. Зацепление кода резко снижается. В JS код цепляется импортами. Нет импортов - нет зацепления.
Подобная красота достигается вот таким типовым кодом:
export class Service {
constructor(dep1, dep2, dep3, ...) { }
}
Да, на самом деле, с JSDoc'ами, код должен выглядеть вот так вот:
Приходится по сути дублировать JSDoc'ами типы зависимостей. Но. В замен такому геморрою мы получаем не только слабое зацепление но и ещё кое-какие DI'ные плюшки - синглтоны и транзиентные объекты, например.
И это в ванильном JavaScript'е. Один и тот же код и для браузера, и для ноды. Без какой-либо транспиляции. Ну разве не красота?!
Да, ваш пример совершенно корректен. Так можно и нужно писать код в ES6+ - через import'ы. Как я написал в статье: "Резон использовать этот принцип появляется в тот момент, когда разработчики начинают задумываться не о том, как реализовывать бизнес-функции, а о том, как организовать код так, чтобы можно было продолжать реализовывать бизнес-функции с приемлемой скоростью."
Грубо говоря, до десятков npm-пакетов и сотен или даже тысяч es-модулей можно не заморачиваться с IoC.
Точно так. А для навигации по коду я использую JSDoc'и (как вы указали в комменте выше). В PhpStorm'е работают очень даже неплохо. В них, кстати, и интерфейсы есть, и привязка к имплементации. Можно над конкретикой JS'а надстроить свою собственную абстракщину :)
Не всегда. Иногда такое называется "список требований".
Вот что-то мне показалось, что в чём-то автор лукавит. "Если вы не понимаете, как работает Нода, то сядьте и разберитесь в ней." (с) Ага, и с Пайтоном так же? И с JS? И с TS? А есть ещё Java, PHP, C#, Go тот же. RDBMS - сядьте и разберитесь с Oracle/MS SQL/PostgreSQL. Изучите отличия MariaDB от MySQL. А потом разберите отличия всего это многообразия от NoSQL DBs, объектно-ориентированных БД, БД документов и иерархических БД. Чем отличается Tailwind от CSS3. Когда лучше применять React, когда Angular, а когда Vue? Для чего нужен Svelte? Нужен ли вообще $mol? Можно изучить ещё различия PHP'шных фреймворков друг от друга - Laravel, Symfony. Java'вский Spring. Занырнуть в архитектуру и особенности функционирования WordPress, Magento, Joomla, Django, Odoo, 1C, Liferay. Понять чем CMS отличаются от CRM, а всё это от ERP и BPMS.
Мне кажется, что тут зарождается (или уже родилась?) новая религия "Сядь и разберись, если не понятно". Я вас уверяю, что все, кто писал, с точки зрения автора, говно-проекты, именно так и сделали - сели и разобрались. А получилось, что получилось. Кто-то разбирается лучше, кто-то хуже. Автор может всем ответственно заявить, что он уже разобрался с Нодой? Или хотя бы хоть с чем-то разобрался до конца?
"Я знаю только то, что ничего не знаю, но другие не знают и этого" (с) Сократ
Я не пробрасываю типы (вернее, классы/объекты/функции), я использую их напрямую там, где в них есть необходимость. Контейнер знает, каким образом получить доступ к каждому es-модулю проекта и его содержимому (экспорту). Он может напрямую создавать требуемые объекты, без посредников. В данном примере это описано в пункте "7. Карта зависимостей" в виде переменной map, где зависимости между идентификатором зависимости и местом расположения его исходников прописано вручную.
В реальных проектах идентификаторы зависимостей выглядят более кучеряво и, как правило, основаны на полном имени класса, который создаёт нужную для внедрения зависимость (например
com.vendor.project.module.BaseClass- в Java или/Vendor/Project/Module/BaseClassв PHP). Так как идентификатор зависимости - это строка, то лишь от фантазии разработчика зависит, какие правила он использует для преобразования строки в:путь к файлу с исходниками для импорта;
определение нужного экспорта внутри es6-модуля;
флаг создания нового экземпляра зависимости (transient) или передачи уже существующего (singleton);
флаг добавления адаптора (wrapper'а) к создаваемой зависимости;
Зависимость вполне может выглядеть, как URL:
@vendor/package/esmodule.mjs#exportName?singleton&scope=requestЗависимость - это не просто тип (уровень исходного кода), это объект, существующий в runtime. Тут не столько в терминах исходного кода нужно мыслить, сколько в терминах работающей программы (те же синглтоны).
Спасибо за вопрос, очень полезный оказался, как минимум для меня.
Не согласен с вашей точкой зрения.
код в туториале должен быть понятен, а не близок к production ready.
Продуманная система типов в задаче уровня "Hello World!" не нужна, нужна демонстрация базовых принципов.
"Качественный шаблон" для решения задачи IoC и внедрения зависимостей в ES6+ ниже, но ваша вера в Типы Скриптовы не позволит вам его увидеть:
Ну и вы сами виноваты в своих неоправдавшихся ожиданиях - статья изначально называлась "Внедрение зависимостей в ES6+ 'на пальцах'". В ES6+, а не в TS. В TS, как и в Java, для этой цели зачастую используются аннотации. Ибо транспиляция позволяет.
Согласен.
Т.е., это какие-то внутрипроектные соглашения, но вынесенные глобальный на уровень.
Так же, как я у себя в примере взял за основу, что любой es6-модуль по-умолчанию экспортирует фабричную функцию:
Но только не грозил никому отдельным котлом в постмортеме за их неприменение ;)
Кстати, в JS возможно что угодно экспортировать, как Foo, не обязательно классы:
И будет ни чуть не лучше и не хуже, чем с default:
Вполне возможно. Но на практике я пока что не сталкивался с необходимостью протягивать имя класса через статические импорты. IDE прекрасно подобное разруливает. PhpStorm, как минимум. Даже с опечатками.
Спасибо за пояснение. Но примерно такие же проблемы могут возникнуть и без default'а:
IMHO, иметь один класс в одном модуле - это очень хорошая практика для бэка, судя по Java/PHP. Да и вообще SOLID'но (single-responsibility principle). А раз так, то больше одного экспорта и не особо-то нужно.
По рукам, на мой взгляд, нужно бить не за то, что кто-то что-то по default'у экспортирует, а как раз вот за такое использование (если оно ни чем не обусловлено):
впрочем, как и за:
Чем так плох `export default`, что за него так сурово карают в послежизни?
Да, думаю, неплохо может получиться. Это без спецификации (spec) возможно переименование аргументов при минификации, а если все зависимости идут как ключи объекта, то можно и регуляркой доставать.
Вот пример верстки от того же Артёма для стихов:
ещё один пример от Артёма, без промежуточного блока:
Это и было сделано в изначальном примере. Только там из-за длинных слов создавалось впечатление, что блок сдвинут влево, а так-то он был вписан в родителя:
Спасибо добрым людям (конкретно - Артёму), объяснили, что вы, возможно, имеете в виду под "выключен и отцентрирован" - текст в блоке выровнен по левой границе, ширина блока определяется самым длинным словом текста, сам блок отцентрирован относительно родительского блока (в красной рамке).
Вполне достижимо средствами CSS, как видно в примере. Возможно, $mol просто не очень подходящий инструмент для изысканной вёрстки. Пока через тернии Tree продерёшься до основ...
"Нее, друг, с таким настроением ты слона не продашь!" (с)
У вас исключительно скромные навыки социального взаимодействия и в силу этого вы весьма забавный персонаж. Так факапиться, как вы - тут определённо нужен талант!
Ваш текст изначально был "выключен" по левому краю. Размер родительского элемента в вашем примере 300px. Слова "отцентрирован", "распидарасило", "обескураживает" и "иррациональная" просто длиннее, чем свободное место на предыдущей строке. Я изменил размер родительского элемента, поставив 325px, и стало вот так:
Поставьте
word-break: break-all;и будем вам и центровка, и выключка, и полное обалдевание пользователей от переносов:Если добавить `text-align: center;` напрямую в DOM через DevTools, то вполне себе центруется. Всё-таки HTML/CSS/JS - это базовые технологии для веба. Остальное, включая Tree - преобразовывается к этим трём. Наверное где-то преобразование споткнулось.
Точно. В ES6+ модули загружаются через импорт - статический или динамический. Других вариантов нет.
Это и есть прямой контроль - когда разработчик сервиса должен знать, где находятся его зависимости, чтобы подключить их через import.
Именно. Изменения придётся внести. Если у вас зависимость (logger) используется в одном модуле (service), то придётся делать одно изменение. А если у вас таких сервисов десять тысяч, то придётся делать десять тысяч изменений. Вы всего лишь передвинули один модуль (logger), а менять пути импорта придётся в десяти тысячах скриптов, которые этот модуль используют.
Именно это и приходится делать - использовать JSDoc для описания типов зависимостей.
Вот смотрите, довольно распространённая ситуация. Вы пишите сервис, который должен логировать данные в ходе выполнения своей работы. У вас есть два логгера - консольный (для разработки) и файловый (для прода). Во "взрослых" языках программирования вы можете определить интерфейс логгера. Например так:
затем использовать этот интерфейс при разработке сервиса:
Всё, для разработки сервиса вам не нужно знать, где собственно находятся имплементации логгера и какие (консольные, файловые, сетевый, базы данных).
Но. Чтобы это дело заработало, каким-то образом нужно внедрить зависимость logger в класс service. Обычно это делают через конструктор:
Смотрите, вот валидный JavaScript код, который соответствует поставленной задаче (сервис с логгированием) и в котором нет ни одного импорта. Мы просто резанули задачу по месту склейки (интерфейсу) и можем распараллелить процесс разработки: один разработчик делает сервис, второй - файловый логгер, третий - логгер для перенаправления логов на сервис Sentry.
Вы правы, что без импортов ничего не закрутится. Как разработчик сервиса вы можете поднять окружение (привет TDD!), которое мокирует зависимости в соответствии с заданным интерфейсом и проверяет корректность имплементации:
Вот здесь, в тестовом окружении, вы import'ы и используете. Вот это уже инверсия управления. Разраб сервиса ничего не знает о том, где находится код логгера и какой из логгеров (консольный или файловый) будет использоваться - это вне рамок поставленной ему задачи. Он лишь знает, что каким-то образом его сервис получит логгер при создании. Для разработки сервиса разраб мокирует зависимости, имплементируя нужное ему поведение зависимостей. Это в данном случае логгер просто делает что-то и не возвращает ничего. Зачастую зависимости возвращают какой-то результат и разработчик сервиса может запрограммировать этот результат (или набор результатов) в своей имплементации зависимости. Более того, для разных тестов можно создавать разные имплементации одних и тех же зависимостей. Зацепление кода резко снижается. В JS код цепляется импортами. Нет импортов - нет зацепления.
Подобная красота достигается вот таким типовым кодом:
Да, на самом деле, с JSDoc'ами, код должен выглядеть вот так вот:
А на практике вообще вот так:
Приходится по сути дублировать JSDoc'ами типы зависимостей. Но. В замен такому геморрою мы получаем не только слабое зацепление но и ещё кое-какие DI'ные плюшки - синглтоны и транзиентные объекты, например.
И это в ванильном JavaScript'е. Один и тот же код и для браузера, и для ноды. Без какой-либо транспиляции. Ну разве не красота?!
У JSDoc'а есть перед TS одно немаловажное преимущество - его не надо транспилировать.
Да, ваш пример совершенно корректен. Так можно и нужно писать код в ES6+ - через import'ы. Как я написал в статье: "Резон использовать этот принцип появляется в тот момент, когда разработчики начинают задумываться не о том, как реализовывать бизнес-функции, а о том, как организовать код так, чтобы можно было продолжать реализовывать бизнес-функции с приемлемой скоростью."
Грубо говоря, до десятков npm-пакетов и сотен или даже тысяч es-модулей можно не заморачиваться с IoC.
Точно так. А для навигации по коду я использую JSDoc'и (как вы указали в комменте выше). В PhpStorm'е работают очень даже неплохо. В них, кстати, и интерфейсы есть, и привязка к имплементации. Можно над конкретикой JS'а надстроить свою собственную абстракщину :)