Всем привет.
Пришло время собирать камни — привести архитектуру приложения в божеский вид.
Сейчас она описывается меткой фразой "некогда думать, надо копать".
Проявляется это во всём, начиная с циклических референсов в сборках, благодаря рефлекшену, и заканчивая 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>У топикстартера как раз одна из проблем — как этот контейнер по всей цепочке вызовов протащить. Есть какие-нибудь идеи?