Всем привет.
Пришло время собирать камни — привести архитектуру приложения в божеский вид.
Сейчас она описывается меткой фразой "некогда думать, надо копать".
Проявляется это во всём, начиная с циклических референсов в сборках, благодаря рефлекшену, и заканчивая Constructor Injection c 28 зависимостями.
Если за первое нужно просто отрывать руки, то последнее — следствие непродуманной архитектуры. Но это уже есть и возникает вопрос — как бы изловчиться и, не переписывая десятки мегабайт кода, привести существующее безобразие в более-менее адекватный вид.
В связи с чем вопрос — что делать?
Есть некий воркфлоу приложения, который, продираясь через множество уровней абстракции, уходит всё глубже в дебри internal-сборок и попутно обрастает параметрами-зависимостями, в нижней точки своего падения приходя к тем самым 28 аргументам в конструкторе очередного класса, без которых он жить не может.
Тут и Id сессии, и Id текущей задачи, и идентификатор пользователя, и логгер и ещё чёрт знает что, на лицо ужасное, стрёмное внутри.
Хочется выбросить это в какой-нибудь легко доступный (как в плане кода, так и в плане перфоманса) контекст, с которым будет легко общаться.
Для данных таковым контекстом выступает БД, благодаря тому, что она всего одна, то доступ осуществляется через синглтон (когда-нибудь это обязательно выстрелит -_-).
А вот для всего остального он отсутствует, и тянутся эти зависимости per aspera ad astra.
Возьмём, для примера, логирование.
Вот запускается приложение и моментально начинает писать лог. Имя лога спускается в аргументах командой строки.
Окей, от этого трудно отказаться (хотя и можно), но пока терпимо.
Происходит аутентификация пользователя. Теперь лог пишется уже в каталог этого горемычного юзера.
Юзер выполняет некоторую команду, лог пишется всё в ту же папку, но уже в файл с именем выполняемой команды.
Команда начинает параллелиться и работает с разными компьютерами в сети. Каждая задача начинает писать лог в файл с именем упомянутой выше команды + имя компьютера.
В процессе, мы начинаем использовать более низкоуровневые интерфейсы, которые тоже пишут лог и ни сном ни духом не знают ничего ни о юзерах, ни о командах, ни о хостах. Но при этом тоже хотят писать в лог текущее состояние и логировать ошибки, уходя на ретраи если сетка моргнула, но ещё есть шанс получить отклик от удалённой машинки. А писать то логи нужно в те же самые файлы с именем команды, хоста, в папке юзера. Появляются ThreadLocal-коллекции логеров, через которые проходят все вызовы.
Но низкоуровневые компоненты тоже не лыком шиты и помимо распаралеливания на высшем уровне, дёргают асинхронные методы на нижнем, которые и вовсе выполняются в случайном потоке из пула и в свою очередь тоже дёргают ещё более низкоуровневые компоненты, которые всё ещё хотят писать логи в те же файлы, что и далёкий сверхудёрнувший их компонент. А значит приходится тащить сквозь все слои логер и регистрировать его в статичной ThreadLocal-коллекции из предыдущей серии.
Вот примерно так и рождаются вереницы аргументов в конструкторах и методов, которых от версии к версии становятся всё больше. Иногда видишь подобное безобразие, свернёшь 10-ок аргументов в мини-контекст, поменяешь вызовы. Месяц-другой код живёт, а потом глядь — добавилось ещё два аргумента, а в твоём контексте добавились лишние свойства, которые нужны в 4 из 7 методов, но инциализируются, естественно, всегда. Кода становится ещё больше, страдает читабельность, производительность, и простые компоненты обрастают сложной логикой, нужной для инициализации одного из компонентов нижнего уровня, который просто не знает — куда ему писать лог.
Некоторое время душу терзают мысли о Dependeny Injection. Но в компании уже был неудачный опыт внедрения этой штуки (о котором(ой) я ни сном, ни духом), закончившийся выпиливанием оной с большим количеством матюгов. В общем, не прижилось, да оно и понятно — время жизни и порядок инициализации объектов были непоняты от слова совсем, и зачастую людям возвращались объекты, которые они не просили, и были зарегистрированы вообще не пойми кем, когда и с какой целью. В общем, всё плохо, и опять же из-за непродуманной архитектуры, отрисованой на коленке, чтобы хоть как-то работало в стиле write-only.
В общем, я буду благодарен любым камрадам и камрадкам, которые подскажут — как быть в подобной ситуации. Желательно, с реальными примерами, близкими к описанной ситуации с логированием. Что делать, куда смотреть, что читать, что изучать? Какие паттерны подойдут, а какие не стоит трогать ни в коем случае. Если DP, то с ссылками на книги и статьи по правильной разработке и конфигурации этого чуда в реалиях .NET.
Здравствуйте, LWhisper, Вы писали:
LW>Всем привет.
Для начала вот что надо сделать:
1. Выделить чисто утилитарные классы, которые сами по себе с зависимостями не работают и всё нужное явно декларируют в API, через свойства / параметры.
Этот слой вы скорее всего захотите тестировать и заморачиваться с "вспомни, какие зависимости забыл положить" вам вряд ли понравится.
2. Выделить штуки, которые вы хотите предоставлять в виде зависимостей, по возможности оформить их в виде интерфейсов (разумеется, без фанатизма).
3. Внедрить immutable класс-контекст, который будет отвечать за передачу / подмену зависимостей. Что-то очень легковесное, типа ServiceContainer.
ВАЖНО: Никогда, ни при каких обстоятельствах не повторяйте вот эту ошибку в дизайне (метод по умолчанию возвращает null, если сервис не найден). Довелось поработать на проекте с таким решением, если коротко — выстреливало постоянно.
Методы, которые в случае "нунишмогла" возвращают null, надо обзывать с префиксом Try. Иначе ловить вам null reference в самых внезапных местах до конца жизни проекта.
4. Задокументировать типовые сервисы в виде extension-методов. Т.е. большинство вызовов должны выглядеть как context.GetLogger(), а не как context.GetService<ILogger>(). Без этого различить "используем стандартное API" от "протаскиваем экзотичную фигню" невозможно и код превращается в мешанинуиз зависимостей.
5. Для свежего фреймворка (читай, для 4.6): рассмотрите возможность передавать контекст неявно, через AsyncLocal<T>. Упрощает передачу контекста до
var logger = Context.CurrentContext.Logger(); // Context - static class
а с using static — до
var logger = CurrentContext.Logger();
без необходимости протаскивать контекст через параметры.
Вот после того как всё это сделано, большинство проблем с зависимостями рассосётся и оставшееся можно будет обсуждать предметно. Конкретно:
LW>Происходит аутентификация пользователя. Теперь лог пишется уже в каталог этого горемычного юзера.
В момент аутентификации происходит подмена контекста, в новом контексте переопределяется логгер.
LW>Юзер выполняет некоторую команду, лог пишется всё в ту же папку, но уже в файл с именем выполняемой команды.
Снова подмена контекста.
Оффтоп: Вас саппорт случаем не убивает за необходимость собирать лог из десятка файлов?
LW>Команда начинает параллелиться и работает с разными компьютерами в сети. Каждая задача начинает писать лог в файл с именем упомянутой выше команды + имя компьютера.
Нувыпоняли
Хотя по-хорошему для конкретно этой задачи нужно не использовать один логгер, а добавлять отдельный для каждого класса задач.
LW>Некоторое время душу терзают мысли о Dependeny Injection. Но в компании уже был неудачный опыт внедрения этой штуки (о котором(ой) я ни сном, ни духом), закончившийся выпиливанием оной с большим количеством матюгов.
DI — это уже нашлёпка поверх протаскиваемого контекста. Если с ним бардак, то DI только замаскирует проблему, а не поможет её вылечить.
Re[2]: Передача контекста через множество уровней абстракции приложения
S>3. Внедрить immutable класс-контекст, который будет отвечать за передачу / подмену зависимостей. Что-то очень легковесное, типа ServiceContainer.
Ага, спасибо, взгляну.
S>5. Для свежего фреймворка (читай, для 4.6): рассмотрите возможность передавать контекст неявно, через AsyncLocal<T>.
Вот тут и начинается самое интересное.
.NET 4.6 и VS2015 — это серьёзный шаг, до которого ещё пёхать и пёхать.
Проблему ThreadLocal я уже описывал. Но в связи с тем, что в .NET нет иерархии потоков, как я понимаю, AsyncLocal точно также придётся поддерживать вручную во всех типах, которые запускают потоки. Но было бы здорово почитать про это поподробнее. Цена вопроса, области применения и т.д.
S>В момент аутентификации происходит подмена контекста, в новом контексте переопределяется логгер.
При наличии неявной передачи контекста выглядит довольно просто и удобно. При явной, к сожалению, придётся тащить его в самые отдалённые места, в каждый метод, в каждый класс, который когда либо хотел, хочет или может захотеть что-нибудь залогировать.
S>Оффтоп: Вас саппорт случаем не убивает за необходимость собирать лог из десятка файлов?
Нет, он вполне себе счастлив, так как гигабайты логов из параллельных потоков в одном файле читать было бы весьма неприятно.
S>Хотя по-хорошему для конкретно этой задачи нужно не использовать один логгер, а добавлять отдельный для каждого класса задач.
Можно подробнее?
S>DI — это уже нашлёпка поверх протаскиваемого контекста. Если с ним бардак, то DI только замаскирует проблему, а не поможет её вылечить.
Понял, спасибо.
Re[3]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, LWhisper, Вы писали:
LW>Вот тут и начинается самое интересное. LW>.NET 4.6 и VS2015 — это серьёзный шаг, до которого ещё пёхать и пёхать.
Зря. Таргетинг под 4.6 можно и в 2012й врубить. На всём что древнее сидеть — редкостный мазохизм.
LW>Проблему ThreadLocal я уже описывал.
ThreadLocal вообще obsolete надо объявить. Лучшего способа отхватить граблей в сочетании с тасками я ещё не видел.
Ну ок, не нравится AsyncLocal — можно с LogicalCallContext поэксперементировать. В 4.0 он с тасками емнип не дружил, начиная с 4.5 всё ок. Пример.
LW>Но в связи с тем, что в .NET нет иерархии потоков, как я понимаю, AsyncLocal точно также придётся поддерживать вручную во всех типах, которые запускают потоки.
Вообще-то call context с первого фреймворка был. Ссылки в предыдущем абзаце. Матчасть никто не знает, какабычна
LW>Цена вопроса, области применения и т.д.
В смысле цена вопроса? Вот этого?
Оно автоматом работает. Открываем msdn/гуглим примеры, пишем пруфтесты и используем с чистой совестью. Что-то поломается — узнаете по отпавшим пруфтестам.
Пруфтесты для таких вещей обязательны.
LW>При наличии неявной передачи контекста выглядит довольно просто и удобно. При явной, к сожалению, придётся тащить его в самые отдалённые места, в каждый метод, в каждый класс, который когда либо хотел, хочет или может захотеть что-нибудь залогировать.
Ну да. А какие ещё варианты?
Принцип тот же самый во всех прочих подходах будет, отличается только обёртка.
S>>Хотя по-хорошему для конкретно этой задачи нужно не использовать один логгер, а добавлять отдельный для каждого класса задач. LW>Можно подробнее?
Что-то типа PresentationTraceSources. Т.е. отдельный логгер для каждой из категорий задач. Иначе неправильно подменённый контекст — и весь вывод радостно усвистывает не в тот файл.
Re: Передача контекста через множество уровней абстракции приложения
LW>Вот примерно так и рождаются вереницы аргументов в конструкторах и методов, которых от версии к версии становятся всё больше. Иногда видишь подобное безобразие, свернёшь 10-ок аргументов в мини-контекст, поменяешь вызовы. Месяц-другой код живёт, а потом глядь — добавилось ещё два аргумента, а в твоём контексте добавились лишние свойства, которые нужны в 4 из 7 методов, но инциализируются, естественно, всегда. Кода становится ещё больше, страдает читабельность, производительность, и простые компоненты обрастают сложной логикой, нужной для инициализации одного из компонентов нижнего уровня, который просто не знает — куда ему писать лог.
Для этого идеально подходит комбинация DI + паттерна Wrapper.
LW>Некоторое время душу терзают мысли о Dependeny Injection. Но в компании уже был неудачный опыт внедрения этой штуки (о котором(ой) я ни сном, ни духом), закончившийся выпиливанием оной с большим количеством матюгов. В общем, не прижилось, да оно и понятно — время жизни и порядок инициализации объектов были непоняты от слова совсем, и зачастую людям возвращались объекты, которые они не просили, и были зарегистрированы вообще не пойми кем, когда и с какой целью. В общем, всё плохо, и опять же из-за непродуманной архитектуры, отрисованой на коленке, чтобы хоть как-то работало в стиле write-only.
Речь идет о конкретных DI-контейнерах?
Re[2]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, 0x7be, Вы писали:
0>Для этого идеально подходит комбинация DI + паттерна Wrapper.
DI сам по себе не решает проблему передачи зависимостей по цепочке вызовов. Нужно протаскивать контейнер, что превращает DI в извращённую разновидность service locator.
Wrapper тоже не поможет, т.к. в подобных задачах список зависимостей заранее неизвестен.
Re[3]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>DI сам по себе не решает проблему передачи зависимостей по цепочке вызовов. Нужно протаскивать контейнер, что превращает DI в извращённую разновидность service locator. S>Wrapper тоже не поможет, т.к. в подобных задачах список зависимостей заранее неизвестен.
Зачем контейнер???
Объявляем интерфейс:
interface ILogger
{
void log(string message);
}
Инжектируем его в компонент-пользователя:
class MyComponent
{
public MyComponent(..., Ilogger logger, ...)
{
...
}
}
Имеем "листовую" реализацию ILogger, которая осуществляет физическую запись куда надо.
По необходимости добавляем обёртки, которые добавляют в записываемое сообщение нужные атрибуты: имя компонента, идентификатор потока, дату/время и т.п.
В итоге компонент, в который инжектируется логгер понятия не имеет обо всех этих тонкостях, ему дали трубу — он туда дует, а куда она дует — его не волнует.
Наличие DI-контейнера для этой схемы строго опционально. В конце концов можно сконструировать нужную структуру объектов "руками".
Re[4]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, 0x7be, Вы писали:
0>Имеем "листовую" реализацию ILogger, которая осуществляет физическую запись куда надо.
А, в этом смысле?
Тогда да, получается примерно то же самое, что написал выше, только вместо контекста выступает di-контейнер.
У топикстартера как раз одна из проблем — как этот контейнер по всей цепочке вызовов протащить. Есть какие-нибудь идеи?
Re: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>Зря. Таргетинг под 4.6 можно и в 2012й врубить. На всём что древнее сидеть — редкостный мазохизм.
Пасиб, нашёл.
S>ThreadLocal вообще obsolete надо объявить. Лучшего способа отхватить граблей в сочетании с тасками я ещё не видел.
Ага.
S>Ну ок, не нравится AsyncLocal — можно с LogicalCallContext поэксперементировать. В 4.0 он с тасками емнип не дружил, начиная с 4.5 всё ок. S>Пример. S>Вообще-то call context с первого фреймворка был. Ссылки в предыдущем абзаце. Матчасть никто не знает, какабычна
Спасибо, ознакомлюсь. :D))
S>Что-то типа PresentationTraceSources. Т.е. отдельный логгер для каждой из категорий задач. Иначе неправильно подменённый контекст — и весь вывод радостно усвистывает не в тот файл.
И ещё раз спасибо!
Re[2]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Нахлобуч, Вы писали:
Н>А что если (отстаньте, мыши, я -- стратег): Н>И потом всякий желающий через Context.GetLogger() достает ILogger, уже нафаршированный, гм, "декораторами" и, стало быть, пишущий туда, куда надо.
Вот там, выше, Sinix предложил варианты с AsyncLocal и LogicalCallContext. Если удастся при помощи одного из них прозрачно протащить контекст до любого компонента, то после этого подойдёт любой из вариантов.
Re[5]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>Тогда да, получается примерно то же самое, что написал выше, только вместо контекста выступает di-контейнер.
А сам компонент о контейнере вообще ничего знать не должен. Ему нужны сервисы — ему их дают через параметры конструктора. Как — его не волнует.
S>У топикстартера как раз одна из проблем — как этот контейнер по всей цепочке вызовов протащить. Есть какие-нибудь идеи?
У топикстартера проблема в том, чтобы протащить все те параметры, которые я предлагаю упаковать в разные врапперы.
Re[3]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, LWhisper, Вы писали:
0>>Речь идет о конкретных DI-контейнерах? LW>Использовалось расширение Mocknity для MS Unity.
А зачем? Что мешает руками создавать и инжектировать нужные объекты друг в друга без особой контейнерной магии с autowiring`ом и гаданиями о том, почему контейнер выдал не тот экземпляр?
Re[6]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, 0x7be, Вы писали:
S>>Тогда да, получается примерно то же самое, что написал выше, только вместо контекста выступает di-контейнер. 0>А сам компонент о контейнере вообще ничего знать не должен. Ему нужны сервисы — ему их дают через параметры конструктора. Как — его не волнует.
Ну так в том-то и проблема, что "не знать про контейнер" не получается, у топикстартера это только самый нижний из слоёв. А выше — компонент вызывает другие компоненты, в которые надо тоже протащить зависимости. И далее по цепочке. Причём зависимости надо разруливать динамически, заранее они неизвестны, т.к. реализация обычно за простенький интерфейс прячется.
S>>У топикстартера как раз одна из проблем — как этот контейнер по всей цепочке вызовов протащить. Есть какие-нибудь идеи? 0>У топикстартера проблема в том, чтобы протащить все те параметры, которые я предлагаю упаковать в разные врапперы.
Нееет. Ты похоже просто с такими системами не сталкивался. В них типовая проблема — цепочка вызовов в 5-7 слоёв, в каждом из которых могут потребоваться свои зависимости, о которых вызывающий код ничего знать не должен. И общее число зависимостей — не единицы, а ближе к паре десятков. Ибо они добавляются/убираются буквально на ходу, под текущий набор требований.
Что-то типа сильно нелинейного пайплайна получается.
Re[7]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>Ну так в том-то и проблема, что "не знать про контейнер" не получается, у топикстартера это только самый нижний из слоёв. А выше — компонент вызывает другие компоненты, в которые надо тоже протащить зависимости. И далее по цепочке. Причём зависимости надо разруливать динамически, заранее они неизвестны, т.к. реализация обычно за простенький интерфейс прячется.
А почему в эти другие компоненты зависимости должен протаскивать вызывающий код, а не тот, который его конструирует?
S>Нееет. Ты похоже просто с такими системами не сталкивался. В них типовая проблема — цепочка вызовов в 5-7 слоёв, в каждом из которых могут потребоваться свои зависимости, о которых вызывающий код ничего знать не должен. И общее число зависимостей — не единицы, а ближе к паре десятков. Ибо они добавляются/убираются буквально на ходу, под текущий набор требований.
Вызывающий код и не должен знать. Должен знать код конструирующий.
В любом случае, это пока теория, я не знаю, что там точно у автора.
Re[8]: Передача контекста через множество уровней абстракции приложения
0>А почему в эти другие компоненты зависимости должен протаскивать вызывающий код, а не тот, который его конструирует?
Потому что тогда требование "протаскиваем зависимости" заменяется на "протаскиваем код, который конструирует объекты", т.е. по сути ничего не меняется. Вот типичная схема:
static SomeResult DumpA(..., Context context)
{
var data = context.Get<IAReportService>().PrepareData(...);
var dumper = context.Get<IDumpService>();
return dumper.Dump(data);
}
и
внутри IAReportService нам нужен, допустим, логгинг, доступ к субд, к интернету (подтягиваем обновления данных, если ещё не) и к сервису с API от интегрированной внешней системы.
внутри IDumpService() — IO (используется временный файл), логгинг и сервис-хелпер для редиректа потока данных клиенту даже после завершения метода — чтоб не держать лишние ресурсы и не вызывать метод заново при обрыве связи.
Нюансы в следующем:
1. Зависимости заранее неизвестны. В крайнем случае бывает вот так: завтра клиент попросит — выкинем часть расчётов в пул задач, интернет поменяем на чтение переданного файла, лог будем показывать бегущей строкой на клиенте а IDumper вообще заменим на реализацию клиента. При необходимости конкретная реализация будет подсунута на лету, без пересборки всего хозяйства. При этом клиенты из другой организации на том же сервисе получат контекст с старыми зависимостями.
2. Оба сервиса могут вызывать другие бизнес-сервисы и так пока не надоест.
Оба требования в итоге не оставляют выбора — в каждый из уровней явно/неявно должен протаскиваться текущий контекст. Будет это делаться явно, передачей через параметры, или контейнер будет запихивать самого себя в создаваемый объект в момент вызова Get<>() — это уже нюансы реализации, чисто принципиально подход от этого не поменяется.
Re[9]: Передача контекста через множество уровней абстракции приложения
S>Потому что тогда требование "протаскиваем зависимости" заменяется на "протаскиваем код, который конструирует объекты", т.е. по сути ничего не меняется. Вот типичная схема: S>
S>static SomeResult DumpA(..., Context context)
S>{
S> var data = context.Get<IAReportService>().PrepareData(...);
S> var dumper = context.Get<IDumpService>();
S> return dumper.Dump(data);
S>}
S>
А точно надо протаскивать эти зависимости через цепочку вызовов (т.е. в параметрах методов)?
Я как-то до этого обходится инжектированием зависимостей через конструктор.
S>Нюансы в следующем: S>1. Зависимости заранее неизвестны. В крайнем случае бывает вот так: завтра клиент попросит — выкинем часть расчётов в пул задач, интернет поменяем на чтение переданного файла, лог будем показывать бегущей строкой на клиенте а IDumper вообще заменим на реализацию клиента. При необходимости конкретная реализация будет подсунута на лету, без пересборки всего хозяйства. При этом клиенты из другой организации на том же сервисе получат контекст с старыми зависимостями.
Тут тонкий нюанс. Я в целом против такого подхода, когда в теле метода происходит извлечение ссылок из некоторого Service Provider`а. Такой подход делает внешние зависимости класса "затерянными" в процедурном коде и практически исключает compile-time контроль того, что граф объектов собран как надо.
Re[5]: Передача контекста через множество уровней абстракции приложения
S>>Ну ок, не нравится AsyncLocal — можно с LogicalCallContext поэксперементировать. В 4.0 он с тасками емнип не дружил, начиная с 4.5 всё ок. S>>Пример. S>>Вообще-то call context с первого фреймворка был. Ссылки в предыдущем абзаце. Матчасть никто не знает, какабычна LW>Спасибо, ознакомлюсь. :D))
CallContext — шикарная штука! Работает! Огромное спасибо!
А есть какие-нибудь best practices по разработке бизнес-логики с использованием контекстов? Как донести до читателей-писателей кода, что вот эта штука зависит от того же ILogger, который необходимо проинициализировать, а если этого не сделать, то рухнешь на ровном месте с NotInitializedException? А может и не рухнешь, а рухнет у кастомера, который пошёл по самому редкому сценарию, не покрытому тесами и избежавшего пристального взора QA?
Re[5]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>У топикстартера как раз одна из проблем — как этот контейнер по всей цепочке вызовов протащить. Есть какие-нибудь идеи?
Здравствуйте, Sinix, Вы писали:
S>Нееет. Ты похоже просто с такими системами не сталкивался. В них типовая проблема — цепочка вызовов в 5-7 слоёв, в каждом из которых могут потребоваться свои зависимости, о которых вызывающий код ничего знать не должен. И общее число зависимостей — не единицы, а ближе к паре десятков. Ибо они добавляются/убираются буквально на ходу, под текущий набор требований.
Тот случай, когда телега впереди лошади и злоупотребление DP и IoC. Если вызывающий код не должен знать, значит вызывающий вызывающего должен знать. От таких архитектур и формулировок не по себе становится.
S>Что-то типа сильно нелинейного пайплайна получается.
Кодом людям нужно помогать!
Re[9]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>В крайнем случае бывает вот так: завтра клиент попросит — выкинем часть расчётов в пул задач, интернет поменяем на чтение переданного файла, лог будем показывать бегущей строкой на клиенте а IDumper вообще заменим на реализацию клиента.
Тут, по-моему, за ради душевного спокойствия разработчиков лучше сделать fork.
Кодом людям нужно помогать!
Re[10]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, 0x7be, Вы писали:
0>Я как-то до этого обходится инжектированием зависимостей через конструктор.
Ну так оно работает, только когда зависимости статичны и время жизни сервиса очень мало. Когда зависимости меняются по обстоятельствам, как у топикстартера, приходится или плодить туеву хучу аллокаций (что убивает всю идею с легковесным контекстом на корню), или всё-таки протаскивать контекст по цепочке вызовов.
0>Тут тонкий нюанс. Я в целом против такого подхода, когда в теле метода происходит извлечение ссылок из некоторого Service Provider`а.
Я уж не знаю, капсом писать что ли
У топикстартера требуется подсовывать зависимости _динамически_. Грубо говоря
using (var c = BeginTransaction())
{
service.DoSomething(obj1);
c.Commit();
}
using (var c2 = BeginTransaction())
{
service.DoSomething(obj2);
c2.Commit();
}
инстанс сервиса один, контекст (в этом примере — транзакции) — разные. Куча аллокаций терпима, когда у тебя десятки запросов в минуту. Когда больше — хошь-не-хошь на GC уже приходится посматривать.
Re[8]: Передача контекста через множество уровней абстракции приложения
S>>Нееет. Ты похоже просто с такими системами не сталкивался. В них типовая проблема — цепочка вызовов в 5-7 слоёв, в каждом из которых могут потребоваться свои зависимости, о которых вызывающий код ничего знать не должен. И общее число зависимостей — не единицы, а ближе к паре десятков. Ибо они добавляются/убираются буквально на ходу, под текущий набор требований.
S>Тот случай, когда телега впереди лошади и злоупотребление DP и IoC.
Ну если оно без необходимости так сделано — безусловно да. А когда чуть ли не ключевое биз-требование сводится к "логику обработки собирать на ходу, чуть ли не под каждый запрос индивидуально", все прочие варианты начинают требовать кучу времени на доработку напильником.
Разумеется, там есть свои нюансы, как по тестированию, так и по проектированию API / соблюдению контрактов, но это уже совсем оффтоп.
S>Тут, по-моему, за ради душевного спокойствия разработчиков лучше сделать fork.
Я ж говорю — не сталкивался :P
Форк поддерживать денюжкка стоит. Даже не столько денюжка, сколько время — разработчиков, QA, внедренцев, саппорта, возни с индивидуальным хостингом и т.д. и т.п. И с появлением различных комбинаций доработок это время меньше не становится
В общем, всё легко и просто, но ровно до тех пор, пока исключения единичны. Когда нет — нет
Re[9]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>Форк поддерживать денюжкка стоит. Даже не столько денюжка, сколько время — разработчиков, QA, внедренцев, саппорта, возни с индивидуальным хостингом и т.д. и т.п. И с появлением различных комбинаций доработок это время меньше не становится
Тут надо от заказчика плясать -- если генеральный и денежный, то воленс-ноленс. В противном случае дороже будет поддерживать кодовую базу, которая всё и для всех. Для отдельного разового заказчика можно и форк.
Кодом людям нужно помогать!
Re[6]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, LWhisper, Вы писали:
LW>А есть какие-нибудь best practices по разработке бизнес-логики с использованием контекстов?
Разумеется.
-1. рассмотреть идею 0x7be — все объекты короткоживущие, получаются вызовом метода самого контекста, в момент вызова контекст заполняет в объекте ссылку на себя.
Самый надёжный способ передавать контекст, если нет проблем с излишними аллокациями.
0. По возможности заменить CallContext на AsyncLocal.
Дальше начинается магия, т.к. все прочие варианты работают очень своеобразны, требуют минимум .net 4.5 _и_ это поведение не документировано.
1. ВНИМАТЕЛЬНО прочитать вот этот пост. Особенно текст, выделенный жирным.
2. Учитывать, что CallContext текущего потока подменяется асинхронным контекстом в момент вызова delegate.EndInvoke(). То же самое — для ремотинга для некоторых каналов.
3. Для WCF сервисов c хостингом под IIS по слухам CallContext работает не всегда. Деталей не помню, гугл находит всякую фигню.
4. По тем же слухам, в какой-то из версий, callContext терялся после .ConfigureAwait(false) Я подозреваю, что это было в какой-то из бет, поскольку наши тесты на этом не падали.
5. Тесты-тесты-тесты. Если вы решили использовать недокументированный вариант, то покройте тестами основные комбинации вызовов — через новый поток / thread pool / таски / await, await поверх IO (всякие ReadXxxAsync), комбинации .BeginInvoke() / .EndInvoke(), SynchronizationContext etc. Вставьте отладочные ассерты, которые будут проверять корректность заполнения контекста.
LW>Как донести до читателей-писателей кода, что вот эта штука зависит от того же ILogger, который необходимо проинициализировать, а если этого не сделать, то рухнешь на ровном месте с NotInitializedException? А может и не рухнешь, а рухнет у кастомера, который пошёл по самому редкому сценарию, не покрытому тесами и избежавшего пристального взора QA?
Ну а это уже общая для всех DI-фреймворков проблема. Самый простой способ — используем только документированные зависимости, которые оформлены в виде extension-методов, они должны быть доступны всегда. Для экзотики или пишем тесты + отладочные ассерты, или пусть себе падает — это будет возможно лучше, чем запуск в продакшне непроверенного кода.
Re: Передача контекста через множество уровней абстракции приложения
Здравствуйте, LWhisper, Вы писали:
LW>Проявляется это во всём, начиная с циклических референсов в сборках, благодаря рефлекшену, и заканчивая Constructor Injection c 28 зависимостями.
На одном проекте так и получилось. Вначале всякие static context юзали, потом IoC-контейнеры потом всё это выкинули и заменили тупым простым кодом с 28 зависимостями.
Единственный нюанс — код структурирован — собственно сами компоненты, где логика, юнит-тесты, у них как правило мало зависимостей (если становится много — хороший знак порезать класс). И связывающий код (wiring) — вот тут и появляются 28 параметров в конструкторе, но этот связывающий код — плоский, элементарный, тесты не нужны. Преимущество перед всякими ServiceLocator-подходами как тут упомянутыми Context — ошибки зависимостей будут проверяться компилятором, а не в runtime.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[7]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали: S>Ну а это уже общая для всех DI-фреймворков проблема. Самый простой способ — используем только документированные зависимости, которые оформлены в виде extension-методов, они должны быть доступны всегда. Для экзотики или пишем тесты + отладочные ассерты, или пусть себе падает — это будет возможно лучше, чем запуск в продакшне непроверенного кода.
Да вот особо то и не поможет, так как благодаря прозрачному протаскиванию контекста сквозь слои, где-нибудь уровне на 5ом, будет дёрнут этот самый экстеншен вида Context.GetLogger(), а компонент тремя уровнями выше кто-нибудь импортирует и дёрнет за хвост, даже не догадываясь, что чуть глубже есть какая-то привязка к контексту.
Re[8]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, LWhisper, Вы писали:
LW>Да вот особо то и не поможет, так как благодаря прозрачному протаскиванию контекста сквозь слои, где-нибудь уровне на 5ом, будет дёрнут этот самый экстеншен вида Context.GetLogger(), а компонент тремя уровнями выше кто-нибудь импортирует и дёрнет за хвост, даже не догадываясь, что чуть глубже есть какая-то привязка к контексту.
В смысле? Документированные зависимости должны быть доступны всегда. Их собственно для этого и документируют.
Ну и прозрачное протаскивание на практике абсолютно необязательно.
Re[6]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, LWhisper, Вы писали:
LW>CallContext — шикарная штука!
Скверная это штука. Пользоваться стоит только при полном отсутствии альтернатив
LW>А есть какие-нибудь best practices по разработке бизнес-логики с использованием контекстов? Как донести до читателей-писателей кода, что вот эта штука зависит от того же ILogger, который необходимо проинициализировать, а если этого не сделать, то рухнешь на ровном месте с NotInitializedException?
Главная рекомендация — не вносить динамику там, где можно обойтись статикой. Особенно это любителей DI контейнеров касается.
... << RSDN@Home 1.0.0 alpha 5 rev. 0 on Windows 8 6.2.9200.0>>
Здравствуйте, LWhisper, Вы писали:
LW>Некоторое время душу терзают мысли о Dependeny Injection. Но в компании уже был неудачный опыт внедрения этой штуки (о котором(ой) я ни сном, ни духом), закончившийся выпиливанием оной с большим количеством матюгов. В общем, не прижилось, да оно и понятно — время жизни и порядок инициализации объектов были непоняты от слова совсем, и зачастую людям возвращались объекты, которые они не просили, и были зарегистрированы вообще не пойми кем, когда и с какой целью.
Тут кстати путаница чувствуется. DI это один из способов IoC. А "штука" это, думается, имеешь в виду IoC-container. А регистрация это Service Locator.
В общем, разберись в 4 терминах: DI, SL, IoC, IoC-container.
В большинстве случаев тебе нужен constructor based DI, изредка, в динамической плагинной архитектуре какие-то части возможно потребуют SL. IoC-container хорош для быстрой разработки, но в долгоживущем большом проекте лучше его выкинуть нафиг.
LW>Возьмём, для примера, логирование.
Тут, мне видится не один логгер подменяемый в "глобальной переменной" thread local контекста, а должны быть разные логгеры в разных компонентах (constructor based DI), плюс возможно несколько фабрик, для разных целей: interface UserLoggerFactory {Logger getLoggerForUser(Credentials);} , interface RemoteHostLoggerFactory {Logger getLoggerForHost(InetAddress);} и т.п.
Т.е. каждый компонент имеет данный в конструкторе логгер и менять его нельзя. Ведь ты, думаю, не хочешь, чтобы компонент аутентификации пользователя, даже если он вдруг понадобился повторно, стал писать свои "секретные" логи в папку юзера. Друими словами, все перечисленные тобой сценарии говорят о том, что каждый конкретный компонент имеет свой логгер, переданный на этапе создания компонента; конечно, получается лажа, если все компоненты пытаются лезть и и даже менять одну и ту же глобальную переменную.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[2]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, ·, Вы писали:
·>Тут, мне видится не один логгер подменяемый в "глобальной переменной" thread local контекста, а должны быть разные логгеры в разных компонентах (constructor based DI), плюс возможно несколько фабрик, для разных целей: interface UserLoggerFactory {Logger getLoggerForUser(Credentials);} , interface RemoteHostLoggerFactory {Logger getLoggerForHost(InetAddress);} и т.п. ·>Т.е. каждый компонент имеет данный в конструкторе логгер и менять его нельзя. Ведь ты, думаю, не хочешь, чтобы компонент аутентификации пользователя, даже если он вдруг понадобился повторно, стал писать свои "секретные" логи в папку юзера. Друими словами, все перечисленные тобой сценарии говорят о том, что каждый конкретный компонент имеет свой логгер, переданный на этапе создания компонента; конечно, получается лажа, если все компоненты пытаются лезть и и даже менять одну и ту же глобальную переменную.
Идея замечательная, но не реальная. Я даже не буду сейчас касаться GC, поплохеет разработчикам, которым придётся добавить логер в 25597 классов.
Re[3]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, LWhisper, Вы писали:
LW>·>Т.е. каждый компонент имеет данный в конструкторе логгер и менять его нельзя. Ведь ты, думаю, не хочешь, чтобы компонент аутентификации пользователя, даже если он вдруг понадобился повторно, стал писать свои "секретные" логи в папку юзера. Друими словами, все перечисленные тобой сценарии говорят о том, что каждый конкретный компонент имеет свой логгер, переданный на этапе создания компонента; конечно, получается лажа, если все компоненты пытаются лезть и и даже менять одну и ту же глобальную переменную.
LW>Идея замечательная, но не реальная. Я даже не буду сейчас касаться GC,
А какие проблемы с GC в .net?
LW>поплохеет разработчикам, которым придётся добавить логер в 25597 классов.
Что значит добавить? Если он в 25597 классах уже есть, то просто надо потихотньку начать заменять глобальный вызов "StaticContext.getCurrentLogger()" на constructor injection. В IDEA — два рефакторинга: extract "StaticContext.getCurrentLogger()" as field with initialisation in constructor, затем в конструкторе вынести это же выражение как параметр. Потом потихоньку перекраивать создание объектов, вынося wiring в отдельное место.
Но чуда тут быть никакого не может. Если у тебя есть 25597 мест в которых всё плохо, то придётся менять все 25597 чтобы стало всё хорошо. Technical debt он такой...
Мы помню как-то меняли как-то статический вызов Clock.getCurrentMillis() на this.clock.getCurrentMillis()... тысячи мест, и ничего, за пару дней справились.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[4]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, ·, Вы писали:
·>Что значит добавить? Если он в 25597 классах уже есть, то просто надо потихотньку начать заменять глобальный вызов "StaticContext.getCurrentLogger()" на constructor injection. В IDEA — два рефакторинга: extract "StaticContext.getCurrentLogger()" as field with initialisation in constructor, затем в конструкторе вынести это же выражение как параметр. Потом потихоньку перекраивать создание объектов, вынося wiring в отдельное место.
·>Но чуда тут быть никакого не может. Если у тебя есть 25597 мест в которых всё плохо, то придётся менять все 25597 чтобы стало всё хорошо. Technical debt он такой... ·>Мы помню как-то меняли как-то статический вызов Clock.getCurrentMillis() на this.clock.getCurrentMillis()... тысячи мест, и ничего, за пару дней справились.
Зачем?
Re[5]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, LWhisper, Вы писали:
LW>·>Мы помню как-то меняли как-то статический вызов Clock.getCurrentMillis() на this.clock.getCurrentMillis()... тысячи мест, и ничего, за пару дней справились. LW>Зачем?
У нас время в разных местах используется из разных источников — либо время операционки (физическое), либо "официальное" время некоего главного компонента, чем гарантируется детерминированность выполнения. Это позволяет не только создавать идентичные реплики для failover, но и точно проигрывать в тестовом окружении production logs чтобы воспроизводить проблемы.
А ещё для разных авто-тестов. Во-первых, когда нет статиков проще писать юнит-тесты. А у нас ещё есть функциональные тесты, когда система целиком запускается на множестве машин и эмулируются всякие события, например, ежедневная рассылка отчётов, поэтому нужно уметь менять время во всей системе (мы зовём эту фичу tardis ) пока работает некий сценарий, поэтому время операционки нам не подходит.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re: Передача контекста через множество уровней абстракции приложения
LW>В связи с чем вопрос — что делать?
Намешено к сожалению у тебя куча вопросов. Разных.
LW>Тут и Id сессии, и Id текущей задачи, и идентификатор пользователя
Тут надо отделить мух от котлет. Контекст или State ака данные — это котлеты.
Классы аля логгер — это мухи. Это две абсолютно разные сущности.
Со стейтом можно поступать по разному, если этот стейт надо тягать с собой и хранить в памяти то по сути вариантов всего два.
Делать это явно или не явно. Не явный — передавать его как то неявно Либо через TLS (ThreadStatic) либо дважды неявно — создавать Child контейнер в котором регистрировать Singleton который будет возвращать state либо полностью либо по частям. Child хорошо тем что код который пользуется стэйтом по сути ничего не знает о его времени жизни. Он просто будет зависеть от интерфейса который позволяет работать со стейтом и всё. А минус этого подхода в том что по сути он скрывает в себе некую магию. Т.е. где то создаётся некий child контейнер, в котором есть определённый класс зарегистрированный определённым способом. И не дай бог кто то попросит этот интерфейс у родительского контейнера. Неявная передача контекста через ThreadStatic это проблемы с асинхронным кодом (async/await). Как вариант решения проблем с ThreadStatic и async/await может быть LogicalCallContext. В 4.5 он умеет "теч" между потоками.
Другой, тупой и простой вариант — тупо передавать стейт в каждом вызове каждого метода которому он может понадобится.
Из минусов лично: Лично я вижу парочку
1. Мы часто мы будем передавать "лишние данные", например, в класс необходимо передать информацию только о юзере и не нужны данные сессии и другие. Частично эту проблему можно решить разбив контекст на несколько мелких абстрактных классов или интерфейсов и один наследник. Соответсвенно кому нужен весь контекст будут принимать в параметрах наследник, кому части — части.
2. Может это звучит странно — но это точно не модный способ, он слишком простой, он НЕ даёт вам самовыразиться, показать вам знания аспектов работы контейнеров итп... Он выглядит топорно.
Из плюсов:
1. Это супер просто и абсолютно точно минимизирует различные возмодные проблемы с другими вариантами аля чайлд контейнера, ThreadStatic или LogicalCallContext.
2. Explicit всегда лучше Implicit это аксиома. Имея контекст явно его использование можно легко отслеживать по коду.
3. Это очень дёшево с точки зрения производительности.
Лично я бы сейчас выбрал явную передачу контекста. Причём контекст так же может реализовывать интерфейс который могут принимать классы аля логер в свои методах логирования. Заметь они НЕ будут принимать этот интерфейс в конструкторе а именно в методах и это нормально, ибо контекст это state, данные и они в идеале должны плавать через стек.
Народная мудрось
всем все никому ничего(с).
Re: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>У топикстартера как раз одна из проблем — как этот контейнер по всей цепочке вызовов протащить. Есть какие-нибудь идеи?
Его не нужно никуда протаскивать. Достаточно воспользоваться механизмом событий — у компонента будет набор релевантных событий, а логгер будет на них подписан. Логгер на них будет подписан — совсем не значит, что код логгера надо будет трогать. Будет отдельный слушатель событий, который будет слушать нужные события и пополнять лог, используя логгер.
Re: Передача контекста через множество уровней абстракции приложения
Здравствуйте, LWhisper, Вы писали:
LW>В связи с чем вопрос — что делать? LW>Есть некий воркфлоу приложения, который, продираясь через множество уровней абстракции, уходит всё глубже в дебри internal-сборок и попутно обрастает параметрами-зависимостями, в нижней точки своего падения приходя к тем самым 28 аргументам в конструкторе очередного класса, без которых он жить не может.
Один из главных принципов ООП: лампочка вкручивает себя в лампу сама.
LW>Тут и Id сессии, и Id текущей задачи, и идентификатор пользователя, и логгер и ещё чёрт знает что, на лицо ужасное, стрёмное внутри.
Как компонент, делающий реальную работу, зависит от идентификаторов и журналирования (многие тут очень узко понимают термины, поэтому уточняю: это сакральный логгинг по-русски)? Чтобы выполнить операцию, нужна инструкция, а не журнал — логично? Операция зависит от инструкции, журнал от состояния операции. Зависимость в твоём приложении иная — журнал зависит от компонентов, а не они от него. Если это осознать, то станет легче распутывать клубок зависимостей.
Как журналу узнавать о текущем состоянии компонентов и пополнять себя?
Как компонентам сообщать о своём состоянии независимым образом?
В .NET есть механизм событий — он решает оба вопроса простым способом. Не стоит его игнорировать.
LW>Хочется выбросить это в какой-нибудь легко доступный (как в плане кода, так и в плане перфоманса) контекст, с которым будет легко общаться. LW>Для данных таковым контекстом выступает БД, благодаря тому, что она всего одна, то доступ осуществляется через синглтон (когда-нибудь это обязательно выстрелит -_-). LW>А вот для всего остального он отсутствует, и тянутся эти зависимости per aspera ad astra.
LW>Возьмём, для примера, логирование. LW>Вот запускается приложение и моментально начинает писать лог. Имя лога спускается в аргументах командой строки. LW>Окей, от этого трудно отказаться (хотя и можно), но пока терпимо. LW>Происходит аутентификация пользователя. Теперь лог пишется уже в каталог этого горемычного юзера. LW>Юзер выполняет некоторую команду, лог пишется всё в ту же папку, но уже в файл с именем выполняемой команды. LW>Команда начинает параллелиться и работает с разными компьютерами в сети. Каждая задача начинает писать лог в файл с именем упомянутой выше команды + имя компьютера.
Это перечисление событий, на которые должен реагировать журнал(ы). Нужно продумать механизм публикации событий и подписки на них. Стандартные события .NET, доморощенный диспетчер событий — это уже детали.
LW>В процессе, мы начинаем использовать более низкоуровневые интерфейсы, которые тоже пишут лог и ни сном ни духом не знают ничего ни о юзерах, ни о командах, ни о хостах. Но при этом тоже хотят писать в лог текущее состояние и логировать ошибки, уходя на ретраи если сетка моргнула, но ещё есть шанс получить отклик от удалённой машинки. А писать то логи нужно в те же самые файлы с именем команды, хоста, в папке юзера. Появляются ThreadLocal-коллекции логеров, через которые проходят все вызовы.
Тот же принцип, журнал получает входящие события и сам решает куда они будут сохраняться. Вся хитроумная логика дробления журнала будет сосредоточена в одном месте, никаких контекстов.
LW>Но низкоуровневые компоненты тоже не лыком шиты и помимо распаралеливания на высшем уровне, дёргают асинхронные методы на нижнем, которые и вовсе выполняются в случайном потоке из пула и в свою очередь тоже дёргают ещё более низкоуровневые компоненты, которые всё ещё хотят писать логи в те же файлы, что и далёкий сверхудёрнувший их компонент. А значит приходится тащить сквозь все слои логер и регистрировать его в статичной ThreadLocal-коллекции из предыдущей серии.
Компоненты должны публиковать события, журнал их слушать.
LW>Некоторое время душу терзают мысли о Dependeny Injection. Но в компании уже был неудачный опыт внедрения этой штуки (о котором(ой) я ни сном, ни духом), закончившийся выпиливанием оной с большим количеством матюгов. В общем, не прижилось, да оно и понятно — время жизни и порядок инициализации объектов были непоняты от слова совсем, и зачастую людям возвращались объекты, которые они не просили, и были зарегистрированы вообще не пойми кем, когда и с какой целью. В общем, всё плохо, и опять же из-за непродуманной архитектуры, отрисованой на коленке, чтобы хоть как-то работало в стиле write-only.
Внедрение зависимостей — краткосрочный этап при запуске программы. Он тут просто ни при чём.
Re: Передача контекста через множество уровней абстракции приложения
LW>Тут и Id сессии, и Id текущей задачи, и идентификатор пользователя, и логгер и ещё чёрт знает что, на лицо ужасное, стрёмное внутри.
Ну грубо говоря у тебя проблема с тем что в конструкторы ты передаешь кусок стейта, нарубленого в салат ?
myCtor(int userId, int sessionId, int taskid)
в тупую развернуть зависимости, если задача хочет непременно записать в лог свой ид, ок, пускай мучается
class MyTask
{
Session session;
void WriteLog(string);
};
class Session
{
User user;
void WriteLog(string);
}
myCtor(MyTask )
LW>В процессе, мы начинаем использовать более низкоуровневые интерфейсы, которые тоже пишут лог и ни сном ни духом не знают ничего ни о юзерах, ни о командах, ни о хостах.
вот это ключевой момент во всей эпопее. Большинству кода нахрен не нужна сессия, пользователь, и прочая лабуда. Если критически взглянуть, то она и выше в большинстве случаев не была нужна.
class MyLowLevelContraption
{
MyLowLevelContraption(int userId, int sessionId, int taskid, IMyLogger); // вот такого быть не должно
MyLowLevelContraption(MyTask ); //и такого тоже, хотя оно уже лучше
MyLowLevelContraption(IMyLogger ); // и даже такого не надо делать
.....
}
Здравствуйте, Vladek, Вы писали:
S>>У топикстартера как раз одна из проблем — как этот контейнер по всей цепочке вызовов протащить. Есть какие-нибудь идеи?
V>Его не нужно никуда протаскивать. Достаточно воспользоваться механизмом событий — у компонента будет набор релевантных событий, а логгер будет на них подписан.
Такие советы в 100% случаев означают, что человек или не использовал логирование, или привык давать советы, не задумываясь о последствиях.
Если подобное предлагается абсолютно всерьёз на реальном проекте и намёков человек не понимает, то лечится просто. Поступаем точно так же, как и с полезным предложением. Даётся реальный код с парой сотен вызовов логгера (это очень мало) из которых половина — в static-методах, а ещё треть — в await | итераторах и предлагается оценить трудозатраты по переводу на модель событий. Также расписать возможные косяки (если не назовёт утечку памяти / вызов обработчиков событий в "неправильном" порядке и взаимные side-эффекты от подписчиков — ещё один минус в бюджет предложения) и стоимость мер по их исправлению.
Если по итогам человек не проникся, то дальше вариантов полно. Или подсаживаем в команду с хорошим лидом на переобучение, или перекидываем на проект, где от кипучей деятельности будет польза, или расстаёмся с хорошими рекомендациями, пусть конкуренты берут. Человек, способный из любви к абстрактным идеям протащить их в проект, в итоге навредит так что любая сознательная диверсия детским утренником покажется.
Re[7]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>Здравствуйте, Vladek, Вы писали:
S>>>У топикстартера как раз одна из проблем — как этот контейнер по всей цепочке вызовов протащить. Есть какие-нибудь идеи?
V>>Его не нужно никуда протаскивать. Достаточно воспользоваться механизмом событий — у компонента будет набор релевантных событий, а логгер будет на них подписан. S>Такие советы в 100% случаев означают, что человек или не использовал логирование, или привык давать советы, не задумываясь о последствиях.
S>Если подобное предлагается абсолютно всерьёз на реальном проекте и намёков человек не понимает, то лечится просто. Поступаем точно так же, как и с полезным предложением. Даётся реальный код с парой сотен вызовов логгера (это очень мало) из которых половина — в static-методах, а ещё треть — в await | итераторах и предлагается оценить трудозатраты по переводу на модель событий. Также расписать возможные косяки (если не назовёт утечку памяти / вызов обработчиков событий в "неправильном" порядке и взаимные side-эффекты от подписчиков — ещё один минус в бюджет предложения) и стоимость мер по их исправлению.
Ты уж потрудись, сам распиши косяки. Желательно с примерами. Что мне толку с твоего гонора?
Re[8]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Vladek, Вы писали:
V>Ты уж потрудись, сам распиши косяки. Желательно с примерами. Что мне толку с твоего гонора?
Ну так это не гонор, эт стандартная реакция на рекомендации в стиле "мыши станьте ёжиками". Не, серьёзно, ну как можно советовать очевиднейшую ерунду?
Самый простейший пример (реальный код, имена понятное дело изменены):
Здравствуйте, Sinix, Вы писали:
S>Попробуйте изобразить то же самое на событиях. Только плиз не чтоб было, а как бы вы написали для реального кода, который в продакшн пойдёт.
В продакшен пойдёт код, который не будет загромождать журнал мусором. В случае проблем от журнала мне нужна запись действий пользователя (команды и запросы) и какие-то данные на выходе от программы (минимум время выполнения операции и стек вызов в случае исключительной ситуации), чтобы потом воспроизвести проблему. То есть, внутреннее состояние программы не так уж и важно, а следовательно журнал не очень сильно будет влиять на интерфейсы компонентов. Внутренне состояние программы не важно, если оно детерминировано — это значит, если я допустил ситуацию, когда проблему я могу воспроизвести только в продакшене — я не доделал свою работу и должен не логи мусором забивать, чтобы всё-таки получить какое-то представление о том, что там происходит, а доделывать работу, чтобы программа вела себя стабильно, независимо от внешней среды.
Вернёмся к методу из примера. В нём не будет журнала вообще. Входные параметры (команда, запрос), если они идут от пользователя или любого внешнего события (очередь сообщений, например), будут помещены в журнал в момент их появления, до вызова метода. Информация об исключении вернётся сама вверх по стеку и тоже будет помещена в журнал в нужный момент (обычно там же, где и началась обработка запроса от пользователя/внешнего окружения).
Тут диспетчер dispatcher — занимается передачей команды нужному обработчику. Вот этот вот код — точно такой же диспетчер-обёртка с журналом (в данном случае это обычный отладочный вывод). Обработчики команд тоже могут иметь свои журналы, работающие похожим образом. Код, собственно выполняющий работу, не замусорен вызовами журнала или обращениями к каким-то контекстам.
Re[10]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Vladek, Вы писали:
V>В продакшен пойдёт код, который не будет загромождать журнал мусором. V>Вернёмся к методу из примера. В нём не будет журнала вообще.
Я ж говорю — вы не используете логгинг, что обсуждать-то?
Какой нафиг "Debug.WriteLine"? Вы в продакшне на живой сервер будете отладочные сборки подсовывать??? Или у вас любая ситуация всегда локально воспроизводится, без завязки на пользовательские данные?
V>если я допустил ситуацию, когда проблему я могу воспроизвести только в продакшене — я не доделал свою работу и должен не логи мусором забивать, чтобы всё-таки получить какое-то представление о том, что там происходит, а доделывать работу, чтобы программа вела себя стабильно, независимо от внешней среды.
Угу-угу. Вы это товарищам внедренцам расскажите в ситуации, когда человек за 10k километров от вас и у него уже рабочий день заканчивается, у вас меньше половины дня на починить систему (т.е. минимум часов 5 — не ваши, а QA и внедренцев), удалённый отладчик вы подключить не можете ибо прав на сервер у вас нет, да и не получите, ибо допуск оформлять ещё надо. Абсолютно типовая ситуация в любом более-менее крупном продукте, минимум раз в квартал встречается.
Удачи, что ещё сказать.
Лечится не менее стандартно, инструкцией вида "ставим галочку "всё пропало, помогииите", воспроизводим, оставляем контактные данные, ждём звонка/письма". Если у людей паранойя и доступ к интернету перекрыт, то придётся ещё самим отправить файлы с итогами трассировки по указанному адресу, да.
V>Вернёмся к методу из примера. V>this.dispatcher
Ну, т.е. простой static-метод внезапно становится членом класса и начинает подгребать зависимости, которые в оригинале спокойно передавались в параметрах.
При этом исходная задача — скинуть сообщение об ошибке в конкретный логгер, а не в общий журнал — не решена. Ну и самих событий, ради которых мы и начали изобретать велосипед, тоже внезапно нет.
И это мы делаем для того, чтобы _упростить_ тестирование / сопровождение, ничего не упустил?
Re[11]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>Здравствуйте, Vladek, Вы писали:
V>>В продакшен пойдёт код, который не будет загромождать журнал мусором. V>>Вернёмся к методу из примера. В нём не будет журнала вообще.
S>Я ж говорю — вы не используете логгинг, что обсуждать-то? S>Какой нафиг "Debug.WriteLine"? Вы в продакшне на живой сервер будете отладочные сборки подсовывать??? Или у вас любая ситуация всегда локально воспроизводится, без завязки на пользовательские данные?
Пользовательские данные будут в журнале — запись его действий. А дальше уже можно анализировать локально что происходит с программой после ввода этих данных. Если там есть БД, которая нужна для воспроизведения проблемы, то к ней можно подключиться в режиме чтения или использовать копию.
V>>если я допустил ситуацию, когда проблему я могу воспроизвести только в продакшене — я не доделал свою работу и должен не логи мусором забивать, чтобы всё-таки получить какое-то представление о том, что там происходит, а доделывать работу, чтобы программа вела себя стабильно, независимо от внешней среды.
S>Угу-угу. Вы это товарищам внедренцам расскажите в ситуации, когда человек за 10k километров от вас и у него уже рабочий день заканчивается, у вас меньше половины дня на починить систему (т.е. минимум часов 5 — не ваши, а QA и внедренцев), удалённый отладчик вы подключить не можете ибо прав на сервер у вас нет, да и не получите, ибо допуск оформлять ещё надо. Абсолютно типовая ситуация в любом более-менее крупном продукте, минимум раз в квартал встречается.
S>Удачи, что ещё сказать.
Это вам удачи, со мной такого давно не происходило. Любой конкретный пример из своего опыта тут ни при чём. В самом деле, какой смысл обсуждать косяки в своей работе? Косяки придётся исправлять как придётся, раз уж они случились. Мы обсуждаем как не допускать косяков, а не как их исправлять. Если косяк становится типичной ситуацией — надо что-то менять в консерватории. Брут-форс не поможет.
S>Лечится не менее стандартно, инструкцией вида "ставим галочку "всё пропало, помогииите", воспроизводим, оставляем контактные данные, ждём звонка/письма". Если у людей паранойя и доступ к интернету перекрыт, то придётся ещё самим отправить файлы с итогами трассировки по указанному адресу, да.
V>>Вернёмся к методу из примера. V>>this.dispatcher S>Ну, т.е. простой static-метод внезапно становится членом класса и начинает подгребать зависимости, которые в оригинале спокойно передавались в параметрах. S>При этом исходная задача — скинуть сообщение об ошибке в конкретный логгер, а не в общий журнал — не решена. Ну и самих событий, ради которых мы и начали изобретать велосипед, тоже внезапно нет.
Пример с событиями был в другом посте, я давал ссылку. Мой код — пример моего подхода (без событий). С событиями нет особых проблем — есть замечательная библиотека Rx, которая поможет справиться с перенаправлением событий по журналам. Ещё проще, иметь отдельного слушателя на каждый класс компонентов.
S>И это мы делаем для того, чтобы _упростить_ тестирование / сопровождение, ничего не упустил?
Это надо у автора первого поста спрашивать, зачем ему такая система логов. Мы же предлагаем простые способы решить проблему без привлечения грубой силы.
Re[12]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Vladek, Вы писали:
V>Пользовательские данные будут в журнале — запись его действий. А дальше уже можно анализировать локально что происходит с программой после ввода этих данных. Если там есть БД, которая нужна для воспроизведения проблемы, то к ней можно подключиться в режиме чтения или использовать копию.
Так как они в журнале будут, если вы только для отладочных сборок информацию пишете?
Базы вам в 90% случаев не отдадут ни при каких условиях. Максимум — разрешат перекинуть сисадмину отдельные запросы (только select, разумеется), он их проверит, выполнит и перекинет обратно результат (при этом ещё, бывает, допускается только проверять наличие записей/значений в полях, но не вытаскивать сами данные). Содержимое логов точно так же согласовывается (даже абсолютные пути/url-ы обычно запрещены), копия отправленных данных сохраняется у заказчика для возможных разбирательств.
V>Это вам удачи, со мной такого давно не происходило.
А, ну так это комнатные условия, серьёзно. Любая попытка внедрить боль-менее сложную систему у нового клиента очень часто приводит к выявлению каких-либо косяков. Необязательно это именно ошибки в коде (их как раз ловят быстро). Чаще всего это сочетание неверной настройки и частных случаев нескольких бизнес-правил, которые по отдельности формально верны, но вместе приводят к неочевидным результатам. И хорошо если это заметные вещи типа годовой премии в десятикратном размере (угу, внедренцы по запарке поменяли 10% на 10x), которые легко воспроизводятся и диагностируются. Хуже, когда подобные мелкие ошибки приходится править постфактум. Вот тогда действительно веселуха.
V>Если косяк становится типичной ситуацией — надо что-то менять в консерватории. Брут-форс не поможет.
В любой крупной системе всегда есть что чинить/исправлять, это текучка как обновления в win. Менять в консерватории надо, если рутина в подвиг превращается. Тот же "полдня на исправить" как правило занимает часа четыре до момента поставки фикса клиенту. Ну или, всё те же 4 часа на диагностику "исправление нетривиальное, вот вам костыль, нормально починим с плановой рассылкой обновлений".
V>>>Вернёмся к методу из примера. V> Мой код — пример моего подхода (без событий). С событиями нет особых проблем — есть замечательная библиотека Rx, которая поможет справиться с перенаправлением событий по журналам. Ещё проще, иметь отдельного слушателя на каждый класс компонентов.
Всё замечательно, только оно никак не относится к поставленному вопросу: как быть с реальным кодом, который в 99% случаев на событийную модель ложится преотвратно?
Особенно с приседаниями в виде Rx, которая требует досконального знания матчасти для того, чтобы корректно управлять временем жизни подписки.
Re[13]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, Sinix, Вы писали:
S>Здравствуйте, Vladek, Вы писали:
V>>Пользовательские данные будут в журнале — запись его действий. А дальше уже можно анализировать локально что происходит с программой после ввода этих данных. Если там есть БД, которая нужна для воспроизведения проблемы, то к ней можно подключиться в режиме чтения или использовать копию.
S>Так как они в журнале будут, если вы только для отладочных сборок информацию пишете?
Это зависит от конфигурации программы. В простом случае всё пишется в файл.
S>Базы вам в 90% случаев не отдадут ни при каких условиях. Максимум — разрешат перекинуть сисадмину отдельные запросы (только select, разумеется), он их проверит, выполнит и перекинет обратно результат (при этом ещё, бывает, допускается только проверять наличие записей/значений в полях, но не вытаскивать сами данные). Содержимое логов точно так же согласовывается (даже абсолютные пути/url-ы обычно запрещены), копия отправленных данных сохраняется у заказчика для возможных разбирательств.
Это решаемо, обычно и БД не нужна (проблема проявляется при записи новой информации), если всё-таки надо — её обфусцируют.
V>>Это вам удачи, со мной такого давно не происходило. S>А, ну так это комнатные условия, серьёзно. Любая попытка внедрить боль-менее сложную систему у нового клиента очень часто приводит к выявлению каких-либо косяков. Необязательно это именно ошибки в коде (их как раз ловят быстро). Чаще всего это сочетание неверной настройки и частных случаев нескольких бизнес-правил, которые по отдельности формально верны, но вместе приводят к неочевидным результатам. И хорошо если это заметные вещи типа годовой премии в десятикратном размере (угу, внедренцы по запарке поменяли 10% на 10x), которые легко воспроизводятся и диагностируются. Хуже, когда подобные мелкие ошибки приходится править постфактум. Вот тогда действительно веселуха.
V>>Если косяк становится типичной ситуацией — надо что-то менять в консерватории. Брут-форс не поможет. S>В любой крупной системе всегда есть что чинить/исправлять, это текучка как обновления в win. Менять в консерватории надо, если рутина в подвиг превращается. Тот же "полдня на исправить" как правило занимает часа четыре до момента поставки фикса клиенту. Ну или, всё те же 4 часа на диагностику "исправление нетривиальное, вот вам костыль, нормально починим с плановой рассылкой обновлений".
Да. подвигов быть не должно.
V>>>>Вернёмся к методу из примера. V>> Мой код — пример моего подхода (без событий). С событиями нет особых проблем — есть замечательная библиотека Rx, которая поможет справиться с перенаправлением событий по журналам. Ещё проще, иметь отдельного слушателя на каждый класс компонентов.
S>Всё замечательно, только оно никак не относится к поставленному вопросу: как быть с реальным кодом, который в 99% случаев на событийную модель ложится преотвратно?
Как же не ложится? Автор указал набор событий, которые происходят в программе и влияют на работу с журналом (аутентификация, публикация команд, запуск параллельных операций). Только у него эти события в коде не имеют чёткого представления — вот и надо оформить их явно — в виде обычных дотнетовских событий или ещё как.
S>Особенно с приседаниями в виде Rx, которая требует досконального знания матчасти для того, чтобы корректно управлять временем жизни подписки.
Матчасть надо знать в любом случае. Жизнь компонентов тоже должна быть абсолютно ясна из кода: вот тут компонент рождается, вот тут мы от него избавляемся. Компонент сам может сообщать о своей скорой смерти отдельным событием и журнал будет удалять свою подписку.
Re: Передача контекста через множество уровней абстракции приложения
У меня простой вариант.
* Давать тредам имена в стиле VasyanVasyanov.SESSION14.PC123.CMDID123.THREAD111 .
* Всё писать в один лог и парсить по имени треда .
Re: Передача контекста через множество уровней абстракции приложения
Здравствуйте, LWhisper, Вы писали:
LW>Спасибо за внимание.
1.
У вас тум куча логеров в одном процессе что ли? Это не правильно. Или это не логеры.
2.
Если надо в некоторых условиях писать в лог инфо про юзера, то это должен делать тот кусок кода который вызывает метод логера Write. Если таких место много сделайте экстенш метод вашему тому классу который владеет инфой про юзера. и пользуйтесь им.
3.
В Trace.Listeners свой writer не пробовали подсунуть?
Кстати, писать в один файл их разных потоков никто не запрещает.
Re[2]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, VladCore, Вы писали: VC>У вас тум куча логеров в одном процессе что ли? Это не правильно. Или это не логеры.
А как же .NET'овская практика, создавать по логгеру на класс?
class Some
{
public Some()
{
_log = LogFactory.Get(GetType().Name);
}
}
Re[3]: Передача контекста через множество уровней абстракции приложения
Здравствуйте, UberPsychoSvin, Вы писали:
UPS>Здравствуйте, VladCore, Вы писали: VC>>У вас тум куча логеров в одном процессе что ли? Это не правильно. Или это не логеры. UPS>А как же .NET'овская практика, создавать по логгеру на класс? UPS>
UPS>class Some
UPS>{
UPS> public Some()
UPS> {
UPS> _log = LogFactory.Get(GetType().Name);
UPS> }
UPS>}
UPS>
LW>Возьмём, для примера, логирование. LW>Вот запускается приложение и моментально начинает писать лог. Имя лога спускается в аргументах командой строки. LW>Окей, от этого трудно отказаться (хотя и можно), но пока терпимо. LW>Происходит аутентификация пользователя. Теперь лог пишется уже в каталог этого горемычного юзера.
выделил для тех кто не читал вопрос.
кстати, эта порочная практика с функцией _log = function(type_name) {....} пришла из явы. Когда-то было круто. Её не надо использовать. см. вопрос топик-стартера.