Pull to refresh

Comments 25

Не очень понятна описанная проблема «горизонтальности» типизации.

Так как информация о типах размещается на той же строке, что и исполняемый код, то, чтобы уложиться в ширину экрана, во многих местах приходится одну строку разбивать на несколько.

В итоге вы всё равно разбиваете одну строку на несколько, только добавляете шума в виде комментариев документирования (всякие звёздочки и т.д.).

Лично мне непонятно, чем это
/** @type {Program} */
export const program = {
  ...todoItemsAlgebra,
  ...consoleAlgebra,
}

лучше этого:
export const program: Program = {
  ...todoItemsAlgebra,
  ...consoleAlgebra,
}


Становится ещё заметнее, если интерфейс разрастается:

/**
 * @typedef {{
 *   text: string
 *   id: number
 *   hasSomething: boolean
 *   value: SomeClass
 *   hasTodoItem(id: number): boolean
 * }} Item
 */

// vs.

interface Item {
    text: string
    id: number
    hasSomething: boolean
    value: SomeClass
    hasTodoItem(id: number): boolean
}

При этом вы теряете подсветку синтаксиса.

В случае объявления констант или интерфейсов, проблема действительно не видна. Давайте, вместо этого, попробуем сравнить реализации небольшой служебной функции на TypeScript и JavaScript.

type Option<T> = T | undefined

export const map = <P, R>(func: (arg: P) => R) => (data: Option<P>): Option<R> => data !== undefined ? func(data) : undefined
/** @template T @typedef {T | undefined} Option */

/** @type {<P, R>(func: (data: P) => R) => (data: Option<P>) => Option<R>} */
export const map = func => data => data !== undefined ? func(data) : undefined

Конечно, код на TypeScript можно тоже разбить на несколько строк.

export const map = <P, R>(func: (arg: P) => R) => 
  (data: Option<P>): Option<R> => 
  data !== undefined ? func(data) : undefined

Однако, мне кажется, что разделение объявлений типов функций и runtime кода, как это, например, сделано в Haskell, является более логичным и читабельным.

По поводу подсветки синтаксиса в JsDoc — иметь ее было бы неплохо, но мне не кажется, что для уровня типов подсветка настолько критична, чтобы только из-за нее предпочесть тратить время на решение всех технических проблем, которые тянет за собой работа с .ts файлами.

Конечно, код на TypeScript можно тоже разбить на несколько строк.

И получившийся результат даже короче варианта с комментариями.

Однако, мне кажется, что разделение объявлений типов функций и runtime кода, как это, например, сделано в Haskell, является более логичным и читабельным.

Опять же, никто не мешает отделить описание типов, например так:
type Option<T> = T | undefined
type Foo<P, R> = (arg: P) => R
type Bar<P, R> = (data: Option<P>) => Option<R>

export function map<P, R>(func: Foo<P, R>): Bar<P, R> {
    return (data) => data !== undefined ? func(data) : undefined
}

Теперь и типы отделены, и реализация не загромождена. При этом у вас нет минусов подхода с комментариями.

P.S. Я не эксперт в TS, допускаю, что можно записать ещё изящнее и сохранить при этом типизацию.
/** @type {<P, R>(func: (data: P) => R) => (data: Option<P>) => Option<R>} */
export const map = func => data => data !== undefined ? func(data) : undefined

// vs

type Map = <P, R>(func: (data: P) => R) => (data: Option<P>) => Option<R>;
export const map: Map = func => data => data !== undefined ? func(data) : undefined

Очевидно же, что 2-е лучше. И дело даже не в том, что у вас подсветка синтаксиса (vsCode и в комментарии всё подсветит). А в том что теперь TS компилятор упадёт если вы что-то сломаете в этом файле.


JS-Doc типы нужны для двух вещей:


  • а) у вас тонна JS кода, которую надо постепенно переводить на TS. И часто вы просто не можете это сделать быстро, а уже нужно чтобы наружу от JS файлов торчали нормальные типы. Тут комментарии и спасают. TS верит вам на слово и нормальный перевод с JS на TS вы осуществите тогда, когда будет на то возможность.

  • б) вы пишете что-то примитивное на 10 минут — proof of concept, и не хотите эти 10 минут ковыряться со всякими tsConfig.json и npm. И например хотите просто скопировав код сразу запустить его в браузере.

Просто так же писать JS + JsDoc вместо TS кода — это какой-то особенный вариант садомазохизма.


Касательно FP стиля для типов — оно тоже удобно, но я думаю никто из TS разработчиков не перешёл бы на такой формат, если бы он был ультимативным. Удачным вариантом была бы возможность использовать и то и другое по ситуации.

...
if (input.startsWith('+')) {
      const text = input.substring(1).trim()
      ...
})


А для чего в этой конструкции trim() что мы 'режим'? это просто мелкая придирка, извините

Ничто не мешает пользователю, после того, как он введет '+', просто зажать пробел и, затем, нажать клавишу Enter.

