Механизм внедрения зависимостей для обеспечения наиболее конкретной реализации универсального интерфейса службы - PullRequest
0 голосов
/ 02 февраля 2019

Я чувствую, что я играл в модное лото с названием.Вот краткий пример того, что я спрашиваю.Допустим, у меня есть некоторая иерархия наследования для некоторых сущностей.

class BaseEntity { ... }
class ChildAEntity : BaseEntity { ... }
class GrandChildAEntity : ChildAEntity { ... }
class ChildBEntity : BaseEntity { ... }

Теперь, допустим, у меня есть общий интерфейс для службы с методом, который использует базовый класс:

interface IEntityService<T> where T : BaseEntity { void DoSomething(BaseEntity entity)... }

Iесть некоторые конкретные реализации:

class BaseEntityService : IEntityService<BaseEntity> { ... }
class GrandChildAEntityService : IEntityService<GrandChildAEntity> { ... }
class ChildBEntityService : IEntityService<ChildBEntity> { ... }

Предположим, я зарегистрировал их все в контейнере.Итак, теперь мой вопрос: если я перебираю List из BaseEntity как мне получить зарегистрированный сервис с самым близким соответствием?

var entities = List<BaseEntity>();
// ...
foreach(var entity in entities)
{
    // Get the most specific service?
    var service = GetService(entity.GetType()); // Maybe?
    service.DoSomething(entity);
}

Что я хотел бы сделать, это иметьМеханизм настроен так, что если у сущности есть тип ClassA, метод не найдет службы для определенного класса и вернет BaseEntityService.Позже, если кто-то придет и добавит регистрацию для этой службы:

class ClassAEntityService : IEntityService<ChildAEntity> { ... }

Гипотетический метод GetService начнет предоставлять ClassAEntityService для типов ClassA, не требуя каких-либо дальнейших изменений кода.И наоборот, если кто-то пришел и просто удалил все службы, кроме BaseEntityService, то метод GetService вернул бы это для всех классов, унаследованных от BaseEntity.

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

РЕДАКТИРОВАТЬ:

Некоторое обсуждение с @Funk (см. Ниже) и некоторые дополнительные поиски в Google, эти обсуждения заставили меня подумать, что поиск сделаля добавлю еще несколько модных слов к этому.Похоже, я пытаюсь собрать все преимущества DI-контейнеров, шаблона стратегии и шаблона декоратора безопасным способом и без использования шаблона поиска сервисов.Я начинаю удивляться, если ответ «Использовать функциональный язык».

Ответы [ 3 ]

0 голосов
/ 01 марта 2019

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

interface IEntityService<T> where T : BaseEntity { void DoSomething(BaseEntity entity)... }

вместо

interface IEntityService<T> where T : BaseEntity { void DoSomething(T entity)... }

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

В хорошо спроектированной иерархии DoSomething(BaseEntity entity) не должен изменять свои функциональные возможности в зависимости от фактического (производного) типа.

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

Если функциональность действительно , то зависит от подтипа,возможно, интерфейс DoSomething() относится к самим типам.

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

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

Обратите внимание Сервисный локатор считается антишаблоном.Единственная цель DI-контейнера должна состоять в том, чтобы создавать граф объектов при запуске (в корне композиции).

Что касается хорошего чтения, если вы любите готовить, в .NET есть Внедрение зависимостей.(Manning pub, 2nd ed выходит).


ОБНОВЛЕНИЕ

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

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

В приведенном ниже примере используется инжектор конструктора.

public interface ISleep { void Sleep(); }

class Nocturnal : ISleep { public void Sleep() => Console.WriteLine("NightOwl"); }
class Hibernate : ISleep { public void Sleep() => Console.WriteLine("GrizzlyBear"); }

public abstract class Animal
{
    private readonly ISleep _sleepPattern;

    public Animal(ISleep sleepPattern)
    {
        _sleepPattern = sleepPattern ?? throw new NullReferenceException("Can't sleep");
    }

