Обновить
8K+
51
Alex Gusev@flancer

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

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

Вы вырвали одну фразу

Ведь это и есть классическая формулировка ISP по Мартину, разве нет?

Я использую инверсию зависимости в своих проектах

Проекты на JS или на TS? Судя по "Реально в js нет интерфейсов... ну это надо как-то проверять тогда в коде чтоли...", не на JS.

А я использую инверсию зависимостей в своих проектах именно на JS. Не на TS, не на Java, не на PHP, а именно на "ванильном" ES6+. И эта инверсия у меня такая, какая есть, именно потому, что это JS, а не TS.

Вот классическая формулировка Роберта Мартина:

Clients should not be forced to depend upon interfaces that they do not use.

О направлении требований говорит "Clients should not be forced to depend...". Клиенты (потребители) не должны зависить от неиспользуемых ими интерфейсов.

Это значит, что клиенты сами определяют свою зависимость от интерфейсов, которые им нужны (that they use). Независимо ни от кого. И это уже дело среды исполнения обеспечить поставку таких компонентов, которые нужные клиенту интерфейсы имплементируют. При этом реальные возможности этих компонентов потребителя (клиента), за границами его нужд, не волнуют совершенно. Это может быть God Object, может быть "оркестр", совмещающий несколько интерфейсов, а может быть единичная имплементация именно этого интерфейса. As you wish, как говорится.

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

Сформулируйте, пожалуйста, ISP в том виде, который вы используете. Я покажу, что говорит о направлении требований.

Системный аналитик хорошо представляет откуда берутся требования к вашему коду.

Посмотрите, что такое "I" в аббревиатуре "SOLID" (Interface Segregation Principle). Он как раз об этом - требования идут от потребителя, а не от возможностей поставщика.

А вы попробуйте обсудить эту точку зрения с вашим системным аналитиком :)

В языке где есть интерфейс достаточно написать implements (например php) и быть уверенным что ваши репозитории поддерживают контракт.

Да. Но это в тех языках, где "интерфейс" поддерживается на уровне языка. В JS такого нет.

Получается вы как веб разработчик считаете что для домена (бизнес логики) интерфейс задают потребители бизнес логики?

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

Сидеть и сверять программу с текстом, когда за выполнением контрактов должен отвечать компилятор/интерпретатор это явно два шага назад

Полностью согласен с такой постановкой вопроса. Не человек должен сверять код с кодом, а специальные программы. Поэтому, когда я перешёл с PhpStorm на VSCode, у меня просто волосы дыбом встали от того, насколько плохо поддерживается JSDoc в VSCode. В IDEA (WebStorm, PhpStorm, ...) IDE самостоятельно связывает JSDoc-аннотации @interface и @implements в JS-коде и обеспечивает навигацию по коду, его проверку и автокомплит. И WebStorm умел это делать ещё тогда, когда TypeScirpt'а ещё не существовало.

Чтобы получить аналогичное поведение в VSCode, нужно постараться. Но это и понятно, Microsoft изо всех сил развивает свои активы (TypeScript и VSCode). tsserver не будет поддерживать JSDoc в полной мере.

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

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

В TS есть возможность описать interface в отдельном исходнике, который при транспиляции не превращается в js-код, из-за того, что в JS нет интерфейсов вообще.

Зато в JS есть JSDoc и возможность описать интерфейс как в отдельном файле, так и в types.d.ts (если в пакете интерфейсов не много).

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

"Интерфейс репозитория" вырастает не из "общих свойств всех репозиториев", а из "общих ожиданий всех потребителей всех репозиториев".

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

А так вам просто нужно место (файл), где описать какой-то интерфейс в том или ином виде, предусмотренным соответствующим ЯП, или в md- или txt-файле, если этот ЯП ещё менее развит, чем JS с его JSDoc.

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

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

Но вы можете сделать "фейковый интерфейс" в JS (вот работающий пример):

