Pull to refresh
2
Алексей Балехов@Balek

Автоматизация и интеграция

1
Subscribers
Send message

Конечно, давайте закончим дискуссию. Разрешите только поясниться за приведённую цитату, поскольку вы в ней увидели что-то оскорбительное.

saveName - это первый урок по программированию для детей.

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

А вот этот уровень пока не перешагнули.

Давайте не будем опускаться до оскорблений. Я пишу эмоционально, но атакую идею, а не вас лично. Если тезис про необъективность задел вас, то объясню, что имею в виду.

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

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

Что будет, если сделать import ./storage.js до того, как сделать vi.mock('./storage.js'...)? Например, в setupFiles? Статические импорты - они такие.

Вы даже эксперимент с setupFiles в vitest не поставили.

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

Мне удобнее писать JS-код так.

В этом и проблема. Вы смотрите на проблему сквозь ваши привычки, и не пытаетесь посмотреть на неё объективно. Попробую ещё раз заострить ваше внимание:

  • Между изначальным saveName с явным импортом и createSaveName драмматическая разница в синтаксической сложности. saveName - это первый урок по программированию для детей. createSaveName использует более сложные концепции. Вы сделали продуктовый код сложнее просто по конструкции.

  • Ясно, что эту конструкцию вы привыкли быстро считывать. Но она сложнее не только по написанию. Она из явного импорта, явного указания на конкретную зависимость, с которой работает saveName, делает неявную. Для каждой такой зависимости сразу возникает вопрос: а какие имплементации интерфейса вообще существуют и могут сюда приходить? Предвидя ваше возражение, сражу скажу, что на связность кода это никак не влияет. Сама по себе возможность подменить сторадж никак не делает слабее связность между saveName и ./storage.js . Если программист в логике гвоздями приколотил одно к другому (что не обязательно плохо), то вся эта инверсии контроля - просто обман читателя кода.

  • Усложнение навигации по коду - то ещё удовольствие. Вместо того, чтобы сразу перейти к зависимости, вам нужно прыгнуть на интерфейс, а с него на продовую реализацию.

  • Тест с манкипатчингом (да, vitest, jest, sinon, jasmine - все тестовые фреймворки используют манкипатчинг уже точно больше 10 лет), напротив, более явно выражает, какие зависимости подменяются в тесте. Но здесь разница не столь критична.

И вы всё ещё пока не ответили про интерфейсы и cross-cutting функциональность.

Простите, я не понял вопрос. Зачем мне рантайм-интерфейсы, если я не использую DI? Задачи cross-cutting решаются очень по-разному. Приведите конкретный пример.

const saved = [];

vi.mock('./storage.js', () => ({
  async save(value) {
    saved.push(value);
  },
}));

it('saves normalized value', () => {
  await saveName(' Alex ');
  expect(saved).toEqual(['alex']);
});

https://vitest.dev/guide/mocking

Поэтому и говорят, что в JavaScript DI не нужен. Хотя лучше сказать, что он нигде не нужен. К тестам совершенно не такие требования, как к продуктовому коду. Вполне уже можно было изобрести какую-то магию с подменами и для Java. Но вместо этого джависты коверкают и тащат магию в продуктовый код.

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

Эта статья как будто должна отвечать на мой вопрос, но в очередной раз приводит одну единственную проблему, решаемую с помощью DI - тестирование. Поэтому в очередной раз должен задать стандартный вопрос: чем вам не угодил манкипатчинг для тестирования? Используйте мокинг, чтобы подменять ./storage.js и не придётся усложнять продуктовый код.

Можно вас попросить привести нормальный пример, когда просто импорта зависимости не хватает и вам нужен именно DI? Какую проблему вы решаете с помощью DI?

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

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

Удивительно, что преимущества луковой архитектуры, которые приводятся в статье, гораздо лучше достигаются, если этому паттерну не следовать. Если у вас вся логика эндпоинта прописана в обработчике, а не разбита на 5 слоёв, то любые её правки будут локализованными. Не нужно будет в голове собирать пазл из множества кусочков, а после правок перепроверять, что еще задето этими изменениями. Естественно, держать всю логику в эндпоинте - это не правило. Еще раз: надо смотреть на логические связи. Если логика дублируется, конечно, ее нужно делать переиспользуемой. Если кусок кода имеет сильные внутренние связи и слабые внешние - его можно отделить.

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

