Как избежать Service-Locator Anti-Pattern? - PullRequest
12 голосов
/ 26 июля 2011

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

public abstract class MyController : Controller
{
    protected IKernel kernel;
    public MyController(IKernel kernel) { this.kernel = kernel); }

    protected void DoActions(Type[] types)
    {

        MySpecialResolver resolver = new MySpecialResolver(kernel);
        foreach(var type in types)
        {
            IMyServiceInterface instance = resolver.Get(type);
            instance.DoAction();
        }
    }
}

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

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

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

Обновление

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

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

Ответы [ 3 ]

4 голосов
/ 27 июля 2011

Я согласен с @chrisichris и @Mark Seemann.

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

Тогда я бы разрешил решательпопасть в конструктор контроллера.Это позволит вашему контроллеру быть гораздо более тестируемым.

Например:

public interface IMyServiceResolver
{
    List<IMyServiceInterface> Resolve(Type[] types);
}

public class NinjectMyServiceResolver : IMyServiceResolver
{
    private IKernal container = null;

    public NinjectMyServiceResolver(IKernal container)
    {
        this.container = container;
    }

    public List<IMyServiceInterface> Resolve(Type[] types)
    {
        List<IMyServiceInterface> services = new List<IMyServiceInterface>();

        foreach(var type in types)
        {
            IMyServiceInterface instance = container.Get(type);
            services.Add(instance);
        }

        return services;
    }
}

public abstract class MyController : Controller
{
    private IMyServiceResolver resolver = null;

    public MyController(IMyServiceResolver resolver) 
    { 
        this.resolver = resolver;
    }

    protected void DoActions(Type[] types)
    {
        var services = resolver.Resolve(types);

        foreach(var service in services)
        {
            service.DoAction();
        }
    }
}

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

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

public abstract class MyController : Controller
{
    private static IMyServiceResolver resolver = null;

    public static InitializeResolver(IMyServiceResolver resolver)
    {
        MyController.resolver = resolver;
    }

    public MyController() 
    { 
        // Now we support a default constructor
        // since maybe someone else is instantiating this type
        // that we don't control.
    }

    protected void DoActions(Type[] types)
    {
        var services = resolver.Resolve(types);

        foreach(var service in services)
        {
            service.DoAction();
        }
    }
}

Затем вы должны вызывать это при запуске приложения для инициализации распознавателя:

MyController.InitializeResolver(new NinjectMyServiceResolver(kernal));

Мы сделали это для обработки элементов, созданных в XAML, которые требуют разрешения зависимостей, номы хотели удалить запросы, аналогичные Service Locator.

Прошу прощения за любые синтаксические ошибки:)

Я пишу серию публикаций в блоге на тему рефакторинга приложения MVVM с вызовами Service Locator впросмотреть модели, которые могут вас заинтересовать.Часть 2 скоро появится:)

http://kellabyte.com/2011/07/24/refactoring-to-improve-maintainability-and-blendability-using-ioc-part-1-view-models/

4 голосов
/ 26 июля 2011

Возможно, вам следует просто убрать Kernel, Types и MySpecialResolver и позволить подклассам вызывать DoActions с экземплярами IMyServiceInterface, которые им нужны в качестве аргумента напрямую.И пусть подклассы решат, как они доберутся до этих экземпляров - они должны знать лучше (или, если они не знают, кто именно решает, какие экземпляры IMyServiceInterface необходимы)

1 голос
/ 28 июля 2011

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

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

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

Создать командный интерфейс

Прежде всего, я бы создал командный интерфейс для конкретной реализации.В этом случае типы, отправленные с помощью метода DoActions, генерируются из атрибутов, поэтому я бы создал IAttributeCommand.Я добавляю метод Matches к команде, чтобы объявить команду для использования определенными типами.

public interface IAttributeCommand
{
    bool Matches(Type type);
    void Execute();
}

Добавить реализацию команд

Чтобы реализовать интерфейс, я передаюконкретные зависимости, которые мне нужны для выполнения моей команды (разрешается моим контейнером).Я добавляю предикат в свой метод Matches и определяю свое поведение Execute.

public class MyTypeAttributeCommand : IAttributeCommand
{
    MyDependency dependency;
            SomeOtherDependency otherDependency;

    public MyTypeAttributeCommand (MyDependency dependency, ISomeOtherDependency otherDependency)
    {
        this.dependency = dependency;
                    this.otherDependency = otherDependency
    }

    public bool Matches(Type type)
    {
        return type==typeof(MyType)
    }
    public void Execute()
    {
        // do action using dependency/dependencies
    }
}

Регистрация команд с контейнером

В StructureMap (используйте ваш любимый контейнер) я бы зарегистрировал массив следующим образом:

Scan(s=>
       {
                s.AssembliesFromApplicationBaseDirectory();
                s.AddAllTypesOf<IAttributeCommand>();
                s.WithDefaultConventions();
       } 

Выбор и выполнение команд на основе типа

Наконец, для базового класса я определяю массив IAttributeCommand в моих аргументах конструктора, которые будут вводиться контейнером IOC.Когда производный тип передается в массиве types, я выполню правильную команду на основе предиката.

public abstract class MyController : Controller
{
    protected IAttributeCommand[] commands;

    public MyController(IAttributeCommand[] commands) { this.commands = commands); }

    protected void DoActions(Type[] types)
    {
        foreach(var type in types)
        {
            var command = commands.FirstOrDefault(x=>x.Matches(type));
            if (command==null) continue;

            command.Execute();
        }
    }
}

Если несколько команд могут обрабатывать один тип, вы можете изменить реализацию: commands.Where(x=>x.Matches(type)).ToList().ForEach(Execute);

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

...