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

Обратите внимание, что атрибут [Dependency] не работает для приведенного ниже примера и приводит к StackoverflowException, но внедрение конструктора работает.

ПРИМЕЧАНИЕ (2) В некоторых комментариях ниже начали присваиваться «метки», такие как запах кода, плохой дизайн и т. Д. Итак, во избежание путаницы здесь используется бизнес-установка без какого-либо дизайна в первую очередь.

Этот вопрос, кажется, вызывает серьезное противоречие даже среди некоторых из самых известных гуру C #. На самом деле, этот вопрос выходит далеко за рамки C # и больше относится к чисто компьютерным наукам. Вопрос основан на хорошо известной «битве» между шаблоном поиска служб и шаблоном внедрения чистой зависимости: https : //martinfowler.com/articles/injection.html vs http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/ и последующее обновление для исправления ситуации, когда внедрение зависимостей становится слишком сложным: http://blog.ploeh.dk/2010/02/02/RefactoringtoAggregateServices/

Вот ситуация, которая не вписывается в то, что описано в последних двух, но, кажется, идеально вписывается в первую.

У меня есть большая (более 50) коллекция того, что я назвал микроуслугами. Если у вас есть лучшее имя, пожалуйста, «примените» его при чтении. Каждый из них оперирует одним объектом, назовем его кавычкой. Тем не менее, кортеж (контекст + цитата) кажется более подходящим. Цитата - это бизнес-объект, который обрабатывается и сериализуется в базу данных, а контекст представляет собой некоторую вспомогательную информацию, которая необходима во время обработки цитаты, но не сохраняется в базе данных. Часть этой вспомогательной информации может на самом деле поступать из базы данных или из сторонних сервисов. Это не имеет значения. Сборочная линия приходит на ум как пример из реальной жизни: сборщик (микросервис) получает какой-то ввод (инструкция (контекст) + детали (цитата)), обрабатывает его (что-то делает с деталями в соответствии с инструкцией и / или модифицирует инструкцию) и передает его дальше, если успешно ИЛИ сбрасывает его (вызывает исключение) в случае проблем. Микро-сервисы в конечном итоге объединяются в небольшое количество (около 5) высокоуровневых сервисов. Этот подход линеаризует обработку некоторого очень сложного бизнес-объекта и позволяет тестировать каждый микросервис отдельно от всех остальных: просто задайте ему состояние ввода и проверьте, что он выдает ожидаемый результат.

Вот где это становится интересным. Из-за количества этапов высокоуровневые сервисы начинают зависеть от множества микроуслуг: от 10 и более. Эта зависимость естественна и отражает сложность базового бизнес-объекта. Вдобавок к этому микро сервисы могут добавляться / удаляться практически на постоянной основе: в основном, это некоторые бизнес-правила, которые почти так же гибки, как вода.

Это сильно противоречит приведенной выше рекомендации Марка: если у меня есть 10+ эффективно независимых правил, примененных к цитате в каком-либо высокоуровневом сервисе, то, согласно третьему блогу, я должен объединить их в несколько логических групп, скажем, не более, чем 3-4 вместо введения всех 10+ через конструктор. Но нет логических групп! Хотя некоторые из правил слабо зависят, большинство из них не зависят, и поэтому искусственное их объединение принесет больше вреда, чем пользы.

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

И я даже не упомянул, что правила зависят от штата США, и, таким образом, теоретически существует около 50 наборов правил, по одной коллекции на каждый штат и на каждый рабочий процесс. И хотя некоторые из правил являются общими для всех состояний (например, «сохранить цитату в базе данных»), существует множество правил, специфичных для каждого штата.

Вот очень упрощенный пример.

Цитата - бизнес-объект, который сохраняется в базе данных.

public class Quote
{
    public string SomeQuoteData { get; set; }
    // ...
}

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

public interface IService_1
{
    Quote DoSomething_1(Quote quote);
}
// ...

public interface IService_N
{
    Quote DoSomething_N(Quote quote);
}

Все микро сервисы наследуются от этого интерфейса.

public interface IQuoteProcessor
{
    List<Func<Quote, Quote>> QuotePipeline { get; }
    Quote ProcessQuote(Quote quote = null);
}

// Low level quote processor. It does all workflow related work.
public abstract class QuoteProcessor : IQuoteProcessor
{
    public abstract List<Func<Quote, Quote>> QuotePipeline { get; }

    public Quote ProcessQuote(Quote quote = null)
    {
        // Perform Aggregate over QuotePipeline.
        // That applies each step from workflow to a quote.
        return quote;
    }
}

Один из сервисов «рабочего процесса» высокого уровня.