тест всё равно останется зелёным

Во-первых, не останется, потому что delete же не замокан. Но важнее другое: ничто не мешает подменить весь repo на тот же самый тестовый дублёр. Вам не нужен DI для этого.

В случае DI всё происходит явно, все зависимости всегда видны в списке параметров.

Тут мы говорим о разном. Я имел ввиду явность самой подмены кода. Давайте коротко опишу картину целиком.

У вас есть что-то такое:

def a(x: int): return external.b(x)

Вам нужно подменить b в тестах. Для этого вы переписываете код на:

def a(b: Callable[[int], int], x: int): return b(x)

Во-первых, код просто стал сложнее. Во-вторых, сразу появляется вопрос: а какие именно значения у b могут быть в продакшене? Сюда действительно приходят разные функции или это сделано только для тестирования? В-третьих, вместо явного `with patch`, в тесте моки видны только по неймингу. Сравните:

with patch("external.b", side_effect=lambda x: x):
  a(1)

И

b_mock = lambda x: x
a(b_mock, 1)

Сразу подчеркну, что сам по себе DI никак не влияет на связность между a и b. То, что a получает b при вызове снаружи, не значит, что они не приколочены гвоздями друг к другу. На любое изменение a придётся менять b и на любое изменение b придётся менять a. Это может быть нормально, а может быть и плохо. Но DI на это никак не влияет, и при этом обманывает читателя, якобы a может работать с чем-то, кроме b.

Monkey patching, наоборот, требует знать внутреннее устройство модуля: где именно импортирована зависимость, под каким именем она лежит, в каком месте её нужно подменить.

Приведите, пожалуйста, пример конкретной проблемы. Забегая вперёд скажу: тесты - это не продуктовый код. Причины, по которым что-то плохо работает в продуктовом коде, могут быть совершенно нерелевантны для тестов. Поэтому важно докопаться до сути, а не опираться на чьи-то представления о хорошем и плохом. И я, честно, это и пытаюсь сделать.

Я сфокусируюсь на манки-патчинге.

Monkey patching - это сигнал плохого дизайна. Это обходной путь, чтобы протестировать код изолировано.

Пожалуйста, отбросьте все догмы и чужие мнения и вдумайтесь в логику того, что вы говорите. Цель - "протестировать код изолированно". Под эту цель вы предлагаете переписать продуктовый код. И это - не обходной путь? Ещё раз. Вам нужно протестировать код. Чтобы это сделать, код нужно переписать. Это точно хороший дизайн или всё-таки недостаток инфраструктуры?

Ещё он плох тем, что часто прибивает тест к внутренностям реализации. Переименовал импорт, перенёс объект, изменил способ сборки зависимости - тест сломался, хотя поведение не изменилось.

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

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

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

Спасибо за комментарий, было интересно ознакомиться. На самом деле, у меня в проекте почти в точности реализовано всё то же, что в вышем фреймворке. И я видел описания похожих архитектур ещё пару раз. Кажется, что для проектов, целиком построенных на TS, эта идея прям лежит на поверхности. Но потом возникает вопрос: а как это внедрить в больших гетерогенных проектах? На этом я в статье и пытался сфокусироваться.

Разрешите, я оставлю ссылку на свою вчерашнюю статью: https://habr.com/ru/articles/1043948/

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

В наксте хуком называется совершенно другое. Это компосаблы.

Да нет же. "В идеале" они должны переименовываться именно вместе. А вот всякие особенности проекта уже могут мешать нам это сделать. И тогда архитектура, конечно, не должна этому препятствовать.

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

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

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

Это можно. Но вместо throw нужно использовать функцию `(ex) => {throw ex}`

Средняя длительность солнечных суток 2000 года была равна 86400,002 секунды, то есть убегание всего на 2 миллисекунды в год

А почему секунду не определят так, чтобы в среднем сутки были 24 часа? Или длительность суток настолько нестабильна, что это нельзя сделать?

Ребят, привет! Джва года ждал этой статьи. Пока ожидания оправдываются, очень интересно.)

Больно слышать про тормоза при сериализации в протобаф.) Не копали причины? Просто питоновая либа медленная? Вроде ж в низкоуровневых языках протобаф быстрее.

Я не новорег. Можете мне рассказать, что за тренды?

1
23 ...

Information

Rating
Does not participate
Location
Москва, Москва и Московская обл., Россия
Registered
Activity