Статьи

Я могу иметь зависимости?

Я ненавижу контейнеры IoC. Весна? Злой. Guice? Собственная работа дьявола. Зачем? Потому что это приводит к такому вялому, ленивому, бездумному программированию.

Почему ненавижу?

Хорошо, возможно, я лучше объясню немного. IoC —  отличная идея . Что меня раздражает, так это то, как фреймворки IoC в конечном итоге используются обычными людьми. Ранее я рассказывал о том, как контейнеры IoC приводят нас к реализации моделей анемичных доменов . Проблема в том, что когда у вас есть молоток, все начинает выглядеть как гвоздь. Особенно эти надоедливые пальцы. Если у вас есть структура внедрения зависимостей, все начинает выглядеть как зависимость, которую необходимо внедрить. Нужно реализовать бизнес-логику? Сначала создайте новый класс, протестируйте его, затем сделайте его инъекционным, вставьте его в класс, где он нужен вызывающему коду, протестируйте его в действии, затем бинго — вы просто ударили себя по пальцу.

Теперь у меня есть два класса, в основном тесно связанных, но контейнер IoC скрывает этот факт от меня. Я вижу хороший, чистый интерфейс, внедряемый в систему. Разве я не хороший маленький ОО-разработчик? Нет, ты тупой и ленивый.

Прежде чем вы это знаете, у вашего класса есть дюжина или более зависимостей, каждая из которых имеет дюжину зависимостей, каждая из которых имеет дюжину зависимостей, каждая из которых … вы получаете представление. Вам удалось построить крысиное гнездо графа зависимостей, понемногу. То, что вы TDD не дизайн. Техническое название для этого —  Большой Шар Грязи .

Альтернатива

Вместо этого я думаю, что внедрение зависимостей лучше всего работает на стыках приложений, на границах архитектуры. Скажем, например, вы создаете веб-приложение. Вы создали TradeEntryController, который позволяет пользователям вводить сделки. TradeEntryController, естественно, имеет множество зависимостей от остальной системы. Он должен получить действительные активы для инвестиций и цены, он должен знать, каков ваш баланс, чтобы вы не могли купить больше акций, чем денег в банке и т. Д. И т. Д. Прекрасный пример, когда жизнь без контейнера IoC может стать по-настоящему обременительной.

Но я не думаю, что вам это нужно. Я думаю, что вашему контроллеру нужно несколько специфических зависимостей, которые определяют архитектурную границу, в которой живет контроллер. Над контроллером находится HTTP-запрос, сеанс и все такое, бла-бла. В этом есть бизнес-логика. Ниже это база данных. Таким образом, зависимости, которые мы вводим, должны представлять  только архитектурный контекст, в котором работает контроллер. По большей части это будет характерно для  всех моих контроллеров, а не только для входа в сделку. Контроллеры для управления балансами, списками активов, учетными записями пользователей — все это зависит от знания материала об их сеансе и возможности общаться на следующем уровне: базе данных (или в n-уровневой настройке, возможно, некоторых веб-служб) ,

Итак, почему бы просто не ввести эти зависимости?

public class TradeEntryController {
    public void setSessionManager(ISessionManager sessionManager) { ... }
    public void setTradeDatabase(ITradeDatabase tradeDatabase) { ... }
    public void setAccountDatabase(IAccountDatabase accountDatabase) { ... }
    public void setAssetDatabase(IAssetDatabase assetDatabase) { ... }
}

Затем в моем контроллере я могу получить информацию о пользователе из SessionManager; Я могу получить список активов из AssetDatabase; Я могу проверить баланс пользователя через AccountDatabase; и я могу записать сделку через TradeDatabase. До сих пор так же, как обычный контейнер IoC.

Так что же отличается?

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

public interface ICanHazTradeDatabase {
    void setTradeDatabase(ITradeDatabase tradeDatabase);
}

public class TradeEntryController
    implements ICanHazTradeDatabase, ICanHazAssetDatabase...
{
    ...
}

public class ControllerFactory {
    public Controller createController(Class clazz) {
        Controller c = clazz.newInstance();
        if (c instanceof ICanHazTradeDatabase)
            ((ICanHazTradeDatabase) c).setTradeDatabase(tradeDatabase);
        if (c instanceof ICanHazAssetDatabase)
            ((ICanHazAssetDatabase) c).setAssetDatabase(assetDatabase);
        if ...

        return c;
    }
}

Точная механика ControllerFactory, конечно, зависит от вашей инфраструктуры MVC, но, надеюсь, идея ясна: когда мы создаем экземпляр контроллера, мы проверяем его на соответствие известному набору интерфейсов и выдвигаем очень конкретные зависимости. Это красиво? На самом деле, нет. Это легко написать? Конечно. Это подталкивает зависимости в ваш контроллер? Ну да. Откуда они? Ну, это упражнение для читателя. Но я уверен, что вы можете найти способ сделать ControllerFactory одноэлементным и создать все ваши зависимости в одном месте.

Точка

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

Более интересно то, что я  не могу сделать. Я не могу решить, что мой TradeEntryController нуждается в TradePricingCalculator и внедрить это как зависимость. Ну, я мог бы, но я бы сделал TradePricingCalculator везде доступным, и у меня было немного больше работы, чем если бы я использовал простой старый Spring или Guice — у меня есть интерфейс для создания, пара Строки, чтобы добавить к какой-то страшно названной GlobalControllerFactory. Почему это важно? Это добавляет  трения . Трудно сделать что-то плохое. Вместо этого я вынужден подумать о создании объекта TradePrices и добавлении к нему некоторой функциональности. Я вынужден иметь богатый домен, потому что я не могу просто перенести всю свою функциональность в TradePriceCalculatorVisitorFactoryManagerBuilder.

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