public interface IQuoteCreateService
{
    Quote CreateQuote(Quote quote = null);
}

И его фактическая реализация, где мы используем многие микро-сервисы низкого уровня.

public class QuoteCreateService : QuoteProcessor, IQuoteCreateService
{
    protected IService_1 Service_1;
    // ...
    protected IService_N Service_N;

    public override List<Func<Quote, Quote>> QuotePipeline =>
        new List<Func<Quote, Quote>>
        {
            Service_1.DoSomething_1,
            // ...
            Service_N.DoSomething_N
        };

    public Quote CreateQuote(Quote quote = null) => 
        ProcessQuote(quote);
}

Есть два основных способа достижения DI:

Стандартный подход - внедрить все зависимости через конструктор:

    public QuoteCreateService(
        IService_1 service_1,
        // ...
        IService_N service_N
        )
    {
        Service_1 = service_1;
        // ...
        Service_N = service_N;
    }

А затем зарегистрируйте все типы в Unity:

public static class UnityHelper
{
    public static void RegisterTypes(this IUnityContainer container)
    {
        container.RegisterType<IService_1, Service_1>(
            new ContainerControlledLifetimeManager());
        // ...
        container.RegisterType<IService_N, Service_N>(
            new ContainerControlledLifetimeManager());

        container.RegisterType<IQuoteCreateService, QuoteCreateService>(
            new ContainerControlledLifetimeManager());
    }
}

Тогда Unity сделает свое «волшебство» и разрешит все сервисы во время выполнения. Проблема в том, что в настоящее время у нас есть около 30 таких микро-сервисов, и ожидается, что их число будет расти. Впоследствии некоторые из конструкторов уже получают более 10 сервисов. Это неудобно поддерживать, издеваться и т. Д.

Конечно, эту идею можно использовать здесь: http: //blog.ploeh .dk / 2010/02/02 / RefactoringtoAggregateServices / Однако микросервисы на самом деле не связаны друг с другом, и поэтому их объединение - это искусственный процесс без какого-либо оправдания. Кроме того, это также устранит цель сделать весь рабочий процесс линейным и независимым (микросервис принимает текущее «состояние», затем выполняет какое-то действие с кавычками, а затем просто движется). Никто из них не заботится о каких-либо других микро-услугах до или после них.

Альтернативная идея, по-видимому, заключается в создании единого «хранилища сервисов»:

public interface IServiceRepository
{
    IService_1 Service_1 { get; set; }
    // ...
    IService_N Service_N { get; set; }

    IQuoteCreateService QuoteCreateService { get; set; }
}

public class ServiceRepository : IServiceRepository
{
    protected IUnityContainer Container { get; }

    public ServiceRepository(IUnityContainer container)
    {
        Container = container;
    }

    private IService_1 _service_1;

    public IService_1 Service_1
    {
        get => _service_1 ?? (_service_1 = Container.Resolve<IService_1>());
        set => _service_1 = value;
    }
    // ...
}

Затем зарегистрируйте его в Unity и измените конструктор всех соответствующих сервисов на что-то вроде этого:

    public QuoteCreateService(IServiceRepository repo)
    {
        Service_1 = repo.Service_1;
        // ...
        Service_N = repo.Service_N;
    }

Преимущества этого подхода (по сравнению с предыдущим) следующие:

Все микро-сервисы и сервисы более высокого уровня могут быть созданы в единой форме: новые микро-сервисы могут быть легко добавлены / удалены без необходимости исправления вызова конструктора для сервисов и всех модульных тестов. Впоследствии, обслуживание и сложность уменьшается.

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

Проблема этого подхода заключается в том, что он начинает выглядеть как локатор службы, который некоторые люди считают анти-паттерном: http://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/, а затем люди начинают утверждать, что все зависимости должны быть явными, а не скрыто как в ServiceRepository.

Что мне с этим делать?

4
Konstantin Konstantinov 21 Авг 2018 в 03:47

3 ответа

Лучший ответ

Я бы просто создал один интерфейс:

public interface IDoSomethingAble
{
    Quote DoSomething(Quote quote);
}

И совокупность:

public interface IDoSomethingAggregate : IDoSomethingAble {}

public class DoSomethingAggregate : IDoSomethingAggregate 
{
    private IEnumerable<IDoSomethingAble> somethingAbles;

    public class DoSomethingAggregate(IEnumerable<IDoSomethingAble> somethingAbles)
    {
        _somethingAbles = somethingAbles;
    }

    public Quote DoSomething(Quote quote)
    {
        foreach(var somethingAble in _somethingAbles)
        {
            somethingAble.DoSomething(quote);
        }
        return quote;
    }
}

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

