Подход внедрения зависимостей для объекта, используемого только в одном методе

Я переношу старое приложение ASP.NET 4.7.2 MVC на ASP.NET Core и хочу узнать больше о подходе DI, поскольку он уже глубоко интегрирован в платформу Core. Однако до сих пор я столкнулся с некоторыми проблемами при его использовании. Я понимаю, что DI должен использоваться по всей цепочке вызовов, причем в каждый класс внедряются свои зависимости. Но что, если вы хотите использовать экземпляр класса только в определенном методе вашего текущего? Вы должны внедрить его для всего класса, тратя производительность на потенциально тяжелые экземпляры, когда в этом нет необходимости?

Вот пример;

public class TestClass(ILogger<TestClass> logger)
{
    public void DoSomething()
    {
        // Do work here
    }

    public void DoAnotherThing()
    {
        // Do work here
    }

    private void CallSomethingFromInnerClass()
    {
        // Problematic code
        new InnerClass().DoInnerClassWork();
    }
}

public class InnerClass(ILogger<InnerClass> logger)
{
    public void DoInnerClassWork()
    {
        // Do work here
    }
}

У меня есть класс, который имеет множество методов, каждый из которых делает свое дело. Предположим, что класс нельзя разделить на разные более мелкие классы, возможно, это контроллер. Внутренний класс ожидает регистратор со своей собственной категорией. Теперь я мог бы создать его экземпляр в методе и передать существующий регистратор, но у этого регистратора была бы неправильная категория (TestClass вместо InnerClass). Я также мог бы добавить IServiceProvider, однако, насколько я понимаю, это антишаблон, и его не следует делать при реализации DI. Я мог бы также ввести атрибут [FromService], однако это тоже антишаблон, плюс он работает только для действий контроллера, вызываемых из среды, а не для частных методов.

Другой подход — действительно разделить метод на другой объект (см. это). Однако разве это не просто пинает банку в сторону? Если вам понадобится затем внедрить этот объект для вызова метода, не произойдет ли внедрение для всего TestClass?

Какой здесь лучший подход? Действительно ли ответ вводит его в курс дела всего класса? Что, если внутренний класс является помощником ORM, нужно ли мне каждый раз создавать соединение с БД?

При переходе на ASP.NET Core внедрение внедрения зависимостей (DI) для объектов, используемых в конкретных методах, позволяет сбалансировать эффективность и удобство обслуживания. Внедрение зависимостей для всего класса соответствует принципам SOLID, что упрощает тестирование и обеспечивает согласованность кода. Такие методы, как отложенная инициализация или фабричные методы, оптимизируют производительность, откладывая создание объектов до тех пор, пока они не понадобятся. Разделение задач на более мелкие классы, когда методы требуют отдельных зависимостей, повышает ясность кода.

Md Farid Uddin Kiron 05.07.2024 07:59

Я бы сказал, что это в основном предпочтение. Некоторые люди используют MediatR для контроллеров, чтобы сохранить небольшую длину параметров конструктора. Некоторые люди разделяют контроллеры более детально (минимальный apis, fastpoint). Я бы сказал, что помощник ORM должен создавать соединение не в конструкторе, а в каждом методе или при первом использовании метода.

Yehor Androsov 05.07.2024 08:01

Что касается передовой практики, обычно она предполагает внедрение зависимостей для всего класса, даже если зависимость используется в одном методе. Это способствует согласованности, тестируемости и соблюдению принципов SOLID.

Md Farid Uddin Kiron 05.07.2024 08:01

В таких ситуациях используется ленивая инициализация, а также ее можно применять и к внедрению зависимостей. См. => ссылку

YBB 05.07.2024 08:04

@MdFaridUddinKiron Я согласен, что это будет наиболее последовательный подход, однако это не всегда вариант. Я привел упрощенный пример, в котором контроллер с бизнес-логикой смешивается гораздо ниже в реальном приложении. InnerClass просто невозможно каждый раз создавать экземпляр.

Emberfire 05.07.2024 08:16

@YBB Это кажется фантастическим решением проблемы. Мне, конечно, придется его протестировать, а также прочитать о его потенциальных недостатках и о том, считается ли он анти-шаблоном, но похоже, что это IEnumerable<T> для коллекций и LINQ. Пожалуйста, опубликуйте это как ответ на вопрос, чтобы я мог пометить его как решение. Кроме того, @MdFaridUddinKiron, что ты об этом думаешь?

Emberfire 05.07.2024 08:18

Честно говоря, я не следую ничему раньше, пока не получу конкретное требование. Обычно вам не нужно каждый раз создавать новое соединение с БД из-за пула соединений. Вместо этого внедрите такую ​​службу, как DbContext (для Entity Framework Core), во все приложения, где это необходимо. Таким образом, вы можете использовать отложенную инициализацию для создания экземпляра только при первом обращении к нему. Таким образом, это вызов программиста в зависимости от сценария, и я в основном сосредоточился на проблеме, ориентированной на реализацию в stackoverflow, а не на проблемах, ориентированных на мнения, поскольку они постоянно меняются.

Md Farid Uddin Kiron 05.07.2024 08:28

Трудно объяснить все это в комментариях, поэтому мне нужно подготовить ответ, чтобы объяснить это.

Md Farid Uddin Kiron 05.07.2024 08:49
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
8
78
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

В таких ситуациях используется ленивая инициализация, и ее также можно применить к DI, см. => Ленивая инициализация