/** @interface */
class IRepository {}

и две имплементации:

/** @implements {IRepository} **/
export default class SqliteRepository {}

и

/** @implements {IRepository} **/
export default class PostgresRepository {}

А в зависимостях компонентов указывать именно интерфейс

export default function Service({ repository }) {}

export const __deps__ = {
  repository: "IRepository$",
};

Всё, как в других ЯП.

Да, в Composition Root нужно будет решать, какая из имплементаций интерфейса в какой компонент инжектится ("везде одна и та же" или "в эти - одна, в те - другая"). Но такое решение нужно принимать везде, где указан интерфейс вместо имплементации. Я ж говорю, всё, как в других ЯП.

"Это всё" слишком широкое понятие. А излишняя самоуверенность есть признак узкого кругозора.

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

Да, конечно. Это в любом ЯП так будет. Если вы в runtime умудритесь подсунуть зависимость не с тем контрактом, завалится всё приложение, если не стоит обработки ошибок. Другое дело, что многие ЯП заставляют разрабов описывать контракты и проверяют соответствие контрактов на этапе компиляции - это уменьшает вероятность получить нечто неожиданное в runtime.

ну это надо как-то проверять тогда в коде чтоли...

Проверять не надо - неэффективно. Достаточно обеспечить поставку зависимости с соответствующим контрактом и обработку ошибок на нужном уровне приложения.

язык как будто для маленьких манипуляций с html сделали

Так оно и было. Всё остальное наросло сильно позже.

Я начинал программировать на PHP, когда там ещё не было возможности указывать типы в сигнатуре функции (до PHP 5.0, 2004 год):

function process($user) {}

Очень похоже на нынешний JS, не правда ли?

После 5.0 стало можно писать так:

function process(User $user) {}

А после PHP 7.0 (2015) так:

function sum(int $a, int $b): int {}

Я уверен, что если бы не Microsoft с TypeScript, то и в JS уже давно была бы такая возможность. Но имеем, что имеем. В JS в 2026-м году нет ни интерфейсов, ни возможности указать типы в сигнатуре функций.

Поэтому вот - описываем контракты в коде. Как в PHP четвертьвековой давности.

А если нет, то - что?

Интерфейс - это описание ожиданий компонента на границе с его зависимостью.

Компонент ожидает, что "repository имеет функцию findNameById" - это его право. В данном случае ожидания выражены через код. Можете добавить JSDoc'и, если хотите подробностей. Мы сейчас про JS говорим, тут - вот так. В других языках контракты (ожидания) описываются по-другому.

Так бывает и без DI. Вероятно, вы проверяете то, что не нужно, а то, что нужно, не проверяете. Делайте наоборот.

Что именно вас смущает в этом коде?

export default function Service({ repository }) {
  return {
    async getProfile(id) {
      const name = await repository.findNameById(id);
      return { id, name };
    },
  };
}

Фабрика создаёт объект и инжектит в него зависимость. Обычный JS-код. Можете переписать его под классы, если вам больше нравятся классы.

А фабрика, в свою очередь, является реализацией DI на языке, в сочетании с замыканиями. И без всяких фреймворков.

Можно и так - вручную. Фреймворки лишь помогают унифицировать общие подходы в проекте. Или в проектах. Или в отдельной экосистеме. Всё зависит от масштаба.

Про "удержание стоимости верификации под контролем" было вообще больно читать.

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

Интерфейсы позволяют проверить компонент относительно контрактов (интерфейсов), а реализации - отдельно относительно этих контрактов. Поэтому вместо независимой проверки всех сочетаний получаем приблизительно аддитивную проверку частей плюс ограниченное число интеграционных тестов конкретных сборок.

"Программирование - безнадежно проигранная борьба с непреодолимой сложностью кода и вероломством требований" (с) Jonathan Edwards

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

