Комментарии 15
Спасибо, любопытный подход. Имхо, обмазывать каждый класс рукописными проверками на отсутствие зависимостей и кидание специальным образом помеченного исключения, а потом крутить это в цикле - некрасивое решение. Вместо этого я бы взял строковое представление функции и достал оттуда регуляркой определение аргументов - сразу получаем полный список зависимостей и избавляемся от бойлерплейта.
За export default существует отдельный котел в аду.
Чем так плох `export default`, что за него так сурово карают в послежизни?
Я бы не сказал что он настолько плох, но проблема в том, что для одной сущности можно легко и неочевидно завести несколько имен, включая опечатки.
Хорошее объяснение, почему не рекомендуется export default
есть в Google TypeScript Style Guide (раздел "Экспорт") (хоть и TS, но также подходит и к JS).
Пример поведения, который не всегда является желанным:
bar.ts :
export default class Foo { ... }
custom-module.ts:
import Foo from './bar'; // Валидно.
import Bar from './bar'; // Также валидно.
Потом при ревью, отладке и пр., в голове приходится держать, что Foo === Bar
, что не всегда очевидно и может приводить к неприятным неожиданностям в коде.
Спасибо за пояснение. Но примерно такие же проблемы могут возникнуть и без 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';
Обратите внимание пожалуйста на разницу этих двух примеров:
import Bar from './bar';
и
import {Foo as Bar} from './bar';
Несколько моментов:
В первом случае импортируется "что-то", что далее будет именоваться как
Bar
и нет никаких гарантий, что это будет именно классFoo
. Во втором случае, вы явно импортируете конкретно классFoo
и при помощи оператораas
четко даете понять ревьюверу, что вам необходим именно классFoo
, но в данном модуле он будет именоваться как классBar
.Далее, если взглянуть на первый вариант, будет ли очевидно без помощи IDE, что для того, чтобы понять, что такое
Bar
и какое у него назначение, вам нужно будет в справке прочитать раздел про классFoo
модуля bar. Во втором случае, вcе гораздо очевидней (классFoo
, модульbar
);В JS, где с контролем типов и так все грустно (привет нарастающей популярности TypeScript), встроенная проверка того, что импортируется именно класс
Foo
будет не лишней. Если кто-нибудь случайно или специально сделает какой-то другой класс экспортируемым по умолчанию, то во втором случае вы сразу получите ошибку импорта. А вот в первом случае вы получите ошибку об отсутствии в объекте каких-то методов или свойств и не сразу догадаетесь, что это связано именно с некорректным импортом.
Код обычно читают намного чаще, чем пишут, поэтому чем очевидней код при чтении, тем лучше. В любом случае, стоит придерживаться соглашений по стилю кода (в т.ч. по экспортам/импортам), которые уже приняты в проекте.
В любом случае, стоит придерживаться соглашений по стилю кода (в т.ч. по экспортам/импортам), которые уже приняты в проекте.
Согласен.
За export default существует отдельный котел в аду.
Т.е., это какие-то внутрипроектные соглашения, но вынесенные глобальный на уровень.
Так же, как я у себя в примере взял за основу, что любой es6-модуль по-умолчанию экспортирует фабричную функцию:
export default async function Factory() {
return /* что-угодно */;
};
Но только не грозил никому отдельным котлом в постмортеме за их неприменение ;)
Кстати, в JS возможно что угодно экспортировать, как Foo, не обязательно классы:
const Bar = {
name: 'Bar'
};
export {
Bar as Foo
};
И будет ни чуть не лучше и не хуже, чем с default:
Вот лично для меня, статья выглядит как какая-то поделка новичка. Если вы хотите раскрыть довольно сложную тему, то нужно показывать куски кода близкие к production-ready. То есть должен быть ts, мало-мальски адекватный код стаил, продуманная система типов. Про паттерны порождения объектов и di написано много статей как и на многие другие темы. А вот качественных шаблонов для той или иной задачи единицы, как раз их я и ожидал прочитав заголовок данной статьи.
Не согласен с вашей точкой зрения.
код в туториале должен быть понятен, а не близок к production ready.
Продуманная система типов в задаче уровня "Hello World!" не нужна, нужна демонстрация базовых принципов.
"Качественный шаблон" для решения задачи IoC и внедрения зависимостей в ES6+ ниже, но ваша вера в Типы Скриптовы не позволит вам его увидеть:
export default async function (spec) {
return /* что-угодно */;
};
Ну и вы сами виноваты в своих неоправдавшихся ожиданиях - статья изначально называлась "Внедрение зависимостей в ES6+ 'на пальцах'". В ES6+, а не в TS. В TS, как и в Java, для этой цели зачастую используются аннотации. Ибо транспиляция позволяет.
Спасибо за публикацию. Вопрос по поддержке типизации, если уместен.
Подскажите, пожалуйста, а Вы пробрасываете типы из динамически загружаемого модуля через контейнер в другие es-модули?
Я не пробрасываю типы (вернее, классы/объекты/функции), я использую их напрямую там, где в них есть необходимость. Контейнер знает, каким образом получить доступ к каждому 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. Тут не столько в терминах исходного кода нужно мыслить, сколько в терминах работающей программы (те же синглтоны).
Спасибо за вопрос, очень полезный оказался, как минимум для меня.
Внедрение зависимостей в ES6+ «на пальцах»