Pull to refresh

Comments 30

Нет, не убедили.


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

А как ограничиваеся использование в вашем решении?


getServiceConfig () {
  return ({
    export: {
      read: ['getBar']
    }
  })
}

Интерфейсы, контракты — нет, не слышали.


А то, что библиотека патчит исходные объекты — это вообще за гранью.


Советую посмотреть на Typescript и библиотеки вроде InversifyJS.

Интерфейсы, контракты — что-то такое слышали, но в JS, уж если у нас как-то получен объект — то ВСЕ его методы доступны. Все варианты сделать методы как бы приватными — черная магия чистой воды.
А как ограничиваеся использование в вашем решении?

Пример маловат, из него не все очевидно, мой просчет.
Все методы, объявленные в `export` конечно же будут доступны компонентам, которые имеют на это права. Но! Обычно объект имеет и приватные методы, которые используются для вынесения кода в аккуратные логичные сущности. И как мы помним, приватны они только на уровне договоренности (см первый абзац ответа). В случае использования Antinite экспортируется ТОЛЬКО то, что описано в экспорте. К приватным методам (не объявленным в экспорте) доступа нет. Вообще нет. Совсем. Никак.
Ровно как у компонентов с правами на методы уровня «чтение» не будет доступа к методам других уровней. Совсем.
И да, библиотека патчит исходные объекты. Потому что она ими владеет по праву использования. Как по мне — грех тянущий максимум на написание хорошего комментария ошибки.
Советую посмотреть на Typescript и библиотеки вроде InversifyJS.

Что именно в Typescript позволяет реализовать хотя бы DI? В Typescript есть что-то для контроля доступа к синглтонам? Аудит их использования?
InversifyJS — забавная вещица, но вот вы мне патчить исходные объекты запрещаете, а там во весь рост Proxy используются, которы мало того что пока в драфте, так еще и, положа руку на сердце, делают тоже самое, по сути.

Если у вас большой размер кода и нужно грамотно разделять зоны ответственностей модулей, то самое время внедрить Typescript, а не устраивать валидацию в рантайме.


Про Proxy: во-первых, это уже часть стандарта языка, поддерживается в stable версии Node. Во-вторых, несмотря на использование Proxy, код остаётся типизированным и предсказуемым. В нем всегда можно проследить где метод декларируется и где вызывается. А в вашем подходе найти концы не представляется возможным

Если у вас большой размер кода и нужно грамотно разделять зоны ответственностей модулей, то самое время внедрить Typescript, а не устраивать валидацию в рантайме.

Вероятно я плохо себе представляю возможности Typescript. Каким образом в нем можно разделить видимость методов, кроме как написав кучу оберток интерфейсов?
Про Proxy: во-первых, это уже часть стандарта языка, поддерживается в stable версии Node. Во-вторых, несмотря на использование Proxy, код остаётся типизированным и предсказуемым. В нем всегда можно проследить где метод декларируется и где вызывается. А в вашем подходе найти концы не представляется возможным

И что нам дает типизированность кода в вопросе доступа к методам? И видимо у меня не получилось донести идеи. Первое — концы конечно же можно найти — система в любой момент может отдать граф зависимостей. Приспособить его к тому же Atom — дело техники. Далее — приведенная Вами библиотека — классический вариант DI, разве что с TS интерфейсами, которые существуют только до момента транспилинга. И сам автор указывает на то, что извлечь из контейнера объект следует в самом верху дерева, после чего приходится носиться с этим объектом как с писаной торбой и прокидывать его дальше вглубь. Или прокинуть контейнер и получить -500 к карме. В этом отношении даже древнючий intravenous и то лучше, правда с ним мы так же в итоге не понимаем что откуда берется и что куда девается.
Каким образом в нем можно разделить видимость методов, кроме как написав кучу оберток интерфейсов?

Вы пишете ровно столько же интерфейсов, сколько кода в методе getServiceConfig вашей имплементации. Только с Typescript и интерфейсами вы получаете все преимущества статической типизации.


И что нам дает типизированность кода в вопросе доступа к методам?

private поля и методы.


система в любой момент может отдать граф зависимостей

Зачем городить велосипеды и пытаться интегрировать их в редактор, если уже есть готовые?

Хорошо, положим в TS у нас вроде бы есть приватные методы и поля. Пусть с ними.

