Как стать автором
Обновить

Скрытые зависимости как «запах» проектирования

Время на прочтение3 мин
Количество просмотров8.2K
Автор оригинала: EngineerSpock
Марк Симан написал замечательный пост «Service Locator нарушает инкапсуляцию». Название поста говорит само за себя о том, что он посвящён паттерну (анти-паттерну) Service Locator. Когда программист произвольно в коде вызывает IoC-контейнер для разрешения зависимости того или иного объекта — он использует Service Locator анти\паттерн. Марк рассматривает следующий пример:
public class OrderProcessor : IOrderProcessor
{
    public void Process(Order order)
    {
        var validator = Locator.Resolve<IOrderValidator>();
        if (validator.Validate(order))
        {
            var shipper = Locator.Resolve<IOrderShipper>();
            shipper.Ship(order);
        }
    }
}


Как мы видим инкапсуляция типа OrderProcessor поломана из-за двух скрытых зависимостей, которые по-тихому разрешаются в методе Process. Эти зависимости скрыты от вызывающего кода, что может привести к исключению в runtime в случае, если клиент не настроил должным образом IoC-контейнер, определив в нём необходимые зависимости. В качестве решения проблемы Марк предлагает перенести разрешение зависимостей в конструктор объекта.
public class OrderProcessor : IOrderProcessor
{
    public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
 
    public void Process(Order order)
}

Таким образом, вызывающий код будет в курсе того, что на самом деле требует объект OrderProcessor.

Однако, на мой взгляд, всё же есть сценарии, где сокрытие зависимостей можно применить. Рассмотрим WPF-приложение в котором практически любая ViewModel требует следующие зависимости: IEventAggregator, IProgress, IPromptCreator. Чтобы раскрыть смысл двух последних интерфейсов, добавлю, что реализация IProgress должна уметь принимать кусок долго выполняемого кода и показывать окно со шкалой прогресса, IPromptCreator позволяет открывать окна, запрашивающие подтверждение, согласие или отказ (модальные диалоги). Теперь представим, что есть ViewModels, которые требуют вдобавок две (а может и три) зависимости для создания модели. Вот каким образом ViewModel может начать выглядеть с таким количеством зависимостей:
public class PaymentViewModel: ICanPay
{
    public PaymentViewModel(IPaymentSystem paymentSystem, 
                            IRulesValidator rulesValidator, 
                            IEventAggregator aggregator, 
                            IProgress progress, 
                            IPromptCreator promptCreator)
    public void PayFor(Order order)
}

Какой бардак! Слишком много шума в декларации конструктора. Всего две зависимости действительно несут полезную информацию с точки зрения бизнес-логики.

Если мы, скажем, используем MEF для инъекции зависимостей, то мы можем сделать следующее:
[Export]
public class PaymentViewModel : ICanPay
{
    [Import]
    protected IEventAggregator aggregator;
    [Import]
    protected IProgress progress;
    [Import]
    protected IPromptCreator promptCreator;
 
    public PaymentViewModel(IPaymentSystem paymentSystem, 
                            IRulesValidator rulesValidator)
    {
    }
 
    public void PayFor(Order order)
    {
        //use aggreagtor, progress, promptCreator
    }
}

Мы перенесли зависимости из конструктора в декларации полей и пометили их атрибутом Import. Несмотря на то, что мы не вызываем IoC-контейнер напрямую (хотя, MEF это не IoC-контейнер в чистом виде), мы скрываем зависимости так же, как и в примере Марка. Ничего по сути не поменялось. Почему я считаю, что этот код не так уж плох? По нескольким основным причинам:
  • ViewModels не являются бизнес-сущностями, они представляют собой просто куски кода для склеивания Models и Views. Никто особо не парится насчёт выше указанных зависимостей;
  • ViewModels не являются публичным API и они не переиспользуются (в большинстве случаев);
  • С учётом двух предыдущих пунктов, члены команды могут просто договориться о том, что все ViewModels имеют эти утилитарные зависимости, и всё тут;
  • Эти утилитарные зависимости определены как protected, что позволяет в тестах создать класс ViewModel, наследующий PaymentViewModel, и заменить зависимости моками, поскольку мы будем иметь доступ к тем полям. Таким образом, мы не теряем возможность покрыть тестами PaymentViewModel. В примере Марка (где используется Service Locator) было необходимо настроить IoC-контейнер в проекте юнит-тестов для того, что замокать или застабить те зависимости, а такая практика может стать болезненной для процесса юнит-тестирования.

Заключение


Как я уже говорил, в программировании нет единственно правильных ответов или утверждений на все случаи жизни. Вообще говоря, мы должны избегать использования Service Locator, поскольку это нарушает инкапсуляцию, как и говорит Марк в своей статье. Перед тем как использовать Service Locator, оцените потенциальный вред, который вы нанесёте системе. Если вы уверены, что скрытое разрешение зависимостей не влияет на клиентский код (который могут писать ваши товарищи по команде) и для системы нет потенциального вреда, то вперёд и с песней.
Теги:
Хабы:
Всего голосов 10: ↑6 и ↓4+2
Комментарии36

Публикации

Истории

Работа

.NET разработчик
68 вакансий

Ближайшие события