    public void Sleep() => _sleepPattern.Sleep();
}

public class Lion : Animal
{
    public Lion(ISleep sleepPattern)
        : base(sleepPattern) { }
}

public class Cat : Lion
{
    public Cat(ISleep sleepPattern)
        : base(sleepPattern) { }
}

public class Bear : Animal
{
    public Bear(ISleep sleepPattern)
        : base(sleepPattern) { }
}

public class Program
{
    public static void Main()
    {
        var nocturnal = new Nocturnal();
        var hibernate = new Hibernate();

        var animals = new List<Animal>
        {
            new Lion(nocturnal),
            new Cat(nocturnal),
            new Bear(hibernate)
        };

        var Garfield = new Cat(hibernate);
        animals.Add(Garfield);

        animals.ForEach(a => a.Sleep());
    }
}

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


ОБНОВЛЕНИЕ 2

В вашем примере сна, как бы new Bear(hibernate) и new Lion(nocturnal) были выполнены с использованием DI-контейнера?

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

Вместо автоматической разводки

container.Register( 
    typeof(IZoo), 
    typeof(Zoo));

Мы можем сделать это вручную

container.Register( 
    typeof(Bear), 
    () => new Bear(hibernate));

Обратите внимание, что двусмысленность заключается в том, что в игре присутствует несколько ISleep sleepPattern s, поэтому нам нужно указать тот или иной путь.

Как мне предоставить IHunt в Bear.Hunt и CatОхота, но не Лев. Охота?

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

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

0 голосов
/ 04 марта 2019

с простым инжектором

Если вы используете Простой инжектор для работы в режиме DI, контейнер может помочь с этим. (Если вы не используете Simple Injector, см. «С другими структурами DI» ниже)

Функциональность описана в документации по Simple Injector, в разделе Расширенные сценарии: микшированиенаборы открытых универсальных и неуниверсальных компонентов .

Вам нужно будет немного изменить интерфейс и реализации вашего сервиса.

interface IEntityService<T>
{
    void DoSomething(T entity);
}

class BaseEntityService<T> : IEntityService<T> where T : BaseEntity
{
    public void DoSomething(T entity) => throw new NotImplementedException();
}

class ChildBEntityService<T> : IEntityService<T> where T : ChildBEntity
{
    public void DoSomething(T entity) => throw new NotImplementedException();
}

Сервисы теперь являются универсальнымис ограничением типа, описывающим наименее специфичный тип сущности , который они могут обрабатывать.В качестве бонуса DoSomething теперь придерживается принципа подстановки Лискова.Поскольку реализации служб предоставляют ограничения типов, интерфейсу IEntityService он больше не нужен.

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

Вот рабочий пример, представленный в виде теста xUnit .

[Theory]
[InlineData(typeof(GrandChildAEntity), new[] { typeof(GrandChildAEntityService<GrandChildAEntity>), typeof(BaseEntityService<GrandChildAEntity>) })]
[InlineData(typeof(BaseEntity), new[] { typeof(BaseEntityService<BaseEntity>) })]
[InlineData(typeof(ChildBEntity), new[] { typeof(ChildBEntityService<ChildBEntity>), typeof(BaseEntityService<ChildBEntity>) })]
[InlineData(typeof(ChildAEntity), new[] { typeof(BaseEntityService<ChildAEntity>) })]
public void Test1(Type entityType, Type[] expectedServiceTypes)
{
    var container = new Container();

    // Services will be resolved in the order they were registered
    container.Collection.Register(typeof(IEntityService<>), new[] {
        typeof(ChildBEntityService<>),
        typeof(GrandChildAEntityService<>),
        typeof(BaseEntityService<>),
    });

    container.Verify();

    var serviceType = typeof(IEntityService<>).MakeGenericType(entityType);

    Assert.Equal(
        expectedServiceTypes,
        container.GetAllInstances(serviceType).Select(s => s.GetType())
    );
}