Давайте зайдем все же рассмотрим вариант интеграции с DI.
В любом варианте мы в итоге имеем что-то типа
class Foo {
  constructor(dependency1, dependency2, dependency3) {
    this.ext1 = dependency1
    this.ext2 = dependency2
    this.ext3 = dependency3
  }
 doFoo1 (dataIn) {
     this.ext1.callFn(dataIn)
  }
}

так? Если не брать совсем экстремальные варианты то вроде бы так. А значит при необходимости сделать тест нашего Foo нам надо на минуточку — найти что за объекты пробрасываются в конструктор (а если это довольно далеко от корня — могут быть нюансы), далее — найти методы этих зависимостей, которые используются (по всему коду), далее — сделать на них моки.
Или, используя велосипед — просто посмотреть в перечисление require и дать именно те методы, которые там перечислены. Все! Больше никаких зависимостей у класса нет и быть не может.
Статически объявленная гранулярность зависимостей на уровне методов.
Нет, не нужно? Точно?
просто посмотреть в перечисление require и дать именно те методы, которые там перечислены

Не так уж просто. Вам нужно будет поддерживать моки в будущем, при изменении require. С интерфейсами тоже не сложно: пока интерфейс (контракт) не меняется, то и мок может оставаться прежним. Немного больше работы на старте, зато проще потом.

Апдейт: попробовал реализовать разделение ответственности на Typescript, используя typedi:


import 'reflect-metadata';
import {Inject, Service} from 'typedi';
import {Readable, Writable} from './interfaces';

// Создаем сервис "книга", в который можно читать и писать
@Service("book")
class Book implements Readable, Writable {
  private content: String = 'hello!';

  read() {
    return this.content;
  }

  write(value) {
    this.content = value;
  }

}

// сервис "человек" использует книгу только для чтения
class Person {
  @Inject("book") private book: Readable;

  read() {
    console.log(this.book.read())
  }
}

// сервис "писатель" может писать в книгу
class Writer {
  @Inject("book") private book: Writable;

  write() {
    this.book.write('New content');
  }
}

Получилось декларативно и компактно, я думаю.

Получилось красиво, не спорю.
Но у Вас сервис-потребитель ограничивает себя сам, а у меня сервис-поставщик и домен, в рамках которого он (поставщик) заявлен, контролирует доступность ресурса. Т.е. никто не берет чужого не потому что все честные, а потому что замок висит. В этом вся разница.
в Javascript только что были изобретены функции с сайд-эффектами и приватные поля?
Давайте по существу, что там у нас с «функциями с сайд-эффектами и приватными полями»?
что «самостоятельные компоненты, взаимодействующие друг с другом в пределах одного процесса по контракту, при этом не использующие сеть» это классы любого обьектно ориентированного языка, взаимодействующие через интерфейсы, ну ещё и переменной (полем) в этом языке может быть функция, что придумано давным- давно, а если какой-то язык не в полной мере соответствует концепции ОО, получается нечитабельная и тормозящая в рантайме хрень.
странно, а вот википедия говорит что
Класс — абстрактный тип данных в объектно-ориентированном программировании, задающий общее поведение для группы объектов; модель объекта.

Ну и да, что-то по вашему получается «не читал, но осуждаю». Почитайте, бывает интересно.

следующими видимо будут фемтосервисы.

Кажется, ваша модель безопасности вполне позволяет один раз залинковать друг с другом все сервисы при запуске. Почему было выбрано именно динамическое решение?
Не совсем понимаю вопрос.
Если он в том «почему не все со всеми» — так это и не нужно и неудобно будет, здесь четкое понимание зависимостей прямо в конфигурации видно.
Если «динамическое разрешение доступа» — то по факту-то оно как раз статическое, при сборке всех зависимостей и разрешается.

Ну смотрите, вот вы делаете присваивание this.service[service][funcName] = this.proxyUpcomingExecute.bind(this, service, funcName). Но в функции proxyUpcomingExecute первые два параметра при этом используются вот так:


let grantedItem = this.requireDict[serviceName][action] // action = funcName
return grantedItem.layer.executeAction(grantedItem.ticket, serviceName, action, ...args)

Это означает, что можно было бы биндить непосредственно grantedItem.layer.executeAction вместо proxyUpcomingExecute. Более того, у вас requireDict заполняется в той же функции где и биндится proxyUpcomingExecute — то есть можно было бы вообще использовать непосредственно переменную lookUpRes (которая после становится grantedItem) в той ветке где она уже имеется (а там где ее еще нет — можно отложить заполнение словаря service, избавившись тем самым от requireDict полностью).


