Инверсия контроля
Инверсия контроля (Inversion of Control, IoC) - это важный принцип ООП
Подождите, какой контроль? Контроль чего нужно инвертировать?
Имеется в виду контроль над созданием зависимостей.
Представим себе автомобиль,
без мотора он не сможет работать, потому нужен мотор.
С первого взгляда выглядит как превосходный код. Но принцип инверсии тут не соблюдается.
Дело в том, что автомобиль должен ездить и перевозить грузы, а не создавать моторы и другие запчасти.
Создавая себе мотор, автомобиль занимается тем, для чего он не предназначен, и становится фабрикой на колёсах.
Легко себе представить, во что превратится код этого класса, когда окажется, что автомобиль не может хорошо работать без колёс, руля, магнитолы с DVD, и большого количеств других важных частей.
Согласно принципу инверсии контроля, автомобиль не должен думать о создании зависимостей, вместо этого, в приложении должен быть механизм который позаботится об этом и позволит автомобилю делать то для чего он предназначен.
Есть и вторая проблема с этим автомобилем.
Если вдруг потом понадобится заменить паровой двигатель на бензиновый, то придётся разбирать машину (рефакторить) и налаживать в ней производство нового типа двигателя, и так с каждой маркой машин.
А если двигатели будет производить только моторный завод, то производство новых моторов достаточно будет наладить только один раз.
Теперь, поняв в чём вред отсутствия инверсии контроля,
можно перечислить выгоды от её наличия:
- позволяет модулю сфокусироваться на своей задаче
(не быть фабрикой на колёсах) - ослабляется связанность классов между собой
(мотор больше не неотъемлемая часть автомобиля) - обеспечивает простоту замены модулей
(вместо одного мотора всегда можно подсунуть другой мотор) - упрощает тестирование и ремонт
(вместо мотора всегда можно подсунуть пустышку)
Реализовать этот принцип можно разными способами
- Внедрение зависимости (Dependency injection)
- Через конструктор (Constructor injection)
- Через сеттер (Setter injection)
- Через интерфейс (Interface injection)
- Патерн фабрика (Factory pattern)
- Service locator (Service locator pattern)
- IoC контейнер(Ioc-container)
Тут уже гугль вам в помощь.
Приведу только пример с использованием SwiftSuspenders
(который используется в RobotLegs)
======= IEngine.as ======== public interface IEngine {} ======= Engine.as ======== public class Engine implements IEngine { public function Engine(){ trace('engine constructed'); } } ======= Car.as ======== public class Car { [Inject] public var engine:IEngine; public function start():void{ if(engine) trace('it works'); } } ======= Main.as ======== import flash.display.Sprite; import org.swiftsuspenders.Injector; public class Main extends Sprite{ public function Main(){ var car:Car = new Car(); var injector:Injector = new Injector(); injector.map(IEngine).toType(Engine); injector.injectInto(car); // engine constructed car.start(); //it works } }
Полезная ссылка:
перевод статьи Мартина Фаулера "Inversion of Control Containers and the Dependency Injection pattern"
Всего комментариев 42
Комментарии
14.04.2012 13:37 | |
Цитата:
лучше
Код AS3: car.engine = new Engine(); |
14.04.2012 14:09 | |
Проблема только в том, что если машина подписана на события старого мотора, а ей его кто-то поменял, то получится треш.
|
14.04.2012 14:18 | |
Треш в такой ситуации получится вне зависимости от того каким образом при этом был создан мотор.
SwiftSuspenders для разрешения такой ситуации диспатчит InjectionEvent-ы PRE_CONSTRUCT и POST_CONSTRUCT и еще есть метатег [PostConstruct] a еще проще сделать так: |
|
Обновил(-а) artcraft 14.04.2012 в 15:29
|
14.04.2012 16:33 | |
Меня ещё смущает отсутствие инкапсуляции в таком инжекторе. Надо хотя бы неймпейс свой иметь для таких вещей.
|
14.04.2012 16:52 | |
Что-то пункт "Через интерфейс (Interface injection)" как-то выбивается из списка. Т.е. первые два про то, как ссылку передаём, а этот совсем про другое.
|
14.04.2012 17:10 | |
@etc я не предлагаю всем брать и пользоваться именно SwiftSuspenders, он тут как пример IoC контейнера
@fljot вот тут пример (правда не AS3) |
14.04.2012 23:34 | |
Боже мой, мне так стыдно, что я не знаю, как использовать эти ваши Синглтоны, Интерфейсы...
Но почему-то мне это кажется бесполезным. |
14.04.2012 23:40 | |
MikroAcse Как и классы, как таковые, многим кажутся бесполезными по началу. Всё это нужно когда проекты становятся действительно большими, а программистов много.
|
15.04.2012 00:01 | |
Aquahawk А пример их использования можешь дать? Как они помогают?
|
15.04.2012 02:22 | |
Добавил в статью ссылку на авторитетный источник.
@Rzer Про то как устроен джавовский SAX знаю только понаслышке, поэтому ничего сказать об этом не могу. Под инверсией контроля иногда имеют в виду ещё и немного другое понятие, вот статья про инверсию другого контроля. Возможно в этой цитате речь именно об этой другой инверсии. Цитата:
Есть ли реальный пример зачем это нужно? и где удобней использовать?
В тех ситуациях когда есть пачка взаимозаменяемых плагинов, из которых одновременно используется только один, необходимость такого подхода очевидна. Во всех остальных случаях, если выносить создание зависимости за пределы класса, то вреда не будет, и главное, когда вдруг окажется, что нужно расширить в том месте где изначально этого не планировалось, шанс нарваться на трудности будет гораздо ниже Цитата:
Боже мой, мне так стыдно, что я не знаю, как использовать эти ваши Синглтоны, Интерфейсы...
Но почему-то мне это кажется бесполезным. |
|
Обновил(-а) artcraft 15.04.2012 в 02:27
|
15.04.2012 19:22 | |
Цитата:
- не возможно нормально в отладчике посмотреть.
|
15.04.2012 23:22 | |
Цитата:
Вот тут интерфейс как нельзя кстати, мы сразу сможем увидеть какие же методы принимает наш вид, не ковыряясь в коде глубоко.
|
16.04.2012 00:50 | |
Цитата:
Что невозможно в отладчике посмотреть?
|
16.04.2012 11:55 | |
Цитата:
Поставить бряк на метадату?
|
16.04.2012 14:11 | |
На метадату и не надо ставить брэйкпоинтов.
Метатэг это метка для инжектора, сам он ничего не делает. По сути строчка injector.injectInto(car); обозначает - собрать машину (пройтись по всем помеченным сеттерам и передать им то что указано в конфиге). Это можно сделать и руками, но тогда нужно будет написать не одну строчку а по строчке на каждый сеттер. Поставив брэйк на сеттер будет видно откуда он вызван, а если брэйкпоинт не остановит приложение, то это тоже результат. И еще, SwiftSuspenders при инжекте выбросит ошибку если в конфиге нету нужной информации, или не совпадают типы |
|
Обновил(-а) artcraft 16.04.2012 в 14:21
|
16.04.2012 15:24 | |
Нет, ну что вы, что вы, я вас уверяю, все сделать легко, имея необходимые знания и инструменты! Проблема в том, что дебаггер не поможет это выяснить по причине, которую я объяснил выше (т.е. не является подходящим инструментом), и ошибка эта не очевидная - т.е. нет необходимых знаний. А так, да, конечно все будет в лучшем виде, в сжатые сроки, всенепременно.
Еще раз: сеттер не вызвался - и вы не знаете почему, а не вызвался и вы смогли это зафиксировать. Это 99% всей головной боли от [Bindable] когда убиваешь по нескольку часов на то, чтобы выяснить почему до конкретного места выполнение не дошло. А вы рассказываете про какие-то тривиальные ситуации, которые в жизни вообще не встречаются, или на столько тривиальны, что никто бы и не подумал, что они могут представить сколь-нибудь существенные сложности. Вариант с метаданными является заведомо корявым в 100% случаев. Мы как раз их и обсуждаем. Какие еще не корявые варианты вы имеете в виду? |
|
Обновил(-а) wvxvw 16.04.2012 в 15:29
|
16.04.2012 15:40 | |
Если сеттер не вызывался а над ним стоит метатег [Inject], то значит после создания этого объекта в него не впрыснули зависимости на этапе конструирования - 2 клика и ошибка найдена.
И кстати, если подобная ошибка произойдёт в проекте без метатегов, то искать ошибку будет только труднее. (не так очевидно в какой момент и откуда планировалось этот сеттер вызывать) |
|
Обновил(-а) artcraft 16.04.2012 в 15:58
|
16.04.2012 17:24 | |
"найти все использования" - никогда не полагался на такую функцию, именно по той причине что она находит не всё
вот если вместо [Inject] написать [Injcet] или [inject], a ещё лучше [lnject] то действительно можно долго тупить и искать в чём дело но остальное ситуации, лично мне, кажутся достаточно прозрачными |
|
Обновил(-а) artcraft 16.04.2012 в 17:40
|
16.04.2012 17:48 | |
кстати IDEA на незнакомый метатег выдаёт weak warning, так что, если не средствами компилятора, так средствами IDE и этой ошибки можно избежать
|
17.04.2012 18:21 | |
Вообще иногда бывают свободные минуты времени, которые я уделяю рассуждению о надобности ДИ. На работе сейчас юзаем swiz, и скажу что вообщем опыт весьма позитивный. Кстати такой очепятки там не получится.
Сам найдет единственную реализацию IFoo, которая лежит в маппинге свиза. Бывают конечно стремнаватые ситуации когда реализация IFoo не одна и нужно по айдишнику вылавливать ту, которая нам нужна. Но такие ситуации возникают крайне редко, во всем проекте сейчас может 3 - 4 таких места, и они потихоньку истребляются. В итоге в проекте сейчас около 70 модулей, и все они зависят лишь от фреймворковых, либо базовых модулей проекта. Так что пока что всё довольно хорошо. Сама теория ДИ, меня смущает и лично у меня есть на этот счет некоторые опасения. Но на практите, вроде как, работает стабильно. |
|
Обновил(-а) incvizitor 18.04.2012 в 16:27
|
09.05.2012 23:21 | |
2 incvizitor:
Конкретная ситуация (описал только один аспект): - Eсть контроллер over 2000 строк, который знает о куче объектов - Контроллер пичкает разными объектами вручную через конструкторы команды, отправляющие запрос на сервер и меняющие соотв. образом модель. - разные окошки берут комманды так: controller.newAddItemToInventory(item).run(); - инвентарь, юзера(для изменения денег), сам класс для отправки запросов, и кучу другого контроллер подставляет сам. - в итоге куча передач параметров и здоровенный интерфейс (здоровенный, потому что в нём описаны все методы для всех комманд) Решил создавать комманды инжектором там где они нужны (injector.resolve(MyCommand) as MyCommand), и все бы ничего, но: - текущий пользователь меняется когда заходишь к другу Это что получается, нужно менять меппинг у инжектора при переходе? - некторые комманды при создании в контроллере слушаются им и производятся некоторые действия по окончанию Замепил в инжекторе на метод, который это делает и возвращает комманду, но чем это удобнее, если бы я просто оставил публичный метод newCommand()? Вобщем, очень пугает "нединамичность" этой штуки - замепил, собралось и всё Неужели таких проблем не возникало? И 2-е: Говорят, что правильное использование контейнера означает вызов только одного resolve в точке входа - а остальное - без контейнера происходит заинжектенными в приложение фабриками. Но тогда непонятно зачем он вообще нужен - ведь можно с тем же успехом вместо конфигурации вручную с помощью new понасоздавать все фабрики и объекты в одном стартовом методе - ну кода только больше будет, зато всё прозрачно и зависимостей внуть полезет не больше чем при использовании контейнера с одним вызовом resolve. Не понимаю короче. Иногда кажется, что рефлексия хороша только для запуска тестов |
|
Обновил(-а) expl 09.05.2012 в 23:35
|
12.05.2012 03:57 | |
2 expl,
Ну 2000 строк это точно не есть хорошо. Я делаю так (когда не юзаю ДИ, а простое МВЦ): Моделька кидает всплывающие событие. В нутри события лежит комманда, внутри комманды лежит респондер. Контроллер ловит евент у основной модельки, скармливает комманду сервер коммуникатору. сервер коммуникатор создает объект реквеста и передает ему ссылку на респондер. когда приходит ответ с сервака, дергается респондер который сам изменит в модели все что нужно. То есть контроллер о типах запросов особо то и знать ничего не должен. То есть функционал контроллера в коде выглядит так: dataBase.addEventListener(CommandEvent.COMMAND, _onCommandRecived); private function _onCommandRecived(e:CommandEvent):void{ dispatchCommand(e.command);//диспатч комманды словит серер коммуникатор. } Цитата:
- текущий пользователь меняется когда заходишь к другу
Это что получается, нужно менять меппинг у инжектора при переходе? //Менеджер переключения фермы public class FarmManager{ [Inject] //таким образом инджектается объект public var friendsRespository:IFriendsRepository; [Inject] public var farmModel:FarmModel; [EventHandler(event="ChangeFarmEvent.CHANGE_FARM", properties="userID")] //Вот так в свизе можно ловить евенты которые бросает медиатор public function onUserChanged(userID:String):void{ farmModel.currentUser = friendsRespository.getUserByID(userID); } } //Базоваю въюшка фермы public class FarmViewBase extends Sprite{ [Inject] [Bindable] public var farmModel:FarmModel; } //Въюшка фермы <view:FarmViewBase> <someNS:Image uri="{farmModel.currentUser.iconURI}"/> </view:FarmViewBase> Цитата:
И 2-е:
|
|
Обновил(-а) incvizitor 12.05.2012 в 14:30
|
14.05.2012 01:54 | |
О, спасибо за ответ!
(сам этот задержал, т.к. времени нехватало) -------------------------------------------------------- Сначала по поводу 2-го вопроса: Коли речь в посте о SwiftSuspenders, то там центральная роль отводится Injector-у, на него мепятся зависимости и правила создания объектов: instance.map(IMyClass).toType(MyClass); // MyClass принимает в конструкторе типы IA и IB instance.map(IA).toType(A); instance.map(IB).toSingleton(B); // Дальше настраиваем зависимости для A и B и т.д. .. // Собираем зависимости var instance:IMyClass = injector.getInstance(IMyClass); но и внутри этих классов MyClass, A и т.д. Дабы не протягивать туда параметры через конструкторы и всяческие сеттеры Да и вообще если надо фабрику протянуть - её же делать классом придётся, не заинжектишь же IFactory, про который в рефлексии не написано, что он производит Городить метаданные? Увольте! А так - вместо factory.newInstance() - injector.getInstance(MyClass). Чем чревато тянуть инжектор - я понимаю. Я не понимаю зачем он нужен, если его не тянуть. Ну в самом деле, неужели, если не тянуть инжектор нельзя настроить приложение ручками: // MyClass принимает в конструкторе типы IA и IB var c:C = new C(); var a:IA = new A(c); var d:D = ... ... var b = new B(a, c, d); var instance:IMyClass = new MyClass(a, b); ------------------------------ Однако, я не нашел в swiz никакого getInstance или resolve. Тоесть что получается ральный подход, все объекты берутся не у _какого-то объекта по типу_, а через специальные методы "как-бы фабрик"? Т.е. глядя на класс снаружи всегда можно сказать объекты каких интерфейсов он использует? ----------------------------- Цитата:
Но вернемся к ДИ:
Цитата: - текущий пользователь меняется когда заходишь к другу Это что получается, нужно менять меппинг у инжектора при переходе? Зачем? У тебя есть въюшка допустим фермы. ты перешел на ферму друга и скормил въюхе другую модель. Просто в менеджере (который будет осуществлять переход) нужно заинджектать репозиторий юзеров (из которого можно получить юзера по идишнику), модельку которая хранит текущей фермы. В коде это так Т.е. получается, что инжекция используется только для неменяющихся данных... (ну или контейнеров меняющихся). В чём же ее прикол, в удобной настройке приложения для тестов и для боя? ---------------------------- Цитата:
Ну 2000 строк это точно не есть хорошо. Я делаю так (когда не юзаю ДИ, а простое МВЦ): Моделька кидает всплывающие событие. В нутри события лежит комманда, внутри комманды лежит респондер. Контроллер ловит евент у основной модельки, скармливает комманду сервер коммуникатору. сервер коммуникатор создает объект реквеста и передает ему ссылку на респондер. когда приходит ответ с сервака, дергается респондер который сам изменит в модели все что нужно. То есть контроллер о типах запросов особо то и знать ничего не должен. То есть функционал контроллера в коде выглядит так:
Про респондер, он у тебя ответ с сервера разбирает и меняет модель? А комманда - это объект с необходимой для запроса информацией? (Если это так, то у меня комманда - это и данные для сервера и разбор ответа с изменением модели, она, правда, сама сервер-коммуникатор дергает) Про нелёгкий путь события с командой и респондером и в итоге передачу управления последнему - понятно. За кадром остался вопрос: Кто создаёт событие? Этот товарищ должен обладать информацией, нужной респондеру и комманде для формирования запроса на сервер. Вот эти данные и не хотелось тянуть в окошки, поэтому глобально меняющиеся и постоянные осели в контроллере и сделали его интерфейс непомерно большим (хотя их доля в 2000 строках не велика - там другого хватает). Этот интерфейс как-то даже стабами и моками для тестов подменять рука не подымается. Но, кажется родилось другое решение: - берём и подписываем к методам, возвращающим комманды (в моём понимании этого слова) вот такой тег: - регим весь Controller в injector'е как провайдер - берём комманды во вьюшках как и хотели в прошлый раз: Всё! Раньше мне не нравилось, что мепить неудобно - приходится отдельные методы указывать и еще параметры неявно для комманд мепить, а они меняются. Теперь всё протягивается вручную - а интерфейс сузился до одного метода! (хоть и не по феншую - см. "Сначала по поводу 2-го вопроса") |
|
Обновил(-а) expl 14.05.2012 в 03:01
|
14.05.2012 03:40 | |
Цитата:
expl, я говорил конкретной реализации ДИ (той что во свизе). Можно инжектать и по интерфейсу, если для данного интерфейса существует только одна имплементация. То есть фреймворк понимает что тебе именно нужно скормить. Если имплементаций данного интерфейса несколько, то пойдут конфликты.
Если сделать класс BFactory - то всё понятно, он заинжектится Если не делать класса, а использовать стандартную фабрику, например ClassFactory - то как, кроме метаданных, сказать какой тип объекта мы хотим создавать? (из генериков то у нас только Vector, но даже если его тип в xml-описание попадает, фабрику из него не сделаешь) Т.е. не тип фабрики - нас он не волнует, а тип создаваемого объекта? Ну если только с помощью IFactory во всём проекте мы только один тип объекта создаём - тогда никаких метаданных не потребуется. Цитата:
Цитата:
Т.е. глядя на класс снаружи всегда можно сказать объекты каких интерфейсов он использует? Да, сокрытие информации порядочно хромает. - в конструкторе - который впринципе в интерфейс не идет - в некоторых сеттерах, через которые инжектит инжектор, но в интерфейсе и без них неплохо - они нужны только для внутренней работы класса. Т.е. простой интерфейс не составляет труда подменить Стабом/моком, когда тестируем зависимые от интерфейса классы. Но когда тестируем сам класс - нам совершенно очевидно от чего он зависит. Совсем наоборот - когда класс внутри что-то берёт через injector.get(Type) - поди догадайся какой тип в инжекторе ему надо замепить. Цитата:
Считайте что можно получить ссылку на любой промапленный объект в почти любом классе приложения. Это могут быть и провайдеры, и модельки, и сервер коммуникатор и все что угодно.
Выгода в том, что объект не надо протягивать, он передается через ДИ. [бред] ...можно сконфигурировать вручную, но слепить вручную фабрику, которую не надо переписывать при изменении параметров... может IoC-контейнер не просто удобный конфигуратор, ан-нет - фабрики то хреново инжектятся - т.е. в них вручную надо встраивать injector... [/бред] |
|
Обновил(-а) expl 14.05.2012 в 03:54
|
16.05.2012 01:05 | |
Цитата:
Всё инжектит фреймворк по меппингу?
<?xml version="1.0" encoding="utf-8"?> <swiz:BeanProvider xmlns:swiz_="http://swiz.swizframework.org" xmlns:swiz="org.swizframework.core.*" xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:flasher="http://www.flasher.ru" > <flasher:SomeManager/> <swiz:Prototype type="{Model}" singelton="true"/> <mx:Script><![CDATA[ ]]></mx:Script> </swiz:BeanProvider> "дай мне сюда объект этого типа" пишиться тегом [Inject]. Ваще это можно в доке свиза почитать, она за 30 мин спокойно читается. Фабрики хреново инжектятся, по крайней мере я не нашел хорошего способа их инжектать. Но скажу по правде, лично мне пришлось инжектать фабрику только 1 раз |
|
Обновил(-а) incvizitor 16.05.2012 в 01:21
|
24.05.2012 00:28 | |
Если я правильно понял, то инъекции можно создавать где угодно
|
Последние записи от artcraft
- Что такое entity framework (12.09.2012)
- Подводные камни Dictionary (04.09.2012)
- Волшебное превращение Object --> Class (04.09.2012)
- Инверсия контроля (14.04.2012)
- Loose coupling (10.01.2012)