Re[61]: Что такое Dependency Rejection
От: Sinclair Россия https://github.com/evilguest/
Дата: 31.01.24 10:45
Оценка:
Здравствуйте, ·, Вы писали:

·>У меня будет изначально nextFriday() — с которой всё просто, характер "главного" параметра ЧПФ известен. Поэтому такой проблемы не будет в принципе, функция в местах использования одна — вариант использования один, параметров нет.

Ну как же. У вас с самого начала нельзя внутри nextFriday вызывать now() — потому что тогда вы не сможете её даже протестировать.
Вам сходу придётся реализовывать концепт тайм-провайдера, и как-то привязывать его к вашей nextFriday().
Дальше проблемы начинают собираться в снежный ком — потому, что при малейшем изменении требований вам придётся изобретать очередной изящный способ подсунуть в nextFriday() правильный провайдер времени.
На первый взгляд кажется, что это как раз даёт вам нужную гибкость — типа сама по себе nextFriday() у вас одна и та же, и вы там каким-нибудь DI-конфигом подсовываете ей то обычный DateTime.now(), то Request.ArriveTime, то Request.SubmitTime, то ещё какой-нибудь Request.Document.SignOffTime.
Но на практике это превращается в we wish you a happy debugging, потому что конфиги теперь становятся частью решения, а для них нет ни средств рефакторинга, ни измерения тестового покрытия.
В итоге рано или поздно замена одного провайдера времени на другого, отлаженная на стейджинге, не доезжает до прода, и упс.
Либо вы выкидываете к хренам конфиги, и возвращаетесь к инициализации зависимостей в коде — прямо в контроллере.
То есть вместо того, чтобы полагаться на NextFridayProvider, которого вам дали снаружи, и на то, что в него засунули нужный вам TimeSource, вы просто пишете
var nfp = new NextFridayProvider(new TimeSources.RequestSendTimeSource(this.request));
nfp.NextFriday();

И в итоге вы получаете ровно ту же топологию решения, как и в ФП, вид сбоку. Преимуществ от вашего подхода уже никаких не осталось; остались только недостатки — усложнение тестирования.
В частности, у вас дизайн кэширования может нечаянно положиться на монотонность timeSource, которая в терминах ООП-контрактов невыразима, и вы это никак не обнаружите.
Потому что мок timeSource, с которым вы тестируете, совершенно случайно будет монотонным.

·>Конечно, если в моём случае вдруг возникнет необходимость посчитать nextFriday(platezhka.date) — тогда мне придётся ввести новую функцию (новую, т.е. менять ничего не приходится, от чего так страдает Pauel с его постоянными изменениями дизайна) и она будет отдельной и там уже будем посмотреть где что как анализировать и оптимизировать, нужно ли кешировать, если нужно то как.

Тут вы попадаете в ловушку. Как раз у Pauel никакого изменения дизайна не случится, потому что у него nextFriday с самого начала имела нужную ему сигнатуру.
А вы теперь будете вынуждены продублировать код nextFriday — ну, либо всё же перенести его в новую nextFriday, а старую переделать на вызов новой. Что влечёт за собой не только тестирование новой функции, но и перетестирование и старой.
Оба варианта выглядят так себе, если честно. И это — только одна крошечная функция. В более сложных случаях вы будете тонуть в моках, при этом никак не улучшая качество тестирования.
Да, кстати, забыл спросить — вы же там к своему кэшу собирались таймер приделать, если я не ошибаюсь? А это вы как собираетесь тестировать — будете сидеть каждый раз в ожидании пятницы?
Или там будет ещё один mock?

·>В твоём случае же решение хоть более универсальное, но за универсальность нужно платить. Ты исходишь из изначально неудачно задизайненного случая. У тебя есть некая универсальная nextFriday(someDay) хотя она везде по факту (вроде бы... надо тщательно проверять по всему коду!), используется только как nextFriday(now), ну значит давайте просто сунем в wrapAndCacheMonotonous, да закроем джиру.