Теперь разберемся что такое ваш grantedItem, он же lookUpRes. А это структура из двух свойств, которая формируется вот так: { ticket, layer: this }, при этом единственным ее предназначением является вызов вида x.layer.executeAction(x.ticket, serviceName, action, ...args). Но в таком случае можно было вместо структуры возвращать простую функцию, this.executeAction.bind(this, ticket)!


Это бы одновременно избавило вас от проверки this.grantedTickets.has(ticket) внутрях executeAction — потому что вызов этой функции с невалидным тикетом стал бы невозможным в принципе — вы же this нигде в явном виде сервисам не раскрываете (а еще можно перейти на Typescript и объявить ее приватной).


Продолжаем копать дальше. Первой же строчкой в функции executeAction идет let service = this.registeredWorkers[serviceName] — тот самый который был найден внутри serviceLookup. Его тоже можно забиндить.


Наконец, doExecute. Все что делает этот метод — вызывает this.service[action]. Поскольку action — опять константа, его тоже можно упростить.

Ох, немного неудобно так код обсуждать, в отрыве от него самого.
Но давайте попробую общё — во-первых я не могу себе позволить упрощать код, выкидывая сообщение об ошибках. А так как у нас много ленивых вычислений — верить нельзя никому.
Например — нельзя упросить `proxyUpcomingExecute`, потому что в момент его бинда к `doRequireCall` вполне возможно что `grantedItem.layer` просто отсутствует.
И так много где.
И это мы еще не вспоминаем об асинхронной инициализации сервисов.
Но давайте попробую общё — во-первых я не могу себе позволить упрощать код, выкидывая сообщение об ошибках.

Те сообщения об ошибках, который я предлагаю выкинуть — никогда не будут происходить в принципе.


Конкретно doRequireCall, конечно же, требует всей той логики которая уже написана — за счет своей "динамической" природы. Но вот второй способ обращения к зависимостям (судя по истории — более новый) вполне поддается упрощению, половины ошибок там просто не могут произойти.


А так как у нас много ленивых вычислений [...]
И это мы еще не вспоминаем об асинхронной инициализации сервисов.

Ваша задача прекрасно решается в два этапа: сначала — экспорт всех экшенов, потом связывание и импорт, никаких промежуточных состояний вида "половина сервисов еще не зарезолвились, а остальные нужно использовать" при этом не будет — а значит, и не будет связанных с этим ошибок.

Те сообщения об ошибках, который я предлагаю выкинуть — никогда не будут происходить в принципе.

Да. Но, в теории, они могут появится при внесении изменений. Поэтому моя практика подсказывает проверять то, что может упасть, даже если пока оно падать в общем-то не должно.
Ваша задача прекрасно решается в два этапа: сначала — экспорт всех экшенов, потом связывание и импорт, никаких промежуточных состояний вида «половина сервисов еще не зарезолвились, а остальные нужно использовать» при этом не будет — а значит, и не будет связанных с этим ошибок.

Сложность в том что этих этапов нет, оно начинает разрешаться при импорте домена и как-то там себе разрешается или нет, новый импорт домена — новый цикл разрешений с учетом «ждущих» зависимостей. И если вы 100% уверены что дерево зависимостей корректно и нет асинхронных инициализаций — не нужно даже дожидаться `. onReady()`. Для введения этапов придется жестко задавать момент старта всего процесса, что выглядит не так изящно, и я сейчас не уверен что это сработает всегда. При написании проекта я ориентировался на нативный стиль того, как нода поступает с разрешением зависимостей.
Но мысли интересные, я подумаю.
Да. Но, в теории, они могут появится при внесении изменений. Поэтому моя практика подсказывает проверять то, что может упасть, даже если пока оно падать в общем-то не должно.

Каких таких изменений?


Если значение сначала укладывается в словарь, а потом из него извлекается — то, конечно же, тут может возникнуть ошибка. Но стоит передать значение напрямую, без промежуточного словаря — и ошибка становится невозможной в принципе.

По поводу асинхронных инициализаций. Загрузка модулей в ноде — операция синхронная и блокирующая. Она будет мешать выполняться асинхронным операциям. Поэтому одновременная работа одних сервисов и дозагрузка остальных нормально работать не будет. Все сервисы надо грузить сразу, в самом начале.


Второе соображение: во время выполнения мы, конечно, потенциально можем загрузить сервис (с учетом просадки производительности) — но мы не можем его надежно выгрузить. А значит, изменение списка сервисов во время работы может работать только в одном направлении — то есть является довольно странной фичей.