Обновление: предоставленный источник содержит, по-видимому, устаревшую информацию. Для справки я использовал builder.Services.AddScoped<InnerClass>();, а затем builder.Services.AddScoped<Lazy<InnerClass>>(provider => new Lazy<InnerClass>(provider.GetRequiredService<InnerClass>)), не вызывая builder.Services.AddAppDependencies();, потому что он больше не является частью какой-либо сборки, которую я нашел. Дальше все как в статье написано.

Emberfire 05.07.2024 08:54

Что делать, если вы хотите использовать экземпляр класса только в определенном метод вашего текущего? Вы должны вводить его для весь класс, тратя производительность на потенциально тяжелые экземпляры когда это не нужно?

Согласно вашему сценарию, если вы хотите использовать экземпляр класса только в определенном методе, вам не обязательно внедрять его для всего класса.

Хотя внедрение зависимостей для всего класса является наиболее простым и удобным в сопровождении подходом, использование фабричных методов, ленивая инициализация или внедрение на уровне метода могут обеспечить повышение производительности без нарушения принципа DI.

На самом деле у обоих есть плюсы и минусы. Итак, как я уже говорил вам ранее, это призвание или предпочтение программиста.

Если вам нужно будет затем внедрить этот объект для вызова метода, разве инъекция по-прежнему не происходит для всего TestClass?

При внедрении зависимости в класс в ASP.NET Core внедрение происходит на уровне класса, то есть зависимость доступна для всех методов внутри этого класса. Однако это не обязательно означает, что зависимость создается немедленно или что она оказывает сильное влияние на производительность.

Какой здесь лучший подход? Является ли ответ действительно инъекционным? весь класс? Что, если внутренний класс является помощником ORM, нужно ли мне каждый раз создавать соединение с БД?

Поскольку у него есть несколько подходов, вы можете решить, в зависимости от того, какой из них лучше всего подходит для вашего сценария.

Если вы предпочитаете отложенную инициализацию, она откладывает создание экземпляра до первого доступа к нему. В этом случае вы можете сделать что-то вроде ниже:

public class TestClass
{
    private readonly ILogger<TestClass> _logger;
    private readonly Lazy<InnerClass> _innerClass;

    public TestClass(ILogger<TestClass> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _innerClass = new Lazy<InnerClass>(() =>
            new InnerClass(serviceProvider.GetRequiredService<ILogger<InnerClass>>()));
    }

    private void CallSomethingFromInnerClass()
    {
        _innerClass.Value.DoInnerClassWork();
    }
}

Кроме того, для помощников ORM, таких как DbContext, Entity Framework Core эффективно управляет соединениями посредством пула соединений. Внедрение DbContext в ваши службы обычно эффективно, поскольку оно не создает каждый раз новое соединение с базой данных, а повторно использует существующие соединения из пула.

Да, одно очевидно: ленивая инициализация или ограниченные/переходные сервисы могут обеспечить баланс между эффективностью и соблюдением принципов внедрения зависимостей.

Примечание. Если у вас есть время, я бы рекомендовал вам проверить этот официальный документ.

Ваш ответ подробный и действительно решает проблему. Я принимаю ответ @YBB, так как он сначала упомянул ленивую инъекцию, но после того, как немного поигрался с решением, это кажется хорошим решением, единственным изменением является настройка поставщика экземпляра в методе Main. Спасибо за ваши усилия и время :)

Emberfire 05.07.2024 08:57

P.S. У меня сложилось впечатление, что внедрение на уровне метода с использованием IServiceProvider или [FromService] в действиях контроллера считается антишаблоном и недопустимо. Согласны ли вы, или как вы их видите?

Emberfire 05.07.2024 09:00

Все в порядке, я просто пытался помочь тебе, веселись, чувак.

Md Farid Uddin Kiron 05.07.2024 09:01

Использование IServiceProvider или [FromServices] может показаться удобным, но, опять же, это предпочтение человека.

Md Farid Uddin Kiron 05.07.2024 09:05

Но что, если вы хотите использовать экземпляр класса только в определенном методе вашего текущего? Вы должны внедрить его для всего класса, тратя производительность на потенциально тяжелые экземпляры, когда в этом нет необходимости?

Конструкторы внедрения должны быть простыми, а любую тяжелую инициализацию следует отложить до создания графа объекта. Это делает построение графов больших объектов очень легким и быстрым, и в этом случае вам обычно не придется беспокоиться о потере производительности.

Откладывание инициализации означает, например, подключение к базе данных не внутри конструктора, а при первом использовании класса. Например, такие O/RM, как Entity Framework, используют этот подход. Только при вызове любого из свойств DbSet будут установлены соединения с базой данных. Введение неиспользованного DbContext довольно легко.

Тем не менее, графы объектов, состоящие из тысяч экземпляров, по-прежнему могут вызывать проблемы с производительностью. Однако мой опыт показывает, что очень большие графы объектов существуют только в тех графах объектов, к классам которых применяется запах кода Over-Injection конструктора. Другими словами, занятия, нарушающие Принцип единой ответственности.

Помимо уменьшения количества зависимостей класса, обычно более быстрым решением (или обходным путем) является увеличение срока существования зависимостей. Например, от Transient к Scoped. Это кэширует компонент со всеми его созданными зависимостями и может значительно сократить количество создаваемых объектов в графе объектов.

У меня есть класс, который имеет множество методов, каждый из которых делает свое дело.

Если методы имеют низкую связность, это указывает на нарушение принципа единой ответственности.

Предположим, что класс нельзя разделить на разные более мелкие классы, возможно, это контроллер.

Даже контроллеры могут быть разделены, и, возможно, так и должно быть, когда число аргументов их конструктора растет. Однако следует позаботиться о том, чтобы пути URL-адресов API приложения оставались неизменными, чтобы предотвратить поломку клиентов.

Другие вопросы по теме