Форум
Архитектура программного обеспечения
Тема
Как правильно задавать вопросы
B
I
abc
U
X
3
X
3
H1
H2
H3
H4
H5
H6
Asm
C/C++
C#
Erlang
Haskell
IDL
Java
Lisp
MSIL
Nemerle
ObjC
OCaml
Pascal
Perl
PHP
Prolog
Python
Ruby
Rust
SQL
VB
Здравствуйте, ·, Вы писали: ·>Здравствуйте, Sinclair, Вы писали: S>>Всё правильно он пишет. Вы заменили прямолинейный понятный код, в котором негде сделать ошибку, на код с распределённой ответственностью. Теперь у вас Time.now() расположен максимально далеко от места его использования. ·>Где можно сделать ошибки - я описывал. Проблемы с Time.now я тоже описывал. Несколько раз. Вы на это просто закрываете глаза. S>>·>:facepalm: а в вашем стиле [tt]lastMonday(LocalDateTime.now())[/tt] и [tt]nextFriday(LocalDateTime.now())[/tt] можно вызывать, да?! :)) S>>Можно, но так никто не делает. Потому, что то место, где они нужны - это тоже функция. И в неё по умолчанию не передаётся ссылка на instantSource, а передаются готовые аргументы start и end. Не её дело принимать решения о том, где начало интервала, а где - конец. S>>Поэтому мы в неё передаём [i]значения[/i]. А значения - тоже вычисляются [i]чистой[/i] функцией. Чистота означает, что мы не делаем из неё грязных вызовов - поэтому никаких [tt]lastMonday(LocalDateTime.now())[/tt] и [tt]nextFriday(LocalDateTime.now())[/tt], а исключительно [tt]lastMonday(currentTime)[/tt] и [tt]nextFriday(currentTime)[/tt]/ ·>Это очевидный детский сад. Я говорю о коде, где будет стоять LocalDateTime.now(). И как это место протестировать? S>>·>Ну ясен пень. Это слишком простой вопрос, не стоящий обсуждения. Ну да, мы должны получить некий момент и от него плясать по бизнес-логике. Скажем, тот же TradeDate нужно взять только один раз для данного ордера, в момент его прихода в handler, запомнить его и плясать от него далее, вычисляя всякие там settlement date, days to maturity и т.п. S>>Ну вы же так не делаете. ·>Делаю конечно, не фантазируй. Я говорю об "острых краях" приложения, а ты о скучной рутине внутри. Как именно покрывать максимальное количество кода легковесными быстрыми тестами, чтобы подавляющее большинство ошибок ловить ещё до коммита, а не после "успешного" деплоя на прод. S>>Ну, пока что ваш код не удовлетворяет предлагаемым вами же требованиям ;) Напомню, что ваш кэш знать не знает ни про какой event log. И возвращает всегда локальное поле, а чтобы его обновить, нужно срабатывание таймера. Таймер ваш никак с instantSource не связан, это прямая дорога к рассогласованию. ·>Связан, конечно. S>>·>Я в первую очередь говорю о главном применении моков - отрезать входы-выходы системы (тот самый Time.Now, а ещё сеть, фс, бд, етс), которые в реальности завязываются на что-то тяжело контролируемое или очень медленное. S>>Ок, давайте откажемся от обсуждения времени - в конце концов, сделать lastFriday достаточно медленной, чтобы наше обсуждение имело смысл, скорее всего не получится. ·>Ну lastFriday это довольно условный пример. И медленность там может много откуда взяться. Да даже тупо проверка всех таблиц таймзон, dst-переходов, а ещё можно придумать логику хождения в бд для проверки праздников и т.п. - внезапно и не такой уж скучный пример. S>>Правильный способ отрезания входов-выходов - ровно такой, как настаивает коллега Pauel. Потому что в вашем способе вы их никуда не отрезаете - вы их заменяете плохими аналогами. Ващ подход мы, как и все на рынке, применяли в бою. И неоднократно обожглись о то, что мок не мокает настоящее решение; он мокает [i]ожидания разработчика[/i] об этом решении. ·>Ожидания разработчика берутся не из пустого места, а из фиксации поведения боевой системы. Ну по крайней мере если использовать моки правильно, а не так как Pauel думает их используют. S>>·>Вот это я не понял. У него в примере тест буквально сравнивал текст sql "select * ...". Как такое поймает это? S>>Это он просто переводит с рабочего на понятный. Он же пытался вам объяснить, что проверяет [i]структурную эквивалентность[/i]. Тут важно понимать, какие компоненты у нас в пайплайне. ·>При этом он меня обвинил в том, что мои тесты проверяют как написано, а не ожидания пользователей. А бревно в глазу не замечаете - пользователям-то похрен какая-то там эквивалентность у вас, структурная или не очень. S>>Если вы строите текст SQL руками - ну да, тогда будет именно сравнение текстов. Но в наше время так делать неэффективно. Обычно мы порождаем не текст SQL, а его AST. Генерацией текста по AST занимается отдельный компонент (чистая функция), и он покрыт своими тестами - нам их писать не надо. ·>И накой сдалась эта ваша AST вашим пользователям? А что если завтра понадобится заменить вашу AST на очередной TLA, то придётся переписывать все тесты, рискуя всё сломать. S>>·>Суть в том, что программист пишет код+тест. В тесте вручную прописывает ожидание [tt]=="sellect * ..."[/tt]. Потом тест, ясен пень проходит, потом ревью, который тоже наверняка пройдёт, не каждый ревьювер способен заметить лишнюю букву. S>>Ну, во-первых, программист же пишет код не в воздухе. Как правило, сначала нужный SQL пишется руками и проверяется интерактивно с тестовой базой, пока не начнёт работать так, как ожидается. Лично я работаю с SQL примерно 30 лет, из них 10 проработал [i]очень[/i] плотно. Но и то - сходу не всегда могу написать корректный SQL. Поэтому мы сначала пишем несколько характерных вариантов запроса и прогоняем их на базе (в том числе - используем ровно тот движок, с которым работает прод. Никаких замен боевого постгре на тестовый SQLite или ещё каких-нибудь моков.) S>>Потом вставляем готовые команды в тест. Потом пишем код. ·>Именно! Против этих всех ручных пассов я и возражаю. Верю, что писать запросики в консоли и копипастить в код было ещё ок 30 лет назад... но пора сказать стоп! Ровно так же можно подключаться из тестов к какому хошь движку и напрямую _автоматически_ выполнять запросы и _автоматически_ ассертить результаты чем вручную копипастить туда-сюда из консоли в код и проверять глазками, похож ли результат ли на правду. А если sqlite (есть кстати ещё h2 которая неплохо умеет притворяться популярными субд) чем-то не устраивает, то запустить [i]с точностью до бита идентичный[/i] проду постре что локально, что в CI-пайплайне для прогона тестов - дело минут в современном мире докеров. S>>·>И только потом, когда некий тест пройдёт от логина до отчёта, то оно грохнется, может быть, если этот тест таки использует именно эту ветку кода для тестовых параметров отчёта. Ведь итесты в принципе не могут покрыть все комбинации, их может быть просто экспоненциально дохрена. S>>У вас - та же проблема, просто вы её предпочитаете не замечать. Вы там запустили тест, а вместо опечатки sellect у вас там вызов функции, которая в inmemory DB работает, а в боевой - нет. SQL вообще состоит из диалектных различий чуть менее, чем полностью. ·>Эта проблема надумана. S>>·>С этим осторожнее. Вот этот твой кеш оказался - с состоянием, внезапно, да ещё и static global. S>>Да, такие вещи нужно делать аккуратно, вы правы. ·>Тут у вас вся ваша функциональщина и вылазит боком, т.к. кеш - это состояние, даже хуже того - шаред, по определению. И вся пюрешность улетучивается. Приходится нехило прыгать по монадам и прочим страшным словам. А в шарпах-ts где нет нормальной чистоты, только и остаётся отстреливать себе ноги. S>>>>Не знаю. А откуда у вас в CalendarLogic берётся instantSource? S>>·>Через DI/CI. S>>Ну вот он у вас в тестах - один, в продакшне - другой. Упс. ·>Суть в том, что тест от прода отличается ровно одной строчкой кода, в этом и цель всего этого. И строчка эта если и меняется, то раз в никогда. И если требует изменения - это становится явным и очевидным, требует более аккуратной валидации релиза. У вас же эта вся грязь будет в каждом методе каждого контроллера. S>>·>CompositionRoot - один на всё приложение, меняется относительно редко, обычно тупой линейный код, и составляет доли процента от объёма всего приложения. Его можно ежедневно глазками каждую строчку просматривать всей командой. S>>Ну, так это ровно тот же аргумент "против", который вы мне только что рассказывали. Вот вы взяли и заменили ваш код [i]глобально[/i], не разбираясь, где там можно применять кэширование, а где - нельзя. ·>Именно. Т.к. nextFriday() - всё сразу видно, всё на ладони - нужно заглянуть ровно в одно место чтобы понять можно ли применять кеширование и если можно то какое, и, во время релиза, не надо проверять каждый метод контроллера, а достаточно один сценарий, на то что хоть где-то этот кеш заработал ожидаемым способом, т.к. во всех остальных местах всё работает так же, т.к. код идентичный. S>>Потому что ваш кэширующий CalendarLogic [i]полагается[/i] на то, что в нём timeSource согласован с timer, а в коде это никак не выражено. Вот вы написали replay, который полагается на возможность замены глобального timeSource на чтение из eventLog, и погоняли его. И так получилось, что данные, на которых вы его гоняли, границу пятницы не пересекали. Откуда вы знаете, что нужно пересечь границу пятницы? Да ниоткуда, у вас в проекте таких "особых моментов" - тыщи и тыщи. Код коверадж вам красит весь ваш CalendarLogic зелёненьким, причин сомневаться в работоспособности нету. ·>Именно, что это ровно один компонент CalendarLogic на всё приложение, вся грязь и грабли рядышком. В этом и суть, что всё в одном месте, а не размазано по всему коду. S>>·>И вот такой модуль можно тестировать простыми тестами, подсовывая туда моковый timeSource и ровно этот же модуль будет зваться из "main" метода реального приложения, куда будет запихан SystemClock в качестве timeSource. S>>Проблема опять только в двух вещах: S>>1. Слишком много кода в стеке при выполнении тестов ·>Тесты, которые тестируют такой модуль, конечно, тяжелее чем типичные юнит-тесты, но они всё равно достаточно быстрые, т.к. никакого IO нет, всё внутри языка. S>>2. Слишком много доверия к тому, что моки адекватно изображают реальные компоненты. ·>Это ортогональная проблема. В тестировании пюрешек ровно эта же проблема тоже есть, для параметров. Тебе приходится верить, что твои тестовые параметры изображают реальные значения. Ещё раз, моки отличаются от параметров лишь способом доставки значений. Реальность данных относится именно к самим значениям, а не способу их передачи. S>>·>И это по всему коду - везде в тысячах мест. Да? И как это тестировать? Как делать replay? S>>Смотря как мы сохраняем историю для этого replay. Но в целом - код, в котором зашит Time.now(), replay не поддаётся. Это одна из его проблем. ·>Ясен пень, типичный синглтон же. S>>Поэтому у нас там будут вызовы не Time.now(), а какого-нибудь request.ArrivalTime. С самого начала, естественно. Обратите внимание - в каждом вызове время будет своё, и мне достаточно поднять реквест из лога и скормить его ровно тому же методу обработки. ·>А откуда в request появится ArrivalTime? И как все эти места протестировать? Учти, у нас 100500 различных видов request. S>>Вам придётся лепить костыли и шаманить, чтобы прикрутить магический instantSource, способный доставать время не из глобального RTC-источника, а из "текущего обрабатываемого запроса". ·>event sourcing. S>>·>Почему? У нас появляется требование считать nextFriday для платёжки - добавляем новый код для этого, другой код вообще можно не трогать. S>>Вы же только что поняли, что "другой код" всё же придётся трогать ;) ·>Ы? S>>·>Я обычно использую специальный тестовый шедулер, который запускает установленные таймеры при изменении тестового источника времени. У есть метод типа [tt]tickClock(7, DAYS)[/tt]. Код писать лень, но идея, надеюсь, очевидна... S>>Не, нифига не очевидна. В том-то и дело, что ваш тестовый "источник времени" - это мок, который только и умеет что отдавать заданную вами константу в ответ на now(). Написать его так, чтобы он умел корректно триггерить таймеры можно, но не факт, что вы про это вспомните вовремя ;) ·>Самое позднее когда я вспомню - это после упавшего локально теста, ещё до коммита. S>>·>Именно. Универсальность - не даёт делать более эффективую реализацию. S>>Даёт, даёт ;) ·>А ты попробуй. Над мемоизацией посмеялись, над статической глобальной переменной [tt]_cache[/tt] поплакали. Ещё идеи остались? S>>>>И это у вас прекрасно грохнется в продакшне, как только на машине сменят таймзону аккурат около полуночи пятницы. S>>·>Нет, конечно. Таймеры ставят на instant, т.е. абсолютный момент времени, независящий от зоны. S>>Вот именно об этом я и говорю. Инстант не наступил, а nextFriday уже должна возвращать новое значение. Упс. ·>Шозабред? Этот инстант логически эквивалентен твоему [tt]_cache.nextFriday[/tt]. В любом случае, это всё элементарно покрывается юнит-тестами (ага, с моками), даже если я и налажал код в браузере правильно написать. S>>·>Ну я код в браузере набирал, мог и ошибиться. В реальности буду запускать тесты, ясен пень. S>>Так у вас и тесты ничего не покажут. ·>Шозабред. S>>·>Через моки же - возвращай что хошь в каком хошь порядке. S>>Ну так для начала надо захотеть. Вы - не захотели. ·>Для моей реализации это и не надо. S>>·>В сымсле ботва с таймзонами? Ну это у меня уже привычка - писать тесты для дат около всяких dst переходов и с экзотическими таймзонами. S>>Ботва с таймзонами очень простая: у вас один и тот же instantSource() сначала возвращает "01:00 пятницы", а потом - "17:00 четверга". Просто пользователь перелетел через Атлантику, делов-то. ·>Это ты наверное имеешь в виду, если wall clock поменяет таймзону. Только это невозможно в моём случае по дизайну. Или ты не понимаешь что такое instant. Это конретная точка на линии физического времени, а не показания на циферблате часов типа "01:00 пятница" для человеков. ·>Хотя да, это наверное может поломаться, если эффекты СТО учитывать... S>>·>Ну смысле если мы из одного юнита А перенесли код в другой юнит Б, то тесты А всё ещё должны проходить. Но они будут идти через А в Б. Это некрасиво. После того как убедились, что А всё ещё работает. Тесты желательно перенести чтобы тестить Б напрямую. S>>Не [i]перенести[/i], а [i]скопировать[/i]. А для полноты ощущений наверное нужно в тестах А заменить Б на мок Б - чтобы это был именно юнит тест, а то уже какая-то интеграция получается. ·>Можно и скопировать. Потом удалить лишнее, если мешается. S>>>>А как тогда вы защититесь от будущего джуна, который там что-нибудь отвинтит? S>>·>Не понял, что "что-нибудь"? S>>Значит возьмёт и поменяет вызов вашего компонента Б на какую-нибудь ерунду. ·>А если в твоей функции джун поменяет вызов на какую-нибудь ерунду? S>>·>Ну не нравится, не ешь. Об чём спор? Для чего нужен таймер - я тебе написал, для low latency. Без таймера твой вариант у меня тоже элементарно реализуется, правда мне совесть не позволит убить ещё одного котёнка делая static глобальную переменную. Можно даже гибридное решение наворотить, обновляя кеш по таймеру из другого треда, но если вдруг момент timeSource.now() промазывает по кешу, то идти по медленному пути. S>>Это решение, очевидно, хуже обоих обсуждаемых вариантов. Оно одновременно оборудовано таймером (усложнение зависимостей, хрупкость) и всё ещё иногда идёт по медленному пути. ·>Это же я фантазирую. Наверно да, такое вряд ли понадобится. S>>Так-то и в ФП ваш вариант элементарно реализуется, потому что способ закинуть замыкание в таймер есть более-менее везде. ·>Как? S>>·>Это как? Если у нас в SLA написано, что max response time 10ms, а SlowlyCalculatePrevAndNextFridays работает 11ms, то выбора тупо нет, и похрен всем твоя точка зрения. S>>Ну, ок, я таких SLA не встречал. Это примерно как 100% availability - обеспечить невозможно. Если у вас такой SLA - ну, ок, согласен, это может влиять на дизайн. ·>Ну... было всего 99.99% афаир. S>>>>В-третьих, там этот "спайк задержки" работает на 1 запрос. Практически невозможно придумать SLA, на которое это повлияет. S>>·>Открой для себя мир low latency. S>>С удовольствием. Пришлите ссылку на публичную SLA, в которой есть такие требования, я почитаю. ·>Было дело в lmax. Насчёт публичных доков - не знаю. S>>·>У нас это было серьёзный production incident. Каждый запрос, обработка которого была >10ms - тикет и расследование. Больше таких двух в день - и клиент нам звонит для разборок. S>>Ну, тогда у нас есть тесты не только функциональные, но и с бенчмарками, и никто не выкатывает в прод код, [i]надеясь[/i], что он быстрый. И ошибки вроде промахов кэша обнаруживаются регулярным способом, а не мамойклянус. ·>Ясен пень. Но это ещё и означает, что такие фривольности с глобальными кешами просто недопустимы. Т.к. промахи могут быть недетерминированными и все эти бенчмарки их ловят негарантированно. Как в примере со _слишком_ старой платёжкой. Обычно платёжки не очень старые и даже прогон месячной прод-нагрузки может ничего не выявить, а случайный залётный дятел попадается редко, но метко. S>>·>Это повлияет только на новый код с платёжкой. Подменить "случайно" по всему приложению так не выйдет. А вот твоя глобальная переменная - happy debugging. S>>У нас тоже подменить случайно по всему приложению не выйдет - потому что "всё приложение" пользуется функцией nextFriday(x). А кэширование к этой функции приделано только там, где это потребовалось. ·>Требоваться может везде (или почти везде), в этом и проблема. Суть примера была в том, что "всё приложение" использует не общий "nextFriday(x)", а конкретный "nextFriday(Time.Now)" во многих местах (см. моё начальное "[i]Если у нас конечная цель иметь именно nextFriday()[/i]"). Поэтому этот общий код у меня и вынесен в одно место, которое безопасно кешировать, т.к. известен этот самый x. У тебя - неизвестно, в этом и беда. Напомню: ЧПФ. S>>·>У меня была цель продемонстрировать не основы объектно-ориентированного анализа, а факт, что можно максимально оптимизировать реализацию nextFriday(), т.к. у нас известно многое внутри самого метода. Навешивая это всё снаружи, приходится делать всё более пессимистично, т.к. инфы тупо меньше. S>>Нет такого факта, я вам в который раз повторяю. В вашем instantSource нет никакой гарантии, что следующий вызов now() не вернёт момент времени [i]раньше[/i], чем предыдущий. ·>Это не так. В худшем случае, я могу тупо найти все те одно-два места использования _конструктора_ CalendarLogic удостовериться что там может быть и какими свойствами обладает. Ещё раз. ЧПФ. S>>И по коду, который вы пишете, примерно понятно, как вы будете это делать в продакшн - в частности, вы даже не задумались о том, что CalendarLogic у вас будет декоратором или наследником старого CalendarLogic. Просто взяли и заменили код, на который завязан весь проект. Ну заменили вы "статик глобал" на "глобал синглтон" - что улучшилось-то? ·>Нет никакого глобал. Есть composition root. S>>>>который ФП-программист пишет инстинктивно, т.к. в ФП такая идиоматика. S>>·>Ага, смешно у Pauel инстинкт с memoize сработал. S>>Бывает, чо. В реале он бы привинтил к nextFriday memoize, убедился, что тот не помогает, и сел бы разбираться в причинах и искать способ сделать это корректно. Но у него понятная архитектура. ·>Совершенно верно! Любая, даже самая сложная проблема обязательно имеет простое, легкое для понимания, неправильное решение. S>>>>А надо - наоборот: зависимостей - избегать, стейта - избегать. S>>·>Ага, и ты тут такой: [tt]static (DateTime prevFriday, DateTime nextFriday)? _cache[/tt] :)) А потом хрясь и в догонку [tt]nextFriday(LocalDateTime.now())[/tt]! :))) S>>Всё правильно - весь стейт торчит на самом верху, и его мало. А глубокие вызовы все pure, и позволяют тестироваться по табличкам безо всяких моков. ·>Может я не понял, что ты называешь самым верхом, но это у вас - метод контроллера, коих в типичном приложении сотни. У же меня этим верхом является один на всех composition root. S>>>>Понадобится стейт - всегда можно добавить его по необходимости. S>>·>А вот попробуй избавиться от этого static и у тебя полезет колбасня. S>>Не полезет у меня никакой колбасни ;) S>>Просто будет локальная переменная в замыкании. ·>Как эта локальная переменная будет шариться между контроллерами? S>>>>Нет у вас никакого широкого простора для оптимизаций. Есть [i]иллюзии[/i] того, что timeSource.now() ведёт себя как-то иначе, чем аргумент (x). И, естественно, оптимизации, построенные на этих иллюзиях, будут приводить к фейлам. S>>·>ЧПФ же. У нас зафиксирован timeSource же. S>>Ну и что, что ЧПФ? Вы вообще ближе к началу этого поста признались, что вам как таковой instantSource() вовсе не нужен, а нужен ровно один instant, от которого пляшет вся бизнес-логика. S>>Так и какой смысл подменять параметр типа instant на параметр типа instantSource, который вы хотите позвать ровно один раз в рамках экземпляра бизнес-логики? С какой целью вы это делаете? ·>В случае trade date, instant - это физическая точка во времени, для железки. А для ордера нужны календарные показания, для трейдеров. CalendarLogic эту логику и обеспечивает. S>>·>Сокет ещё утром был подключён к конкретной бирже. Диспечерезацией уже занимается сетевая карта. S>>Ну и прекрасно - тогда у нас есть глобальная информация о том, с какой биржей мы работаем, внутри messageHandler. ·>Что значит глобальная? Приложение может подключаться не нескольким биржам одновременно, разными сокетами. S>>·>Почему неверный? Таймеры тоже срабатывают по timeSource. Проигрывание лога воспроизводит всё поведение системы, но уже без привязки к физическим часам. Ровно это же используется в тестах, когда мы выполняем шаги: "Делаем трейд. Ждём до конца месяца. Получаем стейтмент". S>>Где у вас это выражено в коде? Как у вас срабатывают таймеры, когда время идёт "назад"? ·>:facepalm: Время не ходит назад. Это физически невозможно. Проблема возникла конкретно у тебя, т.к. у тебя общий всемогутер [tt]nextFriday(x)[/tt], где в качестве [tt]x[/tt] может быть как и текущее время, так и старые платёжки.
Теги:
Введите теги разделенные пробелами. Обрамляйте в кавычки словосочетания с пробелами внутри, например:
"Visual Studio" .NET
Имя, пароль:
Загрузить
Нравится наш сайт?
Помогите его развитию!
Отключить смайлики
Получать ответы по e-mail
Проверить правописание
Параметры проверки …