Нет, это работает не так. Я же вроде вам написал простую пошаговую инструкцию. Повторю на всякий случай: мы НЕ пишем универсальную политику кэширования.
Начинается всё не с того, что у нас там "везде по факту". А с того, что у нас есть конкретный запрос, который выходит за рамки нормативов. Мы профилируем его, и если видим, что боттлнек — именно в вызове nextFriday, мы в этом конкретном месте пишем
nextFriday = wrapAndCacheMonotonous(nextFriday);

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

·>Ура. Но знание о том, что этот кеш требует определённый характер значений параметра — нигде в коде не зафиксировано; и когда в следующей джире через год потребуется nextFriday(platezhka.date) — просто так и напишут, проблему обнаружат только потом, когда заметят что кеш теряется в неожиданные моменты времени, придётся искать в мониторинге и метриках корреляцию между внезапными провалами в производительности и запросами, когда клиент приходит со слишком старой платёжкой, что случается только после дождичка в четверг в полнолуние. Happy debugging.

Вот видите, сколько неверных выводов вы сделали из неверных предпосылок.

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

Это всё верно, вот только смысла при этом увлекаться stateful-дизайном нет никакого. А stateless и в тестировании проще, и в последующем рефакторинге. В частности, у вас кэширование приходится тащить внутрь nextFriday, а в ФП оно прекрасно сидит снаружи.

·>Я не про монотонность, а про то, что в nextFriday(x) — есть степень свободы что передавать в качестве x — и приходится делать предположения и анализ что может быть этим x во всём коде. В моём случае у меня явное ЧПФ nextFriday = () => nextFriday(timeSource.now()), зафиксированно знание о том, что же может быть этим самым x есть более широкий простор для оптимизаций.

Ну нет конечно. Вы же не зафиксировали, что такое у вас timeSource. Единственный способ гарантировать, что у вас там именно now() со всеми его свойствами (кстати, монотонность к их числу не относится ) — это выкинуть timeSource и написать прямо DateTime.Now. Но так нельзя — будут проблемы с тестированием; поэтому придумываем timeSource. Но вы никак степень свободы не изменили — вы просто отложили проблему на один уровень дальше по стеку. Достаточно написать новую реализацию ITimeSource, которая ведёт себя не так, как ваш мок, как ваш код запросто сломается. Причём самое клёвое — так это то, что вы эту сломанность никакими тестами не обнаружите Только по жалобам клиентов в продакшне.

·>Вот когда появится бизнес-требование обработки старых платёжек, тогда можно будет отрефакторить, добавив новую функцию nextFriday(someDay) или даже вообще сразу ЧПФ nextFriday(platezhka).

Вы предлагаете всё более и более плохие решения. В частности, nextFriday(platezhka), где алгоритмы вычисления границ недели (неизменные примерно с 14 века) смешиваются с подробностями реализации конкретного бизнес-документа. Это — прямо канонический пример того, как делать не надо. Вы минимизируете повторную используемость кода, увеличивая coupling и снижая cohesion.

S>>Положа руку на сердце: сколько таких мест вы ожидаете в крупном проекте? Три, пять?

·>Не очень понял что ты именно спрашиваешь, ясен пень, что nextFriday я вообще считаю такого не будет нигде. Аналогом могу придумать например бизнес-понятие tradeDate() — текущий торговый день биржи. Который обычно переключается раз в сутки, зависит от системный часов, таймзоны биржи, официального времени работы с учётом праздников, т.е. вычисление не очень тривиально, но значение используется очень часто и из разных мест, в т.ч. и latency sensitive.
Прекрасный пример.
·>Иметь синглтон static-кеш как у тебя — просто недопустимо, т.к. торговая система может работать с толпой разных бирж.
Ну так нам и кэш будет не нужен.
Просто во всех местах, где нам нужен tradeDate, мы включаем его в сигнатуру функций, и скармливаем снаружи. И код, который процессит ордер, так и написан processExchangeOrder(order, exchange.tradeDate).
Что, в частности, позволяет нам лёгким движением руки выполнить replay пятничной сессии в понедельник, если в коде биржи обнаружилась ошибка и мы вынуждены аннулировать результаты торгов.
Вообще, практически 100% бизнес-кода, в котором есть прямые вызовы DateTime.Now() — это просто ошибка, которая ждёт своего момента. Но это так, уже беллетристика.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.