В статье опущена самая важная часть. Поделитесь, пожалуйста, изначальным промтом. Если распишете все шаги, то буду премного благодарен. Если оно заняло всего 10 минут, выписать готовое решение должно быть еще быстрее.
Луковая архитектура - это отличный способ сделать из простой задачи сложную, а из сложной - невыносимо сложную. Код надо структурировать на основе логических связей, а не по формальным признакам.
Удивительно, что преимущества луковой архитектуры, которые приводятся в статье, гораздо лучше достигаются, если этому паттерну не следовать. Если у вас вся логика эндпоинта прописана в обработчике, а не разбита на 5 слоёв, то любые её правки будут локализованными. Не нужно будет в голове собирать пазл из множества кусочков, а после правок перепроверять, что еще задето этими изменениями. Естественно, держать всю логику в эндпоинте - это не правило. Еще раз: надо смотреть на логические связи. Если логика дублируется, конечно, ее нужно делать переиспользуемой. Если кусок кода имеет сильные внутренние связи и слабые внешние - его можно отделить.
Отдельно обращу внимание, что эта методология нарушает причинность, что часто можно увидеть по уродливым интерфейсам нижних слоёв. Слои, обращающиеся за данными, не зависят от верхних только формально, по коду. На деле же их логика часто не является независимой, это просто вырванный кусок. Поэтому часто ему очень сложно придумать осмысленный нейминг.
Во-первых, не останется, потому что delete же не замокан. Но важнее другое: ничто не мешает подменить весь repo на тот же самый тестовый дублёр. Вам не нужен DI для этого.
В случае DI всё происходит явно, все зависимости всегда видны в списке параметров.
Тут мы говорим о разном. Я имел ввиду явность самой подмены кода. Давайте коротко опишу картину целиком.
У вас есть что-то такое:
def a(x: int): return external.b(x)
Вам нужно подменить b в тестах. Для этого вы переписываете код на:
Во-первых, код просто стал сложнее. Во-вторых, сразу появляется вопрос: а какие именно значения у 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, эта идея прям лежит на поверхности. Но потом возникает вопрос: а как это внедрить в больших гетерогенных проектах? На этом я в статье и пытался сфокусироваться.
Идея в том, что не надо мучиться с натягиванием OpenAPI на свой проект, а ввести свой язык описания и кодогенераторы, согласованные с вашей архитектурой.
Да нет же. "В идеале" они должны переименовываться именно вместе. А вот всякие особенности проекта уже могут мешать нам это сделать. И тогда архитектура, конечно, не должна этому препятствовать.
Да, это немного словоблудие, но за ним есть и практический смысл. Нужно различать причины, по которым вы не хотите переименовывать столбец. Если эти причины устранимы, то с ними нужно бороться и обеспечить быстрое и простое изменение схемы бд. Например, если вы избегаете переименования из-за того, что у вас нет типизации кода, или миграции бд нужно писать руками, то проблема вовсе не в связанности бэкенда с фронтендом.
То что вы говорите для многих тоже как догма. Но посмотрите под другим углом: если поле на фронте нужно переименовывать, то почему в базе должно оставаться старое название? Ситуации могут быть очень разные, но кажется, что вы хотите вместо переименовывания столбца оставить техдолг. У поля со временем изменился смысл или просто нашелся более подходящий нейминг, а вы хотите в базе оставить устаревшее название, потому что так проще.
Прошу воспринимать без категоричности. Повторю, что проекты могут быть разные, где-то переименовывать столбец действительно очень дорого. На нашем проекте мы придерживаемся архитектуры, при которой можно делать и так, как вы описываете (максимально избегать повторений) и с легкостью отойти от схемы базы, не добавляя при этом бойлерплейта.
Ребят, привет! Джва года ждал этой статьи. Пока ожидания оправдываются, очень интересно.)
Больно слышать про тормоза при сериализации в протобаф.) Не копали причины? Просто питоновая либа медленная? Вроде ж в низкоуровневых языках протобаф быстрее.
Не сравнивали с ts-sql-query? Выглядит очень похоже, только подходы немного разные. На первый взгляд кажется, что типизация киселя через строки лаконичнее, но где-то может быть ограничена в возможностях. Есть запросы, которые нельзя на нем составить или которые не валидируются по типам?
А что делать, если для реакции не хватает только той информации, что есть в базе? Допустим, если нужно еще знать пользователя, который внес эти изменения?
В таком случае её можно было и не решать вообще. Переименовывание и так достаточно редкий кейс. И, повторюсь, при вашем подходе, вы с большой вероятностью забудете поправить `as` импорты, потому что будете полагаться на IDE.
Ну, во-первых, таким импорты будут явно указаны,
Опять это слово «явно». Чем `import Name from` неявен? Тут явно написано, что объект, экспортируемый по дефолту, нужно импортировать с именем Name.
плюс в начале файла можно будет увидеть родное название.
И в дефолтном импорте можно увидеть название файла.
Какие именно «новые» проблемы созданы?
Отсутствие строгой системы в именовании.
Важно писать так, чтобы они были уникальные в местах, где используются.
Т.е. вы про каждый экспорт должны думать, а где и с чем вместе он будет использоваться? Экспорт — такое же локальное имя, как и всё остальное в файле. Он находится в том же смысловом контексте, и должен именоваться аналогично со всем остальным, без добавления контекста в имя. Иначе возникает неконсистентность в именах, которая всё равно не избавит от коллизий имён, а значит `as` придётся использовать, и все проблемы никуда не деваются. Вы не решили проблему, а замели пыль под кровать.
Использование именованных экспортов не решает эту проблему полностью. Импорты с "as" тоже придется править руками. И на практике, скорее всего, их просто забудут исправить. Вместо решения, вы бежите от проблемы, создавая новые.
Полный путь к файлу создает уникальный скоуп для всего содержимого. Всё в файле должно именоваться относительно этого контекста. Тавтология не создаст глобально уникальных имен (если только вы не в несете полный путь в каждое имя).
В статье опущена самая важная часть. Поделитесь, пожалуйста, изначальным промтом. Если распишете все шаги, то буду премного благодарен. Если оно заняло всего 10 минут, выписать готовое решение должно быть еще быстрее.
Луковая архитектура - это отличный способ сделать из простой задачи сложную, а из сложной - невыносимо сложную. Код надо структурировать на основе логических связей, а не по формальным признакам.
Удивительно, что преимущества луковой архитектуры, которые приводятся в статье, гораздо лучше достигаются, если этому паттерну не следовать. Если у вас вся логика эндпоинта прописана в обработчике, а не разбита на 5 слоёв, то любые её правки будут локализованными. Не нужно будет в голове собирать пазл из множества кусочков, а после правок перепроверять, что еще задето этими изменениями. Естественно, держать всю логику в эндпоинте - это не правило. Еще раз: надо смотреть на логические связи. Если логика дублируется, конечно, ее нужно делать переиспользуемой. Если кусок кода имеет сильные внутренние связи и слабые внешние - его можно отделить.
Отдельно обращу внимание, что эта методология нарушает причинность, что часто можно увидеть по уродливым интерфейсам нижних слоёв. Слои, обращающиеся за данными, не зависят от верхних только формально, по коду. На деле же их логика часто не является независимой, это просто вырванный кусок. Поэтому часто ему очень сложно придумать осмысленный нейминг.
Во-первых, не останется, потому что
deleteже не замокан. Но важнее другое: ничто не мешает подменить весьrepoна тот же самый тестовый дублёр. Вам не нужен DI для этого.Тут мы говорим о разном. Я имел ввиду явность самой подмены кода. Давайте коротко опишу картину целиком.
У вас есть что-то такое:
Вам нужно подменить
bв тестах. Для этого вы переписываете код на:Во-первых, код просто стал сложнее. Во-вторых, сразу появляется вопрос: а какие именно значения у
bмогут быть в продакшене? Сюда действительно приходят разные функции или это сделано только для тестирования? В-третьих, вместо явного `with patch`, в тесте моки видны только по неймингу. Сравните:И
Сразу подчеркну, что сам по себе DI никак не влияет на связность между
aиb. То, чтоaполучаетbпри вызове снаружи, не значит, что они не приколочены гвоздями друг к другу. На любое изменениеaпридётся менятьbи на любое изменениеbпридётся менятьa. Это может быть нормально, а может быть и плохо. Но DI на это никак не влияет, и при этом обманывает читателя, якобыaможет работать с чем-то, кромеb.Приведите, пожалуйста, пример конкретной проблемы. Забегая вперёд скажу: тесты - это не продуктовый код. Причины, по которым что-то плохо работает в продуктовом коде, могут быть совершенно нерелевантны для тестов. Поэтому важно докопаться до сути, а не опираться на чьи-то представления о хорошем и плохом. И я, честно, это и пытаюсь сделать.
Я сфокусируюсь на манки-патчинге.
Пожалуйста, отбросьте все догмы и чужие мнения и вдумайтесь в логику того, что вы говорите. Цель - "протестировать код изолированно". Под эту цель вы предлагаете переписать продуктовый код. И это - не обходной путь? Ещё раз. Вам нужно протестировать код. Чтобы это сделать, код нужно переписать. Это точно хороший дизайн или всё-таки недостаток инфраструктуры?
Простите, я не понял, какие изменения имеются в виду. Вы не могли бы привести короткие примеры? Сразу скажу следующий вопрос: а во всех этих случаях, что инжектируемые моки исправлять разве не придётся? Мне не понятно, в каких ситуациях тест с подменой через манки-патчинг может сломаться, а подмена через DI при этом не сломается. Они же подменяют одни и те же интерфейсы. Только манки-патчинг выражает это явно, а использование DI заставляет нарушать связи в коде.
Вся статья опирается на бездоказательные тезисы. Чем плоха "перевёрнутая" пирамида тестирования? Чем плох манки-патчинг? Что мешает сделать высокоуровневые абстракции и не использовать инверсию контроля?
Рекомендую особенно крепко задуматься про манки-патчинг. Использование его в тестах действительно пораждает какие-то проблемы или это просто догматизм?
Спасибо за комментарий, было интересно ознакомиться. На самом деле, у меня в проекте почти в точности реализовано всё то же, что в вышем фреймворке. И я видел описания похожих архитектур ещё пару раз. Кажется, что для проектов, целиком построенных на TS, эта идея прям лежит на поверхности. Но потом возникает вопрос: а как это внедрить в больших гетерогенных проектах? На этом я в статье и пытался сфокусироваться.
Разрешите, я оставлю ссылку на свою вчерашнюю статью: https://habr.com/ru/articles/1043948/
Идея в том, что не надо мучиться с натягиванием OpenAPI на свой проект, а ввести свой язык описания и кодогенераторы, согласованные с вашей архитектурой.
В наксте хуком называется совершенно другое. Это компосаблы.
Да нет же. "В идеале" они должны переименовываться именно вместе. А вот всякие особенности проекта уже могут мешать нам это сделать. И тогда архитектура, конечно, не должна этому препятствовать.
Да, это немного словоблудие, но за ним есть и практический смысл. Нужно различать причины, по которым вы не хотите переименовывать столбец. Если эти причины устранимы, то с ними нужно бороться и обеспечить быстрое и простое изменение схемы бд. Например, если вы избегаете переименования из-за того, что у вас нет типизации кода, или миграции бд нужно писать руками, то проблема вовсе не в связанности бэкенда с фронтендом.
То что вы говорите для многих тоже как догма. Но посмотрите под другим углом: если поле на фронте нужно переименовывать, то почему в базе должно оставаться старое название? Ситуации могут быть очень разные, но кажется, что вы хотите вместо переименовывания столбца оставить техдолг. У поля со временем изменился смысл или просто нашелся более подходящий нейминг, а вы хотите в базе оставить устаревшее название, потому что так проще.
Прошу воспринимать без категоричности. Повторю, что проекты могут быть разные, где-то переименовывать столбец действительно очень дорого. На нашем проекте мы придерживаемся архитектуры, при которой можно делать и так, как вы описываете (максимально избегать повторений) и с легкостью отойти от схемы базы, не добавляя при этом бойлерплейта.
Это можно. Но вместо throw нужно использовать функцию `(ex) => {throw ex}`
А почему секунду не определят так, чтобы в среднем сутки были 24 часа? Или длительность суток настолько нестабильна, что это нельзя сделать?
Ребят, привет! Джва года ждал этой статьи. Пока ожидания оправдываются, очень интересно.)
Больно слышать про тормоза при сериализации в протобаф.) Не копали причины? Просто питоновая либа медленная? Вроде ж в низкоуровневых языках протобаф быстрее.
Я не новорег. Можете мне рассказать, что за тренды?
Не сравнивали с ts-sql-query? Выглядит очень похоже, только подходы немного разные. На первый взгляд кажется, что типизация киселя через строки лаконичнее, но где-то может быть ограничена в возможностях. Есть запросы, которые нельзя на нем составить или которые не валидируются по типам?
А что делать, если для реакции не хватает только той информации, что есть в базе? Допустим, если нужно еще знать пользователя, который внес эти изменения?
Подскажите, пожалуйста, как вы обесточиваете плиту? Контактором?
В таком случае её можно было и не решать вообще. Переименовывание и так достаточно редкий кейс. И, повторюсь, при вашем подходе, вы с большой вероятностью забудете поправить `as` импорты, потому что будете полагаться на IDE.
Опять это слово «явно». Чем `import Name from` неявен? Тут явно написано, что объект, экспортируемый по дефолту, нужно импортировать с именем Name.
И в дефолтном импорте можно увидеть название файла.
Отсутствие строгой системы в именовании.
Т.е. вы про каждый экспорт должны думать, а где и с чем вместе он будет использоваться? Экспорт — такое же локальное имя, как и всё остальное в файле. Он находится в том же смысловом контексте, и должен именоваться аналогично со всем остальным, без добавления контекста в имя. Иначе возникает неконсистентность в именах, которая всё равно не избавит от коллизий имён, а значит `as` придётся использовать, и все проблемы никуда не деваются. Вы не решили проблему, а замели пыль под кровать.
Использование именованных экспортов не решает эту проблему полностью. Импорты с "as" тоже придется править руками. И на практике, скорее всего, их просто забудут исправить. Вместо решения, вы бежите от проблемы, создавая новые.
Полный путь к файлу создает уникальный скоуп для всего содержимого. Всё в файле должно именоваться относительно этого контекста. Тавтология не создаст глобально уникальных имен (если только вы не в несете полный путь в каждое имя).