"Ручной менеджмент токенов" - это лишь один из возможных вариантов управления зависимостями. Я, например, и мои агенты, пишем так (ну, или примерно так):

export default function Service({ repository }) {
  return {
    async getProfile(id) {
      const name = await repository.findNameById(id);
      return { id, name };
    },
  };
}

export const __deps__ = {
  repository: "App_User_Repository$",
};

При отсутствующей runtime-рефлексии в JS приходится "колхозить" runtime-рефлексию самому. Зависимости всех доступных снаружи экспортов отдельного es6-модуля прописываются в специальном экспорте __deps__. После этого анализ зависимостей модуля можно автоматизировать полностью. Синтаксис "языка токенов" может быть любым, я взял для себя namespace'ы из PHP Zend1 фреймворка. Главное, чтобы этот "язык" упаковывал в токене:

  • адрес npm-пакета и es6-модуля в нём;

  • имя экспорта в es6-модуле;

  • форму существования зависимости (singleton or transient).

После этого достаточно в настройке контейнера в Composition Root привязать физическое расположение пакета (в локальной файловой системе или в вебе) к соответствующему namespace'у (App_ или App_User_) и разрешение зависимостей из __deps__ происходит автоматически, как в C#, Java, PHP, ...

Решение с __deps__, кстати, мне ИИ подсказал. Это логика "силиконовых", а не "кожаных". Им так норм.

Сделал при помощи codex-агента скилл на основе этой публикации - architecture-foundations. Можно попробовать применить с ИИ-агентом (claude, codex, opencode,...). Установка через npx :

npx skills add flancer32/skill-architecture-foundations --skill architecture-foundations

или вручную, через копирование каталога architecture-foundations в ~/.agents/skills/.

Интересно попробовать описанные в публикации оси для анализа своих проектов.

Сложность: Средний

Спасибо, посмеялся. Я уже на одной четверти текста перестал связывать одно с другим, а для тиктокера это вообще непролазный лес! Но в целом - я в восторге!! Никогда не смотрел на разработку программ в таком ключе. Получилось очень интересно и познавательно (ну, там, где я понял о чём речь, разумеется :))) ).

Попросил ИИшечку привести пост к моему когнитивному уровню - вроде стало чуть полегче. Выкладываю выжимку, может кому сгодится. Там, правда, под меня "заточено", но каждый может перегнать страницу в PDF, скормить "своей" ИИшке и получить трансляцию идей ТС'а "под себя".

Вот "под меня"

Интересная статья, но, на мой взгляд, её ценность не в том, что программирование буквально «доказывает» Платона, Аристотеля или Лейбница. Философские параллели здесь скорее язык, позволяющий разложить архитектурные решения по независимым вопросам.

В упрощённом виде автор предлагает спрашивать о системе следующее:

  1. Где находится общее: в явно объявленном типе, в фактической структуре объекта или только в именах и соглашениях?

  2. Что первично: устойчивый объект с изменяемым состоянием или поток событий, из которого состояние каждый раз выводится?

  3. Кто может менять состояние: любой обладатель ссылки или только сама сущность через обработку сообщений?

  4. Сохраняется ли форма в runtime: может ли система во время исполнения распознать тип, схему и смысл полученного сообщения?

  5. Кто владеет исполнением: внешний поток, единый event loop, coroutine, actor или отдельный процесс?

  6. Где находится цель: только у пользователя либо представлена внутри системы и самостоятельно ею преследуется?

Из этого получается несколько вполне практических выводов.

Первый: главная проблема конкурентности — сочетание нескольких исполнителей с общим изменяемым состоянием. Лечить её можно в двух независимых направлениях: убрать изменяемость или убрать совместное владение. Отсюда два устойчивых подхода: свободно разделяемые immutable-значения и изолированное mutable-состояние с единственным владельцем.

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

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

