|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Погружаюсь в паттерн "Внедрение зависимостей". Проштудировал "Внедрение зависимостей в .NET" Марка Симана и некоторые попавшиеся по ходу дела примеры. В целом в теории все понятно, но остался все-таки некоторый туман, касающийся практического применения в некоторых случаях. Вполне возможно, что я однобоко смотрю на ситуацию и меня нужно подтолкнуть в нужную сторону. 1. Единственный Resolve Я исхожу из посыла Симана, на котором он категорически настаивает: Resolve используется один раз, все остальные зависимости подгружаются уже автоматически через DI. Иначе ломается вся схема DI, и, если вызывать Resolve на более глубоких слоях, паттерн начинает превращаться в Service Locator, который Симан считает антипаттерном. Что подсказывает ваш опыт? Нужно ли этому правилу следовать неукоснительно? Или без фанатизма: применять Resolve глубже, если таким образом упрощается структура кода. Следующие пункты в какой-то степени вытекают из первого. 2. Конструкторы с параметрами Часто класс создается с конкретными параметрами, передаваемыми через конструктор, после чего класс готов к употреблению. Причем параметры заранее неизвестны (парамеры рантайма). То есть два пути получается: 2а) отказываться от инициализации класса в конструкторе и вводить понятие "неинициализированного класса" и контроль за тем, чтобы неинициализированный класс не мог использоваться, специальный метод инициализации. 2б) делать абстрактную фабрику под каждый конкретный класс и внедрять ее как зависимость В общем, дополнительный гемор или как? 3. Перегруженные конструкторы Это как вариант второго пункта, то есть отказываться от перегруженных конструкторов в принципе и идти по принципу отдельной инициализации класса или персональной фабрики? Тут я приведу конкретный пример, возможно я не прав с архитектурой. Я упрощаю, надо иметь ввиду, что реальный класс несколько сложней. Пример в контексте ASP.NET MVC: есть некая модель бизнес-уровня - к примеру "карточка клиента", содержащая некоторое количество полей (необязательно элементарных). Модель инкапсулирует некоторые проверки, взаимодействие с репозиторием и некоторые другие функции). Модель может быть создана тремя путями: а) создание нового клиента (при этом в конструкторе может быть несколько предустановок, например "родительский клиент") б) загрузка из базы (в конструкторе передается id) в) создание на основе введенных пользователем данных (в конструктор передается пул введенных значений). Все это удобно реализуется тремя перегрузками конструктора. Как изменить подход, чтоб внедрить DI? 4. Необязательные зависимости Опять же пример из жизни MVC: классу контроллера может потребоваться зависимость от нескольких моделей, но реально будет использоваться одна-две, в зависимости как от вызванного метода (Action), так и, например, от результатов валидации. Внедрять зависимости от всех моделей? зачем ему этот ворох? Бить контроллер на множество мелких контроллеров? Тоже не очень удобный вариант: зачем мы тогда вообще объединяем несколько методов в контроллер. ... |
|||
:
Нравится:
Не нравится:
|
|||
29.07.2014, 20:56 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Shocker.Pro, Абстрактная фабрика не нужна, конейнер и есть такая фабрика. Для необязательных зависимостей надо использовать свойства. Надо подбирать контейнер, который имеет возможность ижектить зависимости в виде lazy. Надо учиться работать с дочерними контейнерами. НЕ НАДО инжектить объекты с данными. ... |
|||
:
Нравится:
Не нравится:
|
|||
29.07.2014, 21:21 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
gandjustasАбстрактная фабрика не нужна, конейнер и есть такая фабрика.разница в том, что с помощью фабрики можно получить объект с заданными параметрами в момент создания экземпляра, в то время как зависимость создается заранее. Тут-то у меня и ступор возникает. Собственно, абстрактная фабрика - это как раз рекомендация Симана для случаев, когда нужно создавать экземпляр с параметрами рантайма. gandjustasНадо подбирать контейнер, который имеет возможность ижектить зависимости в виде lazy.Ну а как потом получать эту зависимость с конкретными параметрами. Резолвить через контейнер? lazy - это та же фабрика-посредник, которая позволит создать экземпляр позже. gandjustasДля необязательных зависимостей надо использовать свойства.как это применить к описанной мной ситуации? Ну создал я несколько свойств вместо параметров конструктора. Контейнер ведь будет заполнять их все равно в момент создания экземпляра, а нужность или ненужность той или иной зависимости выясняется уже во время работы экземпляра. gandjustasНадо учиться работать с дочерними контейнерами.В с этим не сталкивался, можно чуть подробнее? gandjustasНЕ НАДО инжектить объекты с данными.имеются ввиду РОСО-объекты? А если есть объект, который обрабатывает данные и промежуточно их хранит? А если ему глубже нужна опять абстрактная зависимость для обработки этих данных? ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 11:30 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Shocker.ProЯ исхожу из посыла Симана, на котором он категорически настаивает: Resolve используется один раз, все остальные зависимости подгружаются уже автоматически через DI. Иначе ломается вся схема DI, и, если вызывать Resolve на более глубоких слоях, паттерн начинает превращаться в Service Locator, который Симан считает антипаттерном. Что подсказывает ваш опыт? Нужно ли этому правилу следовать неукоснительно? Или без фанатизма: применять Resolve глубже, если таким образом упрощается структура кода. я полностью согласен с этим утверждением. надо категорически избегать использовать Service Locator. вариантов масса. можно передавать фабрики. в самом крайнем случае можно резолвить сам контейнер, но обычно мне удавалось обойтись без него. могут быть моменты, где без Service Locator тяжело обойтись. это часть кода, который уже написан кем-то другим без внедрения зависимостей. в той части, если что-то дописывать, передавать какие-нибудь классы, придётся дёргать SL. в штатной ситуации SL не нужен. Shocker.ProЧасто класс создается с конкретными параметрами, передаваемыми через конструктор, после чего класс готов к употреблению. Причем параметры заранее неизвестны (парамеры рантайма). То есть два пути получается: 2а) отказываться от инициализации класса в конструкторе и вводить понятие "неинициализированного класса" и контроль за тем, чтобы неинициализированный класс не мог использоваться, специальный метод инициализации. 2б) делать абстрактную фабрику под каждый конкретный класс и внедрять ее как зависимость В общем, дополнительный гемор или как? классы, которые требуют внедрения зависимостей не надо создавать вручную. дополнительные параметры в таком случае передаются: а) либо через публичные свойства б) либо через инициализатор ещё вариант, внедрять фабрику, которая может инстанцировать класс с набором конкретных параметров. Shocker.ProЭто как вариант второго пункта, то есть отказываться от перегруженных конструкторов в принципе и идти по принципу отдельной инициализации класса или персональной фабрики? Тут я приведу конкретный пример, возможно я не прав с архитектурой. Я упрощаю, надо иметь ввиду, что реальный класс несколько сложней. Пример в контексте ASP.NET MVC: есть некая модель бизнес-уровня - к примеру "карточка клиента", содержащая некоторое количество полей (необязательно элементарных). Модель инкапсулирует некоторые проверки, взаимодействие с репозиторием и некоторые другие функции). Модель может быть создана тремя путями: а) создание нового клиента (при этом в конструкторе может быть несколько предустановок, например "родительский клиент") б) загрузка из базы (в конструкторе передается id) в) создание на основе введенных пользователем данных (в конструктор передается пул введенных значений). Все это удобно реализуется тремя перегрузками конструктора. Как изменить подход, чтоб внедрить DI? однозначно все 3 способа плохи для конструкторов. этим должна озаботиться фабрика, или на крайний случай фабричный метод. Shocker.ProОпять же пример из жизни MVC: классу контроллера может потребоваться зависимость от нескольких моделей, но реально будет использоваться одна-две, в зависимости как от вызванного метода (Action), так и, например, от результатов валидации. Внедрять зависимости от всех моделей? зачем ему этот ворох? Бить контроллер на множество мелких контроллеров? Тоже не очень удобный вариант: зачем мы тогда вообще объединяем несколько методов в контроллер. один из вариантов, это использовать ленивую инициализацию Lazy<T>. можно использовать в параметрах конструктора или в публичных свойствах. бить контроллер не надо. можно аггрегировать наборы в специальные классы. или опять же фабрики. но Lazy<T> самый простой вариант. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 11:42 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
gandjustasАбстрактная фабрика не нужна, конейнер и есть такая фабрика. Перестань вводить людей в заблуждение. Контейнер не есть фабрика, хотя может выполнять её функции. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 11:51 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
hVosttоднозначно все 3 способа плохи для конструкторов. этим должна озаботиться фабрика, или на крайний случай фабричный метод.то есть под каждый класс сущности делать персональную фабрику, предназначенную именно для этого класса? (универсальная типобезопасная фабрика не получится из-за того, что у классов есть специфические параметры создания) hVosttодин из вариантов, это использовать ленивую инициализацию Lazy<T>имеется ввиду System.Lazy<T>? Ок, прикину этот способ на свои задачи. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 11:58 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Shocker.Proто есть под каждый класс сущности делать персональную фабрику, предназначенную именно для этого класса? (универсальная типобезопасная фабрика не получится из-за того, что у классов есть специфические параметры создания) Без конкретной задачи, трудно сказать что будет лучше. Но в целом да, или фабричный метод (который может нести на борту сам класс, можно получать его через функтор). Если на момент создания ты знаешь какой конструктор надо использовать (в твоём варианте), значит ты также будешь знать какую фабрику использовать. Только во втором случае фабрика будет получена через DI, и получит все требуемые зависимости. Ещё вариант, в случае Autofac, можно определить сложный процесс инстанцирования класса с выбором нужного контруктора, но это плохо сопровождается, хотя и меньше кода. Shocker.Proимеется ввиду System.Lazy<T>? Ок, прикину этот способ на свои задачи. Ага. По мне, так очень удобно. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 12:09 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Shocker.ProgandjustasАбстрактная фабрика не нужна, конейнер и есть такая фабрика.разница в том, что с помощью фабрики можно получить объект с заданными параметрами в момент создания экземпляра, в то время как зависимость создается заранее. Тут-то у меня и ступор возникает. С чето ты взял что зависимость создается заранее? В контейнере ты регистрируешь тип и указываешь политику управления временем жизни. Чаще всего это per-request в веб-приложении, чтобы на каждый запрос создавался один экземпляр. Shocker.ProСобственно, абстрактная фабрика - это как раз рекомендация Симана для случаев, когда нужно создавать экземпляр с параметрами рантайма. Хреновая рекомендация честно говоря. Контейнеры обычно умеют: инжектить сами себя (если тебе надо тип в рантайме выбирать) и\или инжектить Func<T>\Lazy<T>, когда в контейнере зарегистрированы типы T, чтобы не создавать объект если он не нужен. Если надо руками создавать фабрики, то выбрасывай контейнер, он бесполезен. Shocker.ProgandjustasНадо подбирать контейнер, который имеет возможность ижектить зависимости в виде lazy.Ну а как потом получать эту зависимость с конкретными параметрами. Резолвить через контейнер? lazy - это та же фабрика-посредник, которая позволит создать экземпляр позже. Конкретные параметры это что? Два варианта: 1) Параметры неизвестные на момент регистрации, но известные на момент выполнения запроса (пользователь, route values итд) - тут нужно использовать дочерние контейеры и в них пихать все параметры запроса. 2) Параметры, вычисляемые только при обработке данных, например сумма заказов по клиенту. В этом случае не нужно использовать IoC вообще - явно напиши код, который обрабатывает необходимые величины. Не надо идею IoC доводить до абсурда, когда все операторы new замняются на инъекцию. До добра такое не доводит. Основная логика в приложении должна быть читаема, чтобы можно пройти по цепочки вызовов, а все инъекции должны быть очевидны (лучше всего разметить атрибутами). Иначе при чтении будет очень сложно определить откуда и как берется значение. Shocker.ProgandjustasНадо учиться работать с дочерними контейнерами.В с этим не сталкивался, можно чуть подробнее? Это зависит от контейнера. Unity например позволяет создавать Child, который а) имеет время жизни меньше parent б) может расширять и переопределять набор зависимостей Shocker.ProgandjustasНЕ НАДО инжектить объекты с данными.имеются ввиду РОСО-объекты? А если есть объект, который обрабатывает данные и промежуточно их хранит? А если ему глубже нужна опять абстрактная зависимость для обработки этих данных? Промежуточные тоже не надо инжектить, код становится нереально сложно читать если логика становится завязана на контейнер. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 12:31 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
gandjustasЕсли надо руками создавать фабрики, то выбрасывай контейнер, он бесполезен. Чумовой бред. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 12:47 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
gandjustasС чето ты взял что зависимость создается заранее? В контейнере ты регистрируешь тип и указываешь политику управления временем жизни. Чаще всего это per-request в веб-приложении, чтобы на каждый запрос создавался один экземпляр.Заранее - не значит в момент запуска приложения, но в момент создания графа, то есть как минимум создания класса, в котором я вызываю подкласс, использующий зависимость, то есть параметры вызова еще неизвестны gandjustasХреновая рекомендация честно говоря. Контейнеры обычно умеют: инжектить сами себя (если тебе надо тип в рантайме выбирать) и\или инжектить Func<T>\Lazy<T>, когда в контейнере зарегистрированы типы T, чтобы не создавать объект если он не нужен. Если надо руками создавать фабрики, то выбрасывай контейнер, он бесполезен.разумеется, лямбда это просто частный случай фабрики. gandjustasКонкретные параметры это что? Два варианта: ... 2) Параметры, вычисляемые только при обработке данных, например сумма заказов по клиенту. В этом случае не нужно использовать IoC вообще - явно напиши код, который обрабатывает необходимые величины.вот именно второй вариант. gandjustasНе надо идею IoC доводить до абсурда, когда все операторы new замняются на инъекцию. До добра такое не доводит.Ну, собственно, я и пытаюсь найти ту границу разумного использования DI. Мне как раз неясно, как быть, если класс, создаваемый через new хочет зависимость, которая в других случаях настраивается через контейнер? Вручную транслировать все (или сам контейнер) через конструктор? gandjustasЭто зависит от контейнера. Unity например позволяет создавать Child, который а) имеет время жизни меньше parent б) может расширять и переопределять набор зависимостейиспользую Autofac. То есть, фактически, это создание нового графа? То есть точка резолва корня этого графа перемещается вглубь вызовов классов? ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 12:47 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Shocker.ProНу, собственно, я и пытаюсь найти ту границу разумного использования DI. Мне как раз неясно, как быть, если класс, создаваемый через new хочет зависимость, которая в других случаях настраивается через контейнер? Вручную транслировать все (или сам контейнер) через конструктор? Зависимости надо снижать. Например DTO классы не должны быть зависимыми. Если есть зависимость и архитектура строится на DI, то надо использовать DI. Тут тяжело сказать, где граница применения DI, её итак видно неплохо. Там где можно избежать использовать DI, над избегать. Тут уже придёт с опытом, теоретически это плохо формализуется. Shocker.Proиспользую Autofac. То есть, фактически, это создание нового графа? То есть точка резолва корня этого графа перемещается вглубь вызовов классов? Да. Но таких моментов должно быть по-меньше. На границе сервисов с разным способом управления времени жизни. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 12:56 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Shocker.ProgandjustasНе надо идею IoC доводить до абсурда, когда все операторы new замняются на инъекцию. До добра такое не доводит.Ну, собственно, я и пытаюсь найти ту границу разумного использования DI. Мне как раз неясно, как быть, если класс, создаваемый через new хочет зависимость, которая в других случаях настраивается через контейнер? Вручную транслировать все (или сам контейнер) через конструктор? Если я правильно понял, то ты пытаешься создать экземпляр класса A, который требует в конструкторе экземпляры классов B и C, а также некоторое значение x, которое известно только в рантайме после вычислений. Тогда, скорее всего, надо переписать класс A таким образом, чтобы значение x передавать не в конструктор, а в метод класса A. Сам класс A инжектить в вызывающий класс. Вообе если ты толком не представляешь архитектуру, то лучше начинать не с контейнера. Начать лучше в обычных классов, которые сами создают нужные экземпляры. Потом можно добавить контейнер и постепенно переделать классы так, чтобы вместо создания экземпляров получать их в конструкторе\свойствах. Обычно это получается как раз идеальный случай IoC. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 13:40 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
gandjustasЕсли я правильно понял, то ты пытаешься создать экземпляр класса A, который требует в конструкторе экземпляры классов B и C, а также некоторое значение x, которое известно только в рантайме после вычислений.да gandjustasТогда, скорее всего, надо переписать класс A таким образом, чтобы значение x передавать не в конструктор, а в метод класса A. Сам класс A инжектить в вызывающий класс.это я подразумевал под инициализатором (отдельно от конструктора). К сожалению, это подразумевает во-первых дополнительный инициализатор, во-вторых защиту от использования неинициализированного класса во всех его методах и свойствах, а это начинает резко снижать его читабельность или простоту модификации (в т.ч. при добавлениях/модификациях методов приходится помнить о наличии неинициализированного состояния). Поэтому пытаюсь понять, может есть иной best practice.. gandjustasВообе если ты толком не представляешь архитектуру, то лучше начинать не с контейнера. Начать лучше в обычных классов, которые сами создают нужные экземпляры. Потом можно добавить контейнер и постепенно переделать классы так, чтобы вместо создания экземпляров получать их в конструкторе\свойствах.как раз у меня уже имеется сильно связанный код и я пытаюсь его отрефакторить с использованием DI (началось с необходимости увязать две библиотеки, где DI подошел идеально) hVosttТам где можно избежать использовать DI, над избегать.ну в вырожденном случае можно вообще обойтись без DI Меня, может быть смутило то, что в этой главе Симан очень фанатично использует DI, вплоть до того, что развязывает модель с репозиторием за счет дополнительной абстрации в виде промежуточного сервиса, заточенного практически персонально под модель. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 14:34 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Shocker.ProМеня, может быть смутило то, что в этой главе Симан очень фанатично использует DI, вплоть до того, что развязывает модель с репозиторием за счет дополнительной абстрации в виде промежуточного сервиса, заточенного практически персонально под модель. Когда тебе надо будет покрыть код тестами приблизительно на 100%, ты поймёшь Симана ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 15:02 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
hVostt, значит главно что бы прога хорошо тестировалась? ребятя вы потиноьку съезжаете с ума ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 16:53 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
ну так для этого и было придумано, unit test-ы всякие и тп. безусловно, нечего IoC/DI огород городить для обычных приложений ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 17:03 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
ViPRosзначит главно что бы прога хорошо тестировалась? ребятя вы потиноьку съезжаете с ума человек совершает ошибки. всегда. даже если это Бог Программирования. тем более при методологии CI, это практически обязательное требование. или по-вашему после каждой небольшой правки в коде необходимо загонять толпу QA и проверять абсолютно всё заново? ну-ну. удачи вам. как только сделаете ЗП каждому тестеру по $10k, я сам лично буду готов работать таким авто-тестером. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 17:17 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Konst_Oneну так для этого и было придумано, unit test-ы всякие и тп. безусловно, нечего IoC/DI огород городить для обычных приложений именно. все истерические вопли по поводу IoC/DI связаны с полным отсутствием какого-либо опыта. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 17:18 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
ViPRoshVostt, значит главно что бы прога хорошо тестировалась? ребятя вы потиноьку съезжаете с ума Согласен! 1. Должна хорошо продаваться. 2. Должна удовлетворять потребности клиентов. Остальное - второстепенно. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 17:46 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
До чего доходит :) Код: c# 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11.
... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 17:51 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
ЕвгенийВViPRoshVostt, значит главно что бы прога хорошо тестировалась? ребятя вы потиноьку съезжаете с ума Согласен! 1. Должна хорошо продаваться. 2. Должна удовлетворять потребности клиентов. Остальное - второстепенно.1 - это задачи отдела маркетинга, 2 - это задачи отдела product design (аналитегов). А если посмотреть с точки зрения целей отдела разработки и тестирования? ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 17:53 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Какбы с колокольни бизнеса и с колокольни процесса разработки открываются разные виды. ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 17:55 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
Админы вообще скажут, что главное - это виртуализация ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 17:56 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
skyANA, да разве можно ради тестирования только сделать автомобиль модульной? для этого должны быть более веские причины ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 18:02 |
|
Внедрение зависимостей: практическое применение
|
|||
---|---|---|---|
#18+
ViPRos, а автомобиль итак модульный. Собирается из комплектующих разных производителей. Если производитель не понравился, то выбирается другой. При этом остальные части авто как-то особо не страдают. Уменьшение сроков поставки новой версии - это более веская причина? ... |
|||
:
Нравится:
Не нравится:
|
|||
30.07.2014, 18:13 |
|
|
start [/forum/topic.php?fid=20&msg=38709341&tid=1402659]: |
0ms |
get settings: |
9ms |
get forum list: |
12ms |
check forum access: |
4ms |
check topic access: |
4ms |
track hit: |
30ms |
get topic data: |
12ms |
get forum data: |
3ms |
get page messages: |
56ms |
get tp. blocked users: |
1ms |
others: | 13ms |
total: | 144ms |
0 / 0 |