Тогда в переменнойinput будет храниться строка вида '+ '. После input.substring(1) плюс будет отрезан, и останутся только пробельные символы.trim их удалит, и, в итоге, строка будет интерпретирована, при дальнейшей обработке, как пустая.

Также, если пользователь введет что-то в духе '+ task1 ', то trim обрежет текст задачи до 'task1'.

Понял, благодарю. Проверка от «дурака».

Реально ли придумать такую архитектуру, которая не заставляла бы чем-то жертвовать?

По моему субъективному мнению - нет.

Когда я слышу об "идеальной, модульной, тестируемой и расширяемой архитектуре", стрелка моего булшитометра резко поднимается и я готовлюсь к тому, что сейчас мне начнут втирать очередную дичь, о том, как же можно такую архитектуру получить. Это прямо как со "свободой, демократией и либеральными ценностями" :)

Статья называется не "идеальная, модульная тестируемая и расширяемая архитектура", а "размышления об ...".

Так что следовало бы сначала прочитать, и возможно высказать аргументы "против", или же напротив, предложить идеи как улучшить.

Ок, я согласен, что мой предыдущий комментарий был мягко говоря мало конструктивным. Я просто высказал точку зрения, что попытки найти универсальную архитектуру мне кажутся малоперспективными.

По поводу содержания - в начале автор делает достаточно конструктивный разбор проблем в нынешней разработке, с которым я вполне согласен. Но начиная с раздела "Типизация", происходит что-то странное - а именно попытка превратить Javascript в Haskell при помощи JSDoc, с уверенность в том, что это решит описанные выше проблемы. Судя по коду репозитория, получился оверинжиниринг с сомнительными преимуществами, при том, что задача достаточно простая и как мне кажется с ее ростом поддержка данного подхода будет все сложнее.

Это мое субъективное мнение и я вполне могу ошибаться, так что не стоит воспринимать это близко к сердцу :)

Декларация репозиториев, на данном этапе, действительно является раздутой. И типы, и runtime код можно было бы легко строить по шаблону, один раз написав порождающие функции, однако, это уже далеко не первая инициатива, которая откладывается в долгий ящик из-за недоделок в TypeScript. Конкретно здесь не хватает возможности динамически именовать параметры в типах функций: TypeScript#44939. Единственное, чем сейчас можно упростить себе жизнь — автоматизировать создание операций для интерпретаторов. Вот так, например, выглядит шаблонный add для интерпретатора репозитория, имеющего форму Map<K, Set<V>>

/** @type {<K, V>(map: Map<K, Set<V>>) => (key: K, value: V) => void} */
export const add = map => (key, value) => {
  const set = map.get(key)

  if (set === undefined) {
    map.set(key, new Set([value]))
  } else {
    set.add(value)
  }
}

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

По поводу JsDoc — я не принуждаю никого использовать его вместо TypeScript, и не говорю, что подход, с построением вычислений на основе алгебр эффектов, обязательно требует использования JsDoc. Все то же самое, с легкостью, можно реализовать и на TypeScript.

Также, 8 архитектура не стремится превратить JavaScript в Haskell, а, наоборот, порицает погоню за академичностью ради академичности. В последней версии был оставлен только минимум абстракций для максимально быстрой разработки и развертывания, без ущерба для качества. Те участки кода, которые вы называете оверинжинирингом, являются дальними родственниками интерфейсов, и классов, эти интерфейсы реализующих. В ООП они никого не смущают, хотя и применяются, зачастую, бесконтрольно. В архитектуре, которая предлагается в этой статье, использование инверсии управления сведено к тому пороговому объему, который минимально необходим для тестирования кода.

Сложность поддержки может показаться большой, но опыт показал, что, в действительности, это не так. Алгебры и интерпретаторы практически никогда не модифицируются — в этом просто нет необходимости. В подавляющем большинстве случаев, если требуется как-то серьезно расширить логику, просто добавляются новые операции или, целиком, сервисы. Это никак не влияет на уже существующий код. Так как алгебры не меняются, то нет необходимости менять и сервисные вычисления. Иногда, как и в случае с операциями, к имеющимся вычислениям лишь добавляются новые. Бизнес логика приложения представлена вычислениями прецедентов. А они, как можно было увидеть в статье, ничем не отличаются от простейшего, по своей структуре, процедурного кода, за исключением взаимодействия с интерпретатором, которое добавляет 2-3 дополнительных символа на каждый вызов. Зато появляется возможность, если требуются какие-то изменения, просто удалить устаревший участок кода и, на замену ему, быстро описать новую логику, используя высокоуровневый интерфейс, предоставляемый интерпретатором и сервисными вычислениями. Ну и теми вычислениями прецедентов, которые остались. Не требуется ни перепроектирование иерархии классов, ни всякие другие, отнимающие время, вещи. Работа получается точечной и, оттого, максимально продуктивной. К этому, бонусом, еще прилагаются тестируемость, переиспользуемость и читабельность.

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

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

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

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

Мне это очень сильно напоминает то, что я слышал из ООП лагеря - объекты изолированы и менять их не нужно, все расширения функционала можно сделать наследованием и тп.

