Здравствуйте, Sinclair, Вы писали:
S>·>У меня будет изначально nextFriday() — с которой всё просто, характер "главного" параметра ЧПФ известен. Поэтому такой проблемы не будет в принципе, функция в местах использования одна — вариант использования один, параметров нет.
S>Ну как же. У вас с самого начала нельзя внутри nextFriday вызывать now() — потому что тогда вы не сможете её даже протестировать.
S>Вам сходу придётся реализовывать концепт тайм-провайдера, и как-то привязывать его к вашей nextFriday().
Не знаю о чём ты тут говоришь. Выше по топику у меня был код тестов.
S>Дальше проблемы начинают собираться в снежный ком — потому, что при малейшем изменении требований вам придётся изобретать очередной изящный способ подсунуть в nextFriday() правильный провайдер времени.
Ну перечитай топик что-ли.
S>На первый взгляд кажется, что это как раз даёт вам нужную гибкость — типа сама по себе nextFriday() у вас одна и та же, и вы там каким-нибудь DI-конфигом подсовываете ей то обычный DateTime.now(), то Request.ArriveTime, то Request.SubmitTime, то ещё какой-нибудь Request.Document.SignOffTime.
Разговор об этом не шел. Был именно "серверное время". Вот
_если_ у меня появится требование с Request.Document.SignOffTime, вот тогда я и введу
nextFriday(someDay), я же это уже написал ~час назад. Это вопрос автоматического рефакторинга "extract method" и нескольких минут от силы. Зато вся эта ботва с кешами и оптимизациями — будет видна как на ладони.
S>Но на практике это превращается в we wish you a happy debugging, потому что конфиги теперь становятся частью решения, а для них нет ни средств рефакторинга, ни измерения тестового покрытия.
Какие конфиги?
S>В итоге рано или поздно замена одного провайдера времени на другого, отлаженная на стейджинге, не доезжает до прода, и упс.
S>Либо вы выкидываете к хренам конфиги, и возвращаетесь к инициализации зависимостей в коде — прямо в контроллере.
Зависимости собираются в Composition Root. Тоже уже обсуждалось.
S>То есть вместо того, чтобы полагаться на NextFridayProvider, которого вам дали снаружи, и на то, что в него засунули нужный вам TimeSource, вы просто пишете
S>S>var nfp = new NextFridayProvider(new TimeSources.RequestSendTimeSource(this.request));
S>nfp.NextFriday();
S>
S>И в итоге вы получаете ровно ту же топологию решения, как и в ФП, вид сбоку. Преимуществ от вашего подхода уже никаких не осталось; остались только недостатки — усложнение тестирования.
Ты почему-то решил, что добавить параметр в NextFriday является какой-то проблемой. Совсем наоборот — добавить просто, т.к. это новое требование и новый код. Убрать или зафиксировать — сложнее, т.к. надо исследовать все варианты и места использования во всём существующем коде.
Да даже с таким кодом — я не понял где тут усложнение тестирования. В худшем случае пара лишних new, которые наверняка вырежутся оптимизатором.
S>В частности, у вас дизайн кэширования может нечаянно положиться на монотонность timeSource, которая в терминах ООП-контрактов невыразима, и вы это никак не обнаружите.
Она выразима в сигнатуре метода
nextFriday() — в call sites не надо заботиться от timeSource. Об этом уже позаботились за меня, заинжектили всё готовенькое. Inversion Of Control.
Если ты имеешь в виду как выражать свойства самого timeSource — в терминах ООП будет другой тип MonotonicTimeSource. И конструктор|фабрика NextFridayProvider может для разых подтипов TimeSource выбирать разные подходы к кешированию.
S>Потому что мок timeSource, с которым вы тестируете, совершенно случайно будет монотонным.
Что значит случайно? Это тестами покрывается.
S>·>Конечно, если в моём случае вдруг возникнет необходимость посчитать nextFriday(platezhka.date) — тогда мне придётся ввести новую функцию (новую, т.е. менять ничего не приходится, от чего так страдает Pauel с его постоянными изменениями дизайна) и она будет отдельной и там уже будем посмотреть где что как анализировать и оптимизировать, нужно ли кешировать, если нужно то как.
S>Тут вы попадаете в ловушку. Как раз у Pauel никакого изменения дизайна не случится, потому что у него nextFriday с самого начала имела нужную ему сигнатуру.
S>А вы теперь будете вынуждены продублировать код nextFriday — ну, либо всё же перенести его в новую nextFriday, а старую переделать на вызов новой.
Аж два рефакторинга "extract method", "move method". Всё. 2 минуты времени.
S>Что влечёт за собой не только тестирование новой функции, но и перетестирование и старой.
Тесты как проходили, так и будут проходить. Впрочем, наверное после этого стоит тесты перенести тоже, для красоты.
S>Оба варианта выглядят так себе, если честно. И это — только одна крошечная функция. В более сложных случаях вы будете тонуть в моках, при этом никак не улучшая качество тестирования.
S>Да, кстати, забыл спросить — вы же там к своему кэшу собирались таймер приделать, если я не ошибаюсь? А это вы как собираетесь тестировать — будете сидеть каждый раз в ожидании пятницы?
S>Или там будет ещё один mock?
Да. Таймеры мочить надо в любом случае. Иначе как _вы_ это будете тестировать? Деплоить всё на сервера и ждать пятницы? Или системные часы в операционке перематывать?
Я, конечно, имею в виду, что мы оба сравниваем одинаковые подходы с обновлением кеша по таймеру. Твой подход с обновлением кеша в момент первого вызова я тоже могу реализовать, без использования таймеров естественно. Но это не универсальный всемогутер, операционные недостатки такого подхода я уже упомянул.
S>·>В твоём случае же решение хоть более универсальное, но за универсальность нужно платить. Ты исходишь из изначально неудачно задизайненного случая. У тебя есть некая универсальная nextFriday(someDay) хотя она везде по факту (вроде бы... надо тщательно проверять по всему коду!), используется только как nextFriday(now), ну значит давайте просто сунем в wrapAndCacheMonotonous, да закроем джиру.
S>Нет, это работает не так. Я же вроде вам написал простую пошаговую инструкцию. Повторю на всякий случай: мы НЕ пишем универсальную политику кэширования.
S>Начинается всё не с того, что у нас там "везде по факту". А с того, что у нас есть конкретный запрос, который выходит за рамки нормативов. Мы профилируем его, и если видим, что боттлнек — именно в вызове nextFriday, мы в этом конкретном месте пишем
S>S>nextFriday = wrapAndCacheMonotonous(nextFriday);
S>
S>Всё. У нас нет множества хрупких связей, где потянув за одну мы разрушаем всё здание. У нас всё плоское и простое. Нет регрессии остального кода, его не нужно перетестировать. Корректность wrapAndCacheMonotonous проверяется набором юнит-тестов, которые никак не зависят от текущего таймстемпа, и вообще отрабатывают за миллисекунды.
Я же показал где хрупкость связи. До строчки
nextFriday = wrapAndCacheMonotonous(nextFriday); писать
nextFriday(platezhka.date) можно, а после только
nextFriday(Time.Now) и больше никак. И факт этот никак в коде не выражен.
S>Вот видите, сколько неверных выводов вы сделали из неверных предпосылок.
Может ты не понял что я имею в виду.
S>·>Нет, я просто не стараюсь писать универсальные всемогутеры, а ровно то, что требуется для решения конкретной задачи, ну может ещё немного в соответствии с достаточно очевидными прогнозами на будущее. Ясновидец из меня никудышный, поэтому приходится выдавать решение из того что известно сейчас, а не для того, что случиться в будущем, я лучше потом порефакторю.
S>Это всё верно, вот только смысла при этом увлекаться stateful-дизайном нет никакого. А stateless и в тестировании проще, и в последующем рефакторинге. В частности, у вас кэширование приходится тащить внутрь nextFriday, а в ФП оно прекрасно сидит снаружи.
Открой для себя паттерн Декоратор.
Насчёт stateless я не спорю, тут полностью согласен. Но не всегда есть такая роскошь.
S>·>Я не про монотонность, а про то, что в nextFriday(x) — есть степень свободы что передавать в качестве x — и приходится делать предположения и анализ что может быть этим x во всём коде. В моём случае у меня явное ЧПФ nextFriday = () => nextFriday(timeSource.now()), зафиксированно знание о том, что же может быть этим самым x есть более широкий простор для оптимизаций.
S>Ну нет конечно. Вы же не зафиксировали, что такое у вас timeSource. Единственный способ гарантировать, что у вас там именно now() со всеми его свойствами (кстати, монотонность к их числу не относится ) — это выкинуть timeSource и написать прямо DateTime.Now. Но так нельзя — будут проблемы с тестированием; поэтому придумываем timeSource. Но вы никак степень свободы не изменили — вы просто отложили проблему на один уровень дальше по стеку. Достаточно написать новую реализацию ITimeSource, которая ведёт себя не так, как ваш мок, как ваш код запросто сломается. Причём самое клёвое — так это то, что вы эту сломанность никакими тестами не обнаружите Только по жалобам клиентов в продакшне.
S>·>Вот когда появится бизнес-требование обработки старых платёжек, тогда можно будет отрефакторить, добавив новую функцию nextFriday(someDay) или даже вообще сразу ЧПФ nextFriday(platezhka).
S>Вы предлагаете всё более и более плохие решения. В частности, nextFriday(platezhka), где алгоритмы вычисления границ недели (неизменные примерно с 14 века) смешиваются с подробностями реализации конкретного бизнес-документа. Это — прямо канонический пример того, как делать не надо. Вы минимизируете повторную используемость кода, увеличивая coupling и снижая cohesion.
Это называется code reuse. Если у меня в 100 местах нужно посчитать
nextFriday для платёжки, то я заведу именно такой метод
nextFriday(platezhka), а не буду в 100 местах копипастить
nextFriday(platezhka.date). Потому что в моей системе в первую очередь важны бизнес-документы, а не алгоритмы 14го века.
S>>>Положа руку на сердце: сколько таких мест вы ожидаете в крупном проекте? Три, пять?
S>·>Не очень понял что ты именно спрашиваешь, ясен пень, что nextFriday я вообще считаю такого не будет нигде. Аналогом могу придумать например бизнес-понятие tradeDate() — текущий торговый день биржи. Который обычно переключается раз в сутки, зависит от системный часов, таймзоны биржи, официального времени работы с учётом праздников, т.е. вычисление не очень тривиально, но значение используется очень часто и из разных мест, в т.ч. и latency sensitive.
S>Прекрасный пример.
S>·>Иметь синглтон static-кеш как у тебя — просто недопустимо, т.к. торговая система может работать с толпой разных бирж.
S>Ну так нам и кэш будет не нужен.
S>Просто во всех местах, где нам нужен tradeDate, мы включаем его в сигнатуру функций, и скармливаем снаружи. И код, который процессит ордер, так и написан processExchangeOrder(order, exchange.tradeDate).
Нет. Метод, который процессит ордер будет выглядеть примерно так
processMessage(ByteBuffer order) и зваться напрямую из сетевого стека. Откуда в сетевом стеке возьмётся вся эта инфа о биржах — я не знаю. Сетевой стек тупой — пришёл пакет из сокета X, дёргаем метод у привязанного к этому сокету messageHandler.
S>Что, в частности, позволяет нам лёгким движением руки выполнить replay пятничной сессии в понедельник, если в коде биржи обнаружилась ошибка и мы вынуждены аннулировать результаты торгов.
Для этого просто подменяется timeSource. Во время replay он будет не system clock, а timestamp из event log.
S>Вообще, практически 100% бизнес-кода, в котором есть прямые вызовы DateTime.Now() — это просто ошибка, которая ждёт своего момента. Но это так, уже беллетристика.
Естественно, т.к. это типичный глобальный синглтон. Поэтому и изобрели
InstantSource. И если я правильно понимаю Pauel (код он как всегда скрывает), он предлагает писать DateTime.Now() в каждом методе контроллера, т.к. именно там у него интеграция всего со всем.