Я ненавижу контейнеры 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.
Выбор, который мы делаем, и технологии, которые мы выбираем, делают некоторые вещи легкими, а другие — сложными. Нам нужно тщательно подумать о том, должны ли вещи, которые мы делаем легко, быть легкими. Всегда можно делать правильные вещи, но иногда нам нужно сделать это проще, чем делать неправильные вещи.