Как и в вашем примере, вы можете добавить ChildAEntityService<T> : IEntityService<T> where T : ChildAEntity и UnusualEntityService<T> : IEntityService<T> where T : IUnusualEntity, и все получится ...

[Theory]
[InlineData(typeof(GrandChildAEntity), new[] { typeof(UnusualEntityService<GrandChildAEntity>), typeof(ChildAEntityService<GrandChildAEntity>), typeof(GrandChildAEntityService<GrandChildAEntity>), typeof(BaseEntityService<GrandChildAEntity>) })]
[InlineData(typeof(BaseEntity), new[] { typeof(BaseEntityService<BaseEntity>) })]
[InlineData(typeof(ChildBEntity), new[] { typeof(UnusualEntityService<ChildBEntity>), typeof(ChildBEntityService<ChildBEntity>), typeof(BaseEntityService<ChildBEntity>) })]
[InlineData(typeof(ChildAEntity), new[] { typeof(ChildAEntityService<ChildAEntity>), typeof(BaseEntityService<ChildAEntity>) })]
public void Test2(Type entityType, Type[] expectedServiceTypes)
{
    var container = new Container();

    // Services will be resolved in the order they were registered
    container.Collection.Register(typeof(IEntityService<>), new[] {
        typeof(UnusualEntityService<>),
        typeof(ChildAEntityService<>),
        typeof(ChildBEntityService<>),
        typeof(GrandChildAEntityService<>),
        typeof(BaseEntityService<>),
    });

    container.Verify();

    var serviceType = typeof(IEntityService<>).MakeGenericType(entityType);

    Assert.Equal(
        expectedServiceTypes,
        container.GetAllInstances(serviceType).Select(s => s.GetType())
    );
}

Как я упоминал ранее, этот пример относится только к Simple Injector,Не все контейнеры могут так элегантно обрабатывать общие регистрации.Например, аналогичная регистрация завершается неудачно с контейнером DI Microsoft :

[Fact]
public void Test3()
{
    var services = new ServiceCollection()
        .AddTransient(typeof(IEntityService<>), typeof(BaseEntityService<>))
        .AddTransient(typeof(IEntityService<>), typeof(GrandChildAEntityService<>))
        .AddTransient(typeof(IEntityService<>), typeof(ChildBEntityService<>))
        .BuildServiceProvider();

    // Exception message: System.ArgumentException : GenericArguments[0], 'GrandChildBEntity', on 'GrandChildAEntityService`1[T]' violates the constraint of type 'T'.
    Assert.Throws<ArgumentException>(
        () => services.GetServices(typeof(IEntityService<ChildBEntity>))
    );
}

с другими структурами DI

Я разработал альтернативное решение, которое будет работать с любым DIcontainer.

На этот раз мы удаляем определение универсального типа из интерфейса.Вместо этого метод CanHandle() сообщит вызывающей стороне, может ли экземпляр обрабатывать данную сущность.

interface IEntityService
{
    // Indicates whether or not the instance is able to handle the entity.
    bool CanHandle(object entity);
    void DoSomething(object entity);
}

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

abstract class GenericEntityService<T> : IEntityService
{
    // Indicates that the service can handle an entity of typeof(T),
    // or of a type that inherits from typeof(T).
    public bool CanHandle(object entity)
        => entity != null && typeof(T).IsAssignableFrom(entity.GetType());

    public void DoSomething(object entity)
    {
        // This could also throw an ArgumentException, although that
        // would violate the Liskov Substitution Principle
        if (!CanHandle(entity)) return;

        DoSomethingImpl((T)entity);
    }

    // This is the method that will do the actual processing
    protected abstract void DoSomethingImpl(T entity);
}

Это означает, что фактические реализации службы могут быть очень простыми, например:

class BaseEntityService : GenericEntityService<BaseEntity>
{
    protected override void DoSomethingImpl(BaseEntity entity) => throw new NotImplementedException();
}