Для введения этапов придется жестко задавать момент старта всего процесса

У вас в примерах уже есть строчка antiniteSys.ensureAllIsReady(), можно старт процесса повесить на этот вызов. Или на первый вызов execute если почему-то решили ensureAllIsReady не вызывать.

Загрузка модулей в ноде — операция синхронная и блокирующая.

Да, синхронная, нет, не блокирующая. Она начинает проходить по дереву зависимостей и тому, что недорезолвилось — ставит флаг. Именно поэтому в принципе возможны циркулярные зависимости (хоть они и зло). Кроме того новая реализация, которая ES6 modules import — вообще асинхронная.
У вас в примерах уже есть строчка antiniteSys.ensureAllIsReady(), можно старт процесса повесить на этот вызов.

Нет, `.ensureAllIsReady()` это проверка, которая в случае фейла бросает эксепшен. Потому что лучше совсем не взлететь, чем взлететь непонятно на чем, в смысле что не все разрезолвилось.
Или на первый вызов execute если почему-то решили ensureAllIsReady не вызывать.
Некрасиво.
Если вопрос стоит так — сделать что-то сложно выглядящее по-простому или что-то просто выглядящее, но сложное внутри — ИМХО нужно выбирать последнее.
Какая, в сущности разница, как устроен черный ящик, если он работает как надо?
Да, синхронная, нет, не блокирующая.

Проверим?


const fs = require('fs');
const process = require('process');

{
  const line = '*'.repeat(100);
  const file = fs.openSync('bigmodule.js', 'w');
  fs.writeSync(file, '/' + '*'.repeat(99));
  for (let i=1; i<999999; i++)
    fs.writeSync(file, line);
  fs.writeSync(file, '*'.repeat(99) + '/');
  fs.closeSync(file);
}

const timestamp = process.hrtime();
setTimeout(() => {
  var elapsed = process.hrtime(timestamp);
  console.log(`Time passed ${elapsed[0] * 1000 + elapsed[1] / 1000000}ms`);
}, 100);

require('./bigmodule.js');

На моем компьютере таймер срабатывает как ему и положено через 100 миллисекунд… а потом еще 400 миллисекунд ждет пока ему освободят поток. Любой синхронный вызов в ноде — блокирующий.


В реальном проекте, конечно же, таких больших модулей не бывает — зато бывают большие деревья модулей или синхронная инициализация в самих модулях.


Нет, .ensureAllIsReady() это проверка, которая в случае фейла бросает эксепшен.

Ну так прекрасно же! Это именно тот момент который нужен — после этой строчки добавления новых слоев или сервисов уже не будет гарантированно.


сделать что-то сложно выглядящее по-простому

Вот не вижу ничего "сложно выглядящего" в методе start() или run(), это общепринятый подход.

UFO just landed and posted this here
Да я вот так тоже нашему PM говорил, а он что-то там про сроки, заказчиков и контракты талдычит :)
Лучший код — это понятный любому, который делает то что надо и не делает того, чего не надо. И отгруженный вовремя.
UFO just landed and posted this here

В целом, если хорошо по-упрощать ваш код, его можно привести вот примерно к такому виду.


processImports(lookupFn) {
    for (const [serviceName, actions] of Object.entries(workerConfig.require || {})) {
        const imports = this.service[service] = {}

        for (const action of actions) {
            const resolved = lookupFn(serviceName, action)
            if (resolved.kind == "success")
                imports[action] = resolved.fn
        }
    }
}

И это вся необходимая обработка импортов!


При этом ту часть что лежит в layer.js можно написать как-то так:


serviceLookup (callerLayer, callerName, callerGroup, serviceName, action) {
    const ticket = { callerGroup, callerName, callerLayer }
    const service = this.registeredWorkers[serviceName]
    if (!service) {
        //  логи, логи
        return
    }

    const { code, fn } = service.tryResolve(callerGroup, action)
    if (code != "success") {
        //  логи, логи
        return
    }

    return (...args) => {
        if (!this.isReady()) /* Ошибочка */;
        if (/*включен аудит*/) /* аудитим */;
        fn(...args);
    }
}
Спасибо за ваш интерес к коду и приведеные примеры, но я не уверен в корректности его поведения. Если проект Вас заинтересовал — приглашаю присоединится на github. Я всегда очень лоялен к мердж-реквестам, проходящим тесты и соблюдающим стилистику кода, благо standart — очень простой стиль.
Sign up to leave a comment.

Articles