Как избежать безумия конструктора инъекций зависимостей?


Я считаю, что мои конструкторы начинают выглядеть так:

public MyClass(Container con, SomeClass1 obj1, SomeClass2, obj2.... )

С постоянно увеличивающимся списком параметров. Поскольку "контейнер" - это мой контейнер для инъекций зависимостей, почему я не могу просто сделать это:

public MyClass(Container con)

для каждого класса? Какие есть минусы? Если я это сделаю, Мне кажется, что я использую прославленную статику. Пожалуйста, поделитесь своими мыслями о МОК и зависимости инъекции безумия.

8 251

8 ответов:

вы правы, что если вы используете контейнер в качестве локатора службы, это более или менее прославленная статическая фабрика. По многим причинам Я считаю это анти-паттерн.

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

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

Я не думаю, что ваши конструкторы классов должны иметь ссылку на ваш период контейнера IOC. Это представляет собой ненужную зависимость между вашим классом и контейнером (тип зависимости IOC пытается избежать!).

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

инъекция зависимостей может действовать как раннее предупреждение для классов, которые становятся слишком большими, особенно из-за возрастающей боли при прохождении всех зависимостей.

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

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

подробный код с пояснением можно посмотреть здесь

лучшие практики для МОК в сложном сервисном слое

:

1) конструктор с постоянно растущим списком параметров.

2) Если класс наследуется (например: RepositoryBase), то изменение конструктор сигнатура вызывает изменения в производных классах.

Решение 1

передать IoC Container конструктора

почему

  • нет больше никогда не увеличивая список параметров
  • подпись конструктора становится просто

почему бы и нет

  • делает вас класс плотно соединенный с контейнером МОК. (что вызывает проблемы при 1. вы хотите использовать этот класс в других проектах, где вы используете различные IoC-контейнер. 2. вы решили изменить контейнер МОК)
  • делает вас менее информативным. (вы не можете действительно посмотреть на конструктор класса и сказать, что ему нужно для функционирования.)
  • класс может получить доступ к потенциально весь сервис.

решение 2

создать класс, который группирует все службы и передать его конструктору

 public abstract class EFRepositoryBase 
 {
    public class Dependency
    {
        public DbContext DbContext { get; }
        public IAuditFactory AuditFactory { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
        }
    }

    protected readonly DbContext DbContext;        
    protected readonly IJobariaAuditFactory auditFactory;

    protected EFRepositoryBase(Dependency dependency)
    {
        DbContext = dependency.DbContext;
        auditFactory= dependency.JobariaAuditFactory;
    }
  }

производный класс

  public class ApplicationEfRepository : EFRepositoryBase      
  {
     public new class Dependency : EFRepositoryBase.Dependency
     {
         public IConcreteDependency ConcreteDependency { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory,
            IConcreteDependency concreteDependency)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
            ConcreteDependency = concreteDependency;
        }
     }

      IConcreteDependency _concreteDependency;

      public ApplicationEfRepository(
          Dependency dependency)
          : base(dependency)
      { 
        _concreteDependency = dependency.ConcreteDependency;
      }
   }

почему

  • добавление новой зависимости в класс не влияет на производные классы
  • класс является агностиком контейнера МОК
  • класс является описательным (в аспекте его зависимостей). По соглашению, если вы хотите знать, какой класс A зависит от того, что информация накапливается в A.Dependency
  • подпись конструктора становится простым

почему бы и нет

  • нужно создать дополнительный класс
  • Регистрация сервиса становится сложной (вам нужно зарегистрировать каждый X.Dependency отдельно)
  • концептуально то же самое, что и передача IoC Container
  • ..

решение 2 является просто сырым, хотя, если есть твердый аргумент против него, то описательный комментарий будет оценен

это подход, который я использую

public class Hero
{

    [Inject]
    private IInventory Inventory { get; set; }

    [Inject]
    private IArmour Armour { get; set; }

    [Inject]
    protected IWeapon Weapon { get; set; }

    [Inject]
    private IAction Jump { get; set; }

    [Inject]
    private IInstanceProvider InstanceProvider { get; set; }


}

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

public class InjectAttribute : Attribute
{

}


public class TestClass
{
    [Inject]
    private SomeDependency sd { get; set; }

    public TestClass()
    {
        Console.WriteLine("ctor");
        Console.WriteLine(sd);
    }
}

public class SomeDependency
{

}


class Program
{
    static void Main(string[] args)
    {
        object tc = FormatterServices.GetUninitializedObject(typeof(TestClass));

        // Get all properties with inject tag
        List<PropertyInfo> pi = typeof(TestClass)
            .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
            .Where(info => info.GetCustomAttributes(typeof(InjectAttribute), false).Length > 0).ToList();

        // We now happen to know there's only one dependency so we take a shortcut just for the sake of this example and just set value to it without inspecting it
        pi[0].SetValue(tc, new SomeDependency(), null);


        // Find the right constructor and Invoke it. 
        ConstructorInfo ci = typeof(TestClass).GetConstructors()[0];
        ci.Invoke(tc, null);

    }
}

в настоящее время я работаю над проектом хобби, который работает следующим образом https://github.com/Jokine/ToolProject/tree/Core

Я прочитал всю эту тему дважды, и я думаю, что люди отвечают тем, что они знают, а не тем, что спрашивают.

исходный вопрос JP выглядит так, как будто он строит объекты, отправляя решатель, а затем кучу классов, но мы предполагаем, что эти классы/объекты сами по себе являются службами, созревшими для инъекции. Что, если они не являются?

JP, если вы хотите использовать di и желание славы смешивания инъекции с контекстными данными, нет из этих паттернов (или предполагаемых "анти-паттернов") конкретно это касается. Это на самом деле сводится к использованию пакет, который будет поддерживать вас в таком начинании.

Container.GetSevice<MyClass>(someObject1, someObject2)

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

но это должно быть сделано, потому что я должен быть в состоянии создавать и зарегистрировать фабрику для MyClass-х, и что завод должен иметь возможность получать/вводить данные, которые не толкнул в "сервис" только ради передачи данных. Если "анти-паттерн" - это негативные последствия, то принуждение к существованию искусственных типов услуг для передачи данных/моделей, безусловно, отрицательно (наравне с вашим чувством об упаковке ваших классов в контейнер. Тот же инстинкт применяется).

есть рамки, которые могут помочь, хотя, даже если они выглядят немного некрасиво. Например, Ninject:

создание экземпляра с помощью Ninject с дополнительными параметрами в конструкторе

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

какую структуру внедрения зависимостей вы используете? Вы пробовали использовать инъекцию на основе сеттера вместо этого?

преимущество инъекции на основе конструктора заключается в том, что она выглядит естественной для Java-программистов, которые не используют di-фреймворки. Вам нужно 5 вещей, чтобы инициализировать класс, то у вас есть 5 аргументов для конструктора. Недостатком является то, что вы заметили, это становится громоздким, когда у вас есть много зависимостей.

с весной вы можете передать необходимые значения вместо этого с помощью сеттеров вы можете использовать аннотации @required для обеспечения их ввода. Недостатком является то, что вам нужно переместить код инициализации из конструктора в другой метод и иметь вызов Spring, который после всех зависимостей вводится, помечая его с помощью @PostConstruct. Я не уверен в других рамках, но я предполагаю, что они делают что-то подобное.

оба способа работают, это вопрос предпочтения.