class ChildBEntityService : GenericEntityService<ChildBEntity>
{
    protected override void DoSomethingImpl(ChildBEntity entity) => throw new NotImplementedException();
}

Чтобы вытащить их из контейнера DI, вам понадобится дружественная фабрика:

class EntityServiceFactory
{
    readonly IServiceProvider serviceProvider;

    public EntityServiceFactory(IServiceProvider serviceProvider)
        => this.serviceProvider = serviceProvider;

    public IEnumerable<IEntityService> GetServices(BaseEntity entity)
        => serviceProvider
            .GetServices<IEntityService>()
            .Where(s => s.CanHandle(entity));
}

И наконец, чтобы доказать, что все это работает:

[Theory]
[InlineData(typeof(GrandChildAEntity), new[] { typeof(UnusualEntityService), typeof(ChildAEntityService), typeof(GrandChildAEntityService), typeof(BaseEntityService) })]
[InlineData(typeof(BaseEntity), new[] { typeof(BaseEntityService) })]
[InlineData(typeof(ChildBEntity), new[] { typeof(UnusualEntityService), typeof(ChildBEntityService), typeof(BaseEntityService) })]
[InlineData(typeof(ChildAEntity), new[] { typeof(ChildAEntityService), typeof(BaseEntityService) })]
public void Test4(Type entityType, Type[] expectedServiceTypes)
{
    // Services appear to be resolved in reverse order of registration, but
    // I'm not sure if this behavior is guaranteed.
    var serviceProvider = new ServiceCollection()
        .AddTransient<IEntityService, UnusualEntityService>()
        .AddTransient<IEntityService, ChildAEntityService>()
        .AddTransient<IEntityService, ChildBEntityService>()
        .AddTransient<IEntityService, GrandChildAEntityService>()
        .AddTransient<IEntityService, BaseEntityService>()
        .AddTransient<EntityServiceFactory>() // this should have an interface, but I omitted it to keep the example concise
        .BuildServiceProvider();

    // Don't get hung up on this line--it's part of the test, not the solution.
    BaseEntity entity = (dynamic)Activator.CreateInstance(entityType);

    var entityServices = serviceProvider
        .GetService<EntityServiceFactory>()
        .GetServices(entity);

    Assert.Equal(
        expectedServiceTypes,
        entityServices.Select(s => s.GetType())
    );
}

Из-за задействованного приведения типов я не думаю, что это так элегантно, как реализация Simple Injector.Это все еще довольно хорошо, и шаблон имеет некоторый прецедент.Это очень похоже на реализацию MVC Core Авторизация на основе политик ;в частности AuthorizationHandler.

0 голосов
/ 05 февраля 2019

Итак, я смог свернуть что-то, что делало то, что мне нужно.

Сначала я сделал интерфейс:

public interface IEntityPolicy<T>
{
    string GetPolicyResult(BaseEntity entity);
}

Затем я сделал несколько реализаций:

public class BaseEntityPolicy : IEntityPolicy<BaseEntity>
{
    public string GetPolicyResult(BaseEntity entity) { return nameof(BaseEntityPolicy); }
}
public class GrandChildAEntityPolicy : IEntityPolicy<GrandChildAEntity>
{
    public string GetPolicyResult(BaseEntity entity) { return nameof(GrandChildAEntityPolicy); }
}
public class ChildBEntityPolicy: IEntityPolicy<ChildBEntity>
{
    public string GetPolicyResult(BaseEntity entity) { return nameof(ChildBEntityPolicy); }
}

Я зарегистрировал каждого из них.

// ...
.AddSingleton<IEntityPolicy<BaseEntity>, BaseEntityPolicy>()
.AddSingleton<IEntityPolicy<GrandChildAEntity>, GrandChildAEntityPolicy>()
.AddSingleton<IEntityPolicy<ChildBEntity>, ChildBEntityPolicy>()
// ...

