Обновить
16K+
50
Alex Gusev@flancer

Я кодирую, потому что я кодирую…

3,1
Рейтинг
100
Подписчики
Отправить сообщение

Не всегда. Иногда такое называется "список требований".

Вот что-то мне показалось, что в чём-то автор лукавит. "Если вы не понимаете, как работает Нода, то сядьте и разберитесь в ней." (с) Ага, и с Пайтоном так же? И с 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+ ниже, но ваша вера в Типы Скриптовы не позволит вам его увидеть:

export default async function (spec) {
    return /* что-угодно */;
};

Ну и вы сами виноваты в своих неоправдавшихся ожиданиях - статья изначально называлась "Внедрение зависимостей в ES6+ 'на пальцах'". В ES6+, а не в TS. В TS, как и в Java, для этой цели зачастую используются аннотации. Ибо транспиляция позволяет.

В любом случае, стоит придерживаться соглашений по стилю кода (в т.ч. по экспортам/импортам), которые уже приняты в проекте.

Согласен.

За export default существует отдельный котел в аду.

Т.е., это какие-то внутрипроектные соглашения, но вынесенные глобальный на уровень.

Так же, как я у себя в примере взял за основу, что любой es6-модуль по-умолчанию экспортирует фабричную функцию:

export default async function Factory() {
    return /* что-угодно */;
};

Но только не грозил никому отдельным котлом в постмортеме за их неприменение ;)

Кстати, в JS возможно что угодно экспортировать, как Foo, не обязательно классы:

const Bar = {
    name: 'Bar'
};

export {
    Bar as Foo
};

И будет ни чуть не лучше и не хуже, чем с default:

Вполне возможно. Но на практике я пока что не сталкивался с необходимостью протягивать имя класса через статические импорты. 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';

Чем так плох `export default`, что за него так сурово карают в послежизни?

Да, думаю, неплохо может получиться. Это без спецификации (spec) возможно переименование аргументов при минификации, а если все зависимости идут как ключи объекта, то можно и регуляркой доставать.

Вот пример верстки от того же Артёма для стихов:

ещё один пример от Артёма, без промежуточного блока:

  <div class="container">
    <div class="text">
      Этот текст должен быть отцентрирован, но его распидарасило. Очень обескураживает такая иррациональная логика! 
    </div>
  </div>
.container {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 300px;
  outline: 1px solid red;
}

.text {
  width: min-content;
  background-color: yellow;
  white-space: wrap;
}

Это и было сделано в изначальном примере. Только там из-за длинных слов создавалось впечатление, что блок сдвинут влево, а так-то он был вписан в родителя:

Спасибо добрым людям (конкретно - Артёму), объяснили, что вы, возможно, имеете в виду под "выключен и отцентрирован" - текст в блоке выровнен по левой границе, ширина блока определяется самым длинным словом текста, сам блок отцентрирован относительно родительского блока (в красной рамке).

Вполне достижимо средствами 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 для описания типов зависимостей.

Вот смотрите, довольно распространённая ситуация. Вы пишите сервис, который должен логировать данные в ходе выполнения своей работы. У вас есть два логгера - консольный (для разработки) и файловый (для прода). Во "взрослых" языках программирования вы можете определить интерфейс логгера. Например так:

/** @interface */
class ILogger {
    error(msg) {}
    info(msg) {}
}

затем использовать этот интерфейс при разработке сервиса:

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'ами, код должен выглядеть вот так вот:

export class Service {
    /**
     * @param {Type1} dep1
     * @param {Type2} dep2
     * @param {Type3} dep3
     * ...
     */
    constructor(dep1, dep2, dep3, ...) { }
}

А на практике вообще вот так:

export class Service {
    /**
     * @param {Type1} dep1
     * @param {Type2} dep2
     * @param {Type3} dep3
     * ...
     */
    constructor({
                    Type1: dep1,
                    Type2: dep2,
                    Type3: dep3,
                    ...
                }) { }
}

Приходится по сути дублировать JSDoc'ами типы зависимостей. Но. В замен такому геморрою мы получаем не только слабое зацепление но и ещё кое-какие DI'ные плюшки - синглтоны и транзиентные объекты, например.

И это в ванильном JavaScript'е. Один и тот же код и для браузера, и для ноды. Без какой-либо транспиляции. Ну разве не красота?!

У JSDoc'а есть перед TS одно немаловажное преимущество - его не надо транспилировать.

Да, ваш пример совершенно корректен. Так можно и нужно писать код в ES6+ - через import'ы. Как я написал в статье: "Резон использовать этот принцип появляется в тот момент, когда разработчики начинают задумываться не о том, как реализовывать бизнес-функции, а о том, как организовать код так, чтобы можно было продолжать реализовывать бизнес-функции с приемлемой скоростью."

Грубо говоря, до десятков npm-пакетов и сотен или даже тысяч es-модулей можно не заморачиваться с IoC.

Точно так. А для навигации по коду я использую JSDoc'и (как вы указали в комменте выше). В PhpStorm'е работают очень даже неплохо. В них, кстати, и интерфейсы есть, и привязка к имплементации. Можно над конкретикой JS'а надстроить свою собственную абстракщину :)

Информация

В рейтинге
1 240-й
Откуда
Рига, Латвия, Латвия
Дата рождения
Зарегистрирован
Активность

Специализация

Фулстек разработчик
Ведущий
От 3 000 €
JavaScript
HTML
CSS
Node.js
Vue.js
Веб-разработка
Progressive Web Apps
PostgreSQL
MySQL
GitHub