Я бы пошел на завод

public class DoSomethingAggregateFactory
{
    public IDoSomethingAggregate Create()
    {
        return new DoSomethingAggregate(GetItems());
    }

    private IEnumerable<IDoSomethingAble> GetItems()
    {
        yield return new Service1();
        yield return new Service2();
        yield return new Service3();
        yield return new Service4();
        yield return new Service5();
    }
}

Все остальное (которое не вводится конструктором) скрывает явные зависимости.


В качестве последнего средства вы также можете создать некоторый объект DTO, внедрить все необходимые службы через конструктор (но только один раз).

Таким образом, вы можете запросить ProcessorServiceScope и получить доступ ко всем сервисам без необходимости создавать логику ctor для каждого класса:

public class ProcessorServiceScope
{
    public Service1 Service1 {get;};
    public ServiceN ServiceN {get;};

    public ProcessorServiceScope(Service1 service1, ServiceN serviceN)
    {
        Service1 = service1;
        ServiceN = serviceN;
    }
}

public class Processor1
{
    public Processor1(ProcessorServiceScope serviceScope)
    {
        //...
    }
}

public class ProcessorN
{
    public ProcessorN(ProcessorServiceScope serviceScope)
    {
        //...
    }
}

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

5
Christian Gollhardt 22 Авг 2018 в 20:00

Рассмотрим различные методы интерфейса, перечисленные:

Quote DoSomething_1(Quote quote);
Quote DoSomething_N(Quote quote);
Quote ProcessQuote(Quote quote = null)
Quote CreateQuote(Quote quote = null);

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

Итак, вместо этого, введите одну абстракцию:

public interface IQuoteProcessor
{
    Quote ProcessQuote(Quote quote);
}

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

public class CompositeQuoteProcessor : IQuoteProcessor
{
    private readonly IReadOnlyCollection<IQuoteProcessor> processors;

    public CompositeQuoteProcessor(params IQuoteProcessor[] processors)
    {
        this.processors = processors ?? throw new ArgumentNullException(nameof(processors));
    }

    public Quote ProcessQuote(Quote quote)
    {
        var q = quote;
        foreach (var p in processors)
            q = p.ProcessQuote(q);
        return q;
    }
}

На данный момент, по сути, вы сделали, я должен подумать. Теперь вы можете создавать различные сервисы (те, которые называются microservices в OP). Вот простой пример:

var processor = new CompositeQuoteProcessor(new Service1(), new Service2());

Такая композиция должна быть в корне композиции приложения.

Различные сервисы могут иметь собственные зависимости:

var processor =
    new CompositeQuoteProcessor(
        new Service3(
            new Foo()),
        new Service4());

Вы можете даже вкладывать Композиты, если это полезно:

var processor =
    new CompositeQuoteProcessor(
        new CompositeQuoteProcessor(
            new Service1(),
            new Service2()),
        new CompositeQuoteProcessor(
            new Service3(
                new Foo()),
            new Service4()));

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

В этом ответе я полностью игнорирую Unity. Внедрение зависимостей - это вопрос разработки программного обеспечения. Если DI-контейнер не может легко составить хороший дизайн, вам лучше воспользоваться Чистый DI, который я здесь имел в виду.


Если вы должны использовать Unity, вы всегда можете создать конкретные классы, которые являются производными от CompositeQuoteProcessor и принимают Конкретные зависимости:

public class SomeQuoteProcessor1 : CompositeQuoteProcessor
{
    public SomeQuoteProcessor1(Service1 service1, Service3 service3) :
        base(service1, service3)
    {
    }
}

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

2
Mark Seemann 22 Авг 2018 в 18:29

Unity поддерживает внедрение свойств. Вместо того, чтобы передавать все эти значения в конструктор, просто используйте общедоступные сеттеры с атрибутом [Dependency]. Это позволяет вам добавлять столько инъекций, сколько вам нужно, без необходимости обновлять конструктор.

public class QuoteCreateService : QuoteProcessor, IQuoteCreateService
{
    [Dependency]
    protected IService_1 Service_1 { get; public set; }
    // ...
    [Dependency]
    protected IService_N Service_N; { get; public set; }

    public override QuoteUpdaterList QuotePipeline =>
        new QuoteUpdaterList
        {
            Service_1.DoSomething_1,
            // ...
            Service_N.DoSomething_N
        };

    public Quote CreateQuote(Quote quote = null) => 
        ProcessQuote(quote);
}
-1
Scott Chamberlain 21 Авг 2018 в 01:58
51940180