Помимо регистрации класса поставщика политики, который выглядит примерно так:

public class PolicyProvider : IPolicyProvider
{
    // constructor and container injection...

    public List<T> GetPolicies<T>(Type entityType)
    {
        var results = new List<T>();
        var currentType = entityType;
        var serviceInterfaceGeneric = typeof(T).GetGenericDefinition();

        while(true)
        {
            var currentServiceInterface = serviceInterfaceGeneric.MakeGenericType(currentType);
            var currentService = container.GetService(currentServiceInterface);
            if(currentService != null)
            {
                results.Add(currentService)
            }
            currentType = currentType.BaseType;
            if(currentType == null)
            {
                break;
            }
        }
        return results;
    }
}

Это позволяет мне выполнять следующие действия:

var grandChild = new GrandChildAEntity();
var policyResults = policyProvider
    .GetPolicies<IEntityPolicy<BaseEntity>>(grandChild.GetType())
    .Select(x => x.GetPolicyResult(x));
// policyResults == { "GrandChildAEntityPolicy", "BaseEntityPolicy" }

Что еще важнее, я могу сделать это, не зная конкретного подкласса.

var entities = new List<BaseEntity> { 
    new GrandChildAEntity(),
    new BaseEntity(),
    new ChildBEntity(),
    new ChildAEntity() };
var policyResults = entities
    .Select(entity => policyProvider
        .GetPolicies<IEntityPolicy<BaseEntity>>(entity.GetType())
        .Select(policy => policy.GetPolicyResult(entity)))
    .ToList();
// policyResults = [
//    { "GrandChildAEntityPolicy", "BaseEntityPolicy" },
//    { "BaseEntityPolicy" },
//    { "ChildBEntityPolicy", "BaseEntityPolicy" }, 
//    { "BaseEntityPolicy" }
// ];

Я немного расширил это, чтобы позволить политикам предоставлять порядковое значение при необходимости, и добавил некоторое кэширование.внутри GetPolicies, поэтому не нужно каждый раз создавать коллекцию.Я также добавил некоторую логику, которая позволяет мне определять интерфейсные политики IUnusualEntityPolicy : IEntityPolicy<IUnusualEntity> и подбирать их.(Подсказка: вычтите интерфейсы currentType.BaseType из currentType, чтобы избежать дублирования.)

(Стоит отметить, что порядок List не гарантирован, поэтому я использовал что-то другое в своем собственном решении.Попробуйте использовать то же самое, прежде чем использовать это.)

Все еще не уверен, существует ли это что-то, что уже существует, или есть термин для него, но это заставляет политики управления сущностями чувствовать себя разъединенными в способе, которым можно управлять.Например, если я зарегистрирую ChildAEntityPolicy : IEntityPolicy<ChildAEntity>, мои результаты автоматически станут:

// policyResults = [
//    { "GrandChildAEntityPolicy", "ChildAEntityPolicy", "BaseEntityPolicy" },
//    { "BaseEntityPolicy" },
//    { "ChildBEntityPolicy", "BaseEntityPolicy" }, 
//    { "ChildAEntityPolicy", "BaseEntityPolicy" }
// ];

РЕДАКТИРОВАТЬ: Хотя я еще не пробовал, ответ @ xander ниже, кажется, иллюстрирует, что ПростойИнжектор может обеспечить большую часть поведения PolicyProvider «из коробки».Там все еще небольшое количество Service Locator, но значительно меньше.Я настоятельно рекомендую проверить это, прежде чем использовать мой полуготовый подход.:)

РЕДАКТИРОВАТЬ 2: Мое понимание опасностей вокруг сервис-локатора состоит в том, что он делает ваши зависимости загадкой.Однако эти политики не являются зависимостями, это необязательные дополнения, и код должен запускаться независимо от того, были ли они зарегистрированы.Что касается тестирования, этот дизайн разделяет логику для интерпретации суммарных результатов политик и логику самих политик.

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