Вообщем, я не имею ничего против использования вашего подхода применительно к каким-то конкретным задачам, но на роль универсального, по моему мнению, он никак не тянет.

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

Вообщем, хочу пожелать удачи в дальнейших исследованиях и я буду рад, если у вас получится хорошее решение для определенного круга задач. Плюс за старания поставил :)

/** @type {TodoItemsAlgebra} */
const todoItemsAlgebra = {
  ...todoItemsRepository,
  ...todoIdsRepository,
}

Как быть с возможным перетиранием имён методов репозиториев? Имею в виду, как описывать методы в новом репозитории, не заглядывая в уже имеющиеся, чтобы не нарваться на конфликт имён при добавлении этого репозитория в алгебру?

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

Например, если у вас есть два репозитория — один хранит объекты задач по id, а второй объекты пользователей, тоже по id, то можно геттеры описать либо как getById, в обоих случаях, либо же можно назвать их более подробно: getTaskById и getUserById. Тогда будет маловероятным, что когда-либо случится пересечение. Какому еще репозиторию вздумается возвращать пользователя по id? Ну, допустим, такое может быть, если у вас есть база данных и локальный кэш. Тогда можно добавить в конце 'FromDB' и 'FromCache'. Получится getUserFromDB и getUserFromCache. Опять ничего не будет пересекаться.

Еще частой практикой является использование названия репозитория при назывании операций. Например, если репозиторий имеет названиеTodoItemsRepository, то его геттер будет называться getTodoItem. А в случае TodoIdsRepository, в данной статье, геттер назвается getNextTodoId. Несовпадение. Хорошим решением будет переименовать этот репозиторий в NextTodoIdRepository, чтобы название более точно отражало его предназначение.

Честно, мы, за год разработки, с конфликтами имен ни разу не сталкивались. Я уже думал о том, как их можно избежать на чисто техническом уровне, но пришел к выводу, что, без сильного усложнения, ничего сделать не получится. Поэтому, лучше оставить все как есть, чем плодить оверинжиниринг.

То есть, в целом, такая архитектура всё же подразумевает ряд устных договоренностей.

А что на счёт код сплитинга? Может, я не до конца прочувствовал философию алгебр, но, допустим, у нас есть React и условная страница (компонент) работает только с методами todoItemsRepository. Как не тянуть на неё и todoIdsRepository и ворох всего остального, что заспредилось в todoItemsAlgebra?Создавать особые алгебры по-странично\по-компонентно?

По поводу код сплиттинга — вычисления могут требовать либо алгебру сервиса, либо алгебру приложения. В случае сервисных вычислений, так как репозитории и фасады, входящие в состав одного сервиса, не целесообразно использовать друг без друга, то не имеет смысла запрашивать что-то меньшее, чем алгебра сервиса. Да, иногда это нарушает принцип минимальных привелегий. Но это оправданный компромисс, чтобы каждый раз не перечислять список требуемых алгебр, что замедляет разработку и очень сильно раздувает код. То же самое и с вычислениями прецедентов. Так как они не подразумевают переиспользование, (хотя оно и возможно, если вырезать код "с мясом"), то не имеет смысла запрашивать в них что-то меньшее, чем алгебра приложения.

Об интеграции с React я планирую сделать отдельную статью. Там, для сохранения кода приложения независимым от фреймворка, требуется наладить двустороннюю связь с компонентами через события, чтобы те могли получать сигналы об изменениях и перерисовываться. А разбор реализации событий пока еще только в планах.

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

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

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

Нужно время, чтобы осмыслить. Допускаю, что не до конца осознал философию и терминологию. Подпишусь, чтобы в будущем почитать про интеграцию с react.

Про принцип единой ответственности понятно, я не имел в виду прямо в компонентах обращаться к алгебрам. Конечно, логика должна проникать внутрь извне (через пропсы). Скорее, вопрос о том, как отдавать на клиент при загрузке страницы необходимый минимум и наполнять его дальше по мере работы SPA (навигация по другим страницам) и ленивой загрузки.

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

Название "Восьмая архитектура", по крайней мере, интригующее. И сразу ассоциации с

Спасибо за статью.

Согласен, название яркое. Почему бы его и не оставить.

Опять же, у данного названия будет предыстория. Как у apple раньше atari в телефонном справочнике. А это хорошо для бренда)

Особенно ярко то, что это не для "попасть на первую строку в тел. справочнике" (что, кстати, очередной миф про Яблоко), а это - восьмая попытка поиска идеальной архитектуры.

Огромный респект за использование jsdoc вместо typescript. Уже несколько лет топлю за такой подход. Имхо, так разработка в разы быстрее

Поставил лайк уже после прочтения вступления. Искренне желаю стать миллионерами!

Да, а что за спектакль? Я что то пропустил?

Про чрезмерное использование метаданных и ссылку - смешно)

Фух. Многабукоф. Но очень толково. Спасибо!

Only those users with full accounts are able to leave comments. Log in, please.