Четвёртый: event sourcing не отменяет объекты, а переносит источник истины. В обычном ООП истина находится в текущем состоянии объекта. В event-sourced-системе — в журнале, а объект становится проекцией истории. Это полезно там, где важны аудит, восстановление, повторная обработка и объяснимость, но не является универсально лучшей моделью.

Пятый: runtime-системе необязательно иметь рефицированные типы самого языка, но ей необходимо какое-то доступное представление протокола — schema, discriminator, валидатор, таблица диспетчеризации или сгенерированный декодер. Замкнутый компонент может самостоятельно интерпретировать сообщение только тогда, когда формат сообщения представлен во время исполнения.

Шестой: автономность исполнения и наличие цели — разные свойства. Actor может иметь собственный цикл обработки и ни к чему не стремиться. Оптимизатор может преследовать цель, оставаясь полностью управляемым внешней петлёй. Для агентных систем это различие принципиально: отдельно должны проектироваться движитель, цель, полномочия и возможность её коррекции.

Наиболее сильной мне кажется последняя часть статьи про LLM-агентов. В обычной системе сообщение и код разделены: данные не должны переписывать поведение обработчика. У языкового агента и инструкции, и данные, и описание цели представлены одним субстратом — текстом. Поэтому message passing сам по себе не обеспечивает изоляции: сообщение может изменить не состояние, а интерпретацию правил и цели.

Практическое следствие — на внутренних границах агентных систем свободный естественный язык, вероятно, должен уступать место более жёстким протоколам:

  • явно заданному типу сообщения;

  • разделению данных, инструкций и системных правил;

  • валидируемому payload;

  • ограниченным capabilities;

  • аудируемому журналу действий;

  • отдельной процедуре изменения цели и полномочий.

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

При этом я бы осторожнее относился к сильным тезисам статьи. Аналогия между actor и монадой или между Paxos и предустановленной гармонией красива, но сама по себе ничего не доказывает. Заявленная ортогональность осей тоже не абсолютна: в реальных языках и runtime эти свойства часто сцеплены. А «закон сложности» работает только тогда, когда систему действительно можно разрезать по узким границам. Плохо декомпозированная распределённая система легко оказывается сложнее хорошего модульного монолита.

Поэтому я бы сформулировал практическое ядро статьи так:

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

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

В любом случае, есть над чем подумать. Спасибо за статью! (y)

Какую проблему вы решаете с помощью DI?

Лично я решаю с помощью DI проблему "распределённой разработки кода слабосвязанными командами разработчиков". Я очень быстро забываю контекст проекта. Через полгода уже не помню ничего, если в течение этого времени не заглядывал код. Так что "я сейчас" и "я через год" - это и есть слабосвязанные команды разработчиков :)

А в принципе, весь IoC для этого был придуман. DI - лишь один из вариантов.

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

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

DI - это про внедрение зависимостей. Зависимости есть в любом коде, состоящем из более, чем одного файла. Статические импорты в JS - это тоже зависимости.

DI в JS можно вообще без классов использовать:

// src/App/Config.mjs

export default () => ({
    greeting: 'Hello',
    punctuation: '!',
});
// src/App/FormatName.mjs

export default () => (name) => {
    const value = name.trim().toLowerCase();
    return value.charAt(0).toUpperCase() + value.slice(1);
};
// src/App/Greet.mjs

export const __deps__ = {
    default: {
        config: 'App_Config$',
        formatName: 'App_FormatName$',
    },
};

export default ({config, formatName}) =>
    (name) => `${config.greeting}, ${formatName(name)}${config.punctuation}`;
// bootstrap.mjs

import Container from '@teqfw/di';

const container = new Container();

container.addNamespaceRoot(
    'App_',
    new URL('./src/App/', import.meta.url).href,
    '.mjs'
);

const greet = await container.get('App_Greet$');

console.log(greet('  aLEX  '));

Вот архив с кодом - достаточно распаковать, затем npm install & npm start.

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

1
23 ...

Информация

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

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

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