Аннотация:
Статья рассказывает о наиболее популярных паттернах внедрения зависимостей, которые будут полезны всем разработчикам, независимо от того, используют они какой-либо контейнер или предпочитают ручную композицию объектов в приложении.
Здравствуйте, Sinix, Вы писали:
S>Такой вопрос по ServiceLocator. Почему по-твоему проблема S>
S>Самое страшное в Service Locator то, что он дает видимость хорошего дизайна. У нас никто не знает о конкретных классах, все завязаны на интерфейсы, все «нормально» тестируется и «расширяется». Но когда вы попробуете использовать ваш код в другом контексте, или когда кто-то попробует использовать его повторно, вы с ужасом поймете, что у вас есть дикая «логическая» связанность, о которой вы и не подозревали.
S>относится только к нему и не относится к прочим паттернам?
S>По-моему, эта проблема общая для любой _динамической_ системы разруливания зависимостей и нюансы конкретной реализации тут мало что решают. Особенно с типовым для биз-логики требованием "настраиваемый объект может _динамически_ создавать другие настраиваемые через DI объекты".
Тут, мне кажется, нужно рассматривать разные аспекты проблемного дизайна.
Одна проблема действительно присуща любой динамической системе завязанной на интерфейсы: никто не знает, что мы используем в каждый конкретный момент времени; никто не знает, все ли реализации следуют контрактам, поскольку они выражены не формально и никто не может размышлять о динамической природе системы (т.е. о системе, как взаимодействующих объектах), поскольку не ясно из каких конкретно объектах эта система состоит.
Я же здесь говорю несколько о другой проблеме.
Допустим, мы протаскиваем зависимости явно через конструкторы. В этом случае, если у класса в конструкторе протаскиваются 4-5 зависимостей, то читателю довольно быстро становится очевидным, что с дизайном что-то не так: как-то добавлять 6-й не хочеся, ведь для этого придется поменять не только этот, но еще и пяток соседних классов.
Если же используется локатор, протащенный через конструктор базового класса, то в любой момент времени, любой класс может получить любую зависимость. Система расширяема до нельзя, но рассуждать о том, какие зависимости у конкретного класса уже невозможно. Для этого нужно чтащтельно изучить его детали реализации.
Именно о такой связанности я писал: с виду, у класса есть одна зависимость — локатор, но по факту, любая попытка использовать его повторно приведет к тому, что придется локатор этот проинициализировать каким-то хитрым образом, что сведет на нет все усилия.
S>Самое парадоксальное, что я видел — когда локатору прямо противопоставляют IOC фреймворки, хотя у них под капотом по факту всё тот же словарик {тип, метод для получения экземпляра}
Мое ИМХО, что локатор — это скорее паттер в котором экземпляр контейнера протаскивается явным образом. Мне сложно сказать, как их можно противопоставлять друг другу...
S>P.S. Кстати, в "Смягчаем проблему" не упомянут самый правильный способ — навешивание на ServceLocator extension-методов, которые явно документируют поддерживаемые методы через интеллисенс.
+1: хороший подход. Но сам никогда им не пользовался.
I>Лично для меня разница, всё-таки, есть. Вот два примера: I>Казалось бы, действие одно и то же. Но тестировать второй вариант значительно проще, так как у нас есть контракт которому нужно соответствовать.
Согласен безусловно. Но у меня на практике этот подход работал только для чисто инфраструктурного кода, который по определению SRP.
Для бизнес-логики идея подыхает из-за комбинаторного взрыва.
Чтоб было понятно о чём речь. Простейший бизнес-сценарий дёргает сервисы расчёта стоимости заказа, учётную политику для конкретного контрагента, определение адресата по кладр и дальше передаёт результат в 5-7 других биз-сценариев, которые выбираются динамически и могут посылать по почте/печатать отчёт/запускать документоооборот и тыды и тыпы.
Даже для такой мелочёвки проще прокинуть один service locator, при необходимости дополняя DI-инъектором внутри классов, что-то типа serviceProvider.InjectAll(this).
В более сложных вещах, когда только краткая сопроводиловка в бумаге за 200 листов перелазит, микроменеджмент на уровне отдельных сервисов нереален в принципе.
На больших масштабах все эти "вы можете отследить каждую зависимость" превращается в "вы _будете_ отслеживать _каждую_ зависимость". Нафиг-нафиг.
I>Если же у нас по коду класса раскаданы вызовы GetService, то это неизменно приводит к тому что тесты сыпятся в рантайме, а это не есть хорошо. I>Для себя обозначил такое правило: "ServiceLocator не должен встречаться в тестируемом коде".
А такие штуки уже не ловятся классическими юнит-тестами в принципе, только интеграционными. Масштаб не тот слегка. Ну, как танкер в микроскоп проверять.
На практике вся разница в том, что мы не воссоздаём всё окружение для каждого теста в отдельности. Вместо этого поднимается инстанс сервиса с включенными отладочными ассертами и дальше он топится пачкой проверок, записанных в виде стандартных юнит-тестов. Как опция, сами тесты подгружаются в инстанс сервиса и используют внутреннее API сервиса, т.е. тот самый servicelocator.
Здравствуйте, Sinix, Вы писали:
S>По-моему, эта проблема общая для любой _динамической_ системы разруливания зависимостей и нюансы конкретной реализации тут мало что решают. Особенно с типовым для биз-логики требованием "настраиваемый объект может _динамически_ создавать другие настраиваемые через DI объекты".
Лично для меня разница, всё-таки, есть. Вот два примера:
Казалось бы, действие одно и то же. Но тестировать второй вариант значительно проще, так как у нас есть контракт которому нужно соответствовать.
Если же у нас по коду класса раскаданы вызовы GetService, то это неизменно приводит к тому что тесты сыпятся в рантайме, а это не есть хорошо.
Для себя обозначил такое правило: "ServiceLocator не должен встречаться в тестируемом коде".
Такой вопрос по ServiceLocator. Почему по-твоему проблема
Самое страшное в Service Locator то, что он дает видимость хорошего дизайна. У нас никто не знает о конкретных классах, все завязаны на интерфейсы, все «нормально» тестируется и «расширяется». Но когда вы попробуете использовать ваш код в другом контексте, или когда кто-то попробует использовать его повторно, вы с ужасом поймете, что у вас есть дикая «логическая» связанность, о которой вы и не подозревали.
относится только к нему и не относится к прочим паттернам?
По-моему, эта проблема общая для любой _динамической_ системы разруливания зависимостей и нюансы конкретной реализации тут мало что решают. Особенно с типовым для биз-логики требованием "настраиваемый объект может _динамически_ создавать другие настраиваемые через DI объекты".
Самое парадоксальное, что я видел — когда локатору прямо противопоставляют IOC фреймворки, хотя у них под капотом по факту всё тот же словарик {тип, метод для получения экземпляра}
P.S. Кстати, в "Смягчаем проблему" не упомянут самый правильный способ — навешивание на ServceLocator extension-методов, которые явно документируют поддерживаемые методы через интеллисенс.
Здравствуйте, SergeyT., Вы писали:
ST>Я же здесь говорю несколько о другой проблеме. ST>Допустим, мы протаскиваем зависимости явно через конструкторы. В этом случае, если у класса в конструкторе протаскиваются 4-5 зависимостей, то читателю довольно быстро становится очевидным, что с дизайном что-то не так: как-то добавлять 6-й не хочеся, ведь для этого придется поменять не только этот, но еще и пяток соседних классов.
Ага, другими словами: локатор провоцирует протаскивать себя, даже если по факту не нужен, и тем самым скрывает косяки, которые без локатора вылезли бы раньше, так?
Вот тут согласен целиком и полностью. Засада в том, чтобы использовать локатор только там, где от него есть толк. Не оттоптавшись самостоятельно на граблях эту разницу фиг почуешь. Проблема, да.
Здравствуйте, Тепляков Сергей Владимирович, Вы писали:
ТСВ>Аннотация: ТСВ>Статья рассказывает о наиболее популярных паттернах внедрения зависимостей, которые будут полезны всем разработчикам, независимо от того, используют они какой-либо контейнер или предпочитают ручную композицию объектов в приложении.
В статье на месте картинок *.emz файлы, которые в браузере посмотреть нет никакой возможности
Здравствуйте, Sinix, Вы писали:
I>>Если же у нас по коду класса раскаданы вызовы GetService, то это неизменно приводит к тому что тесты сыпятся в рантайме, а это не есть хорошо. I>>Для себя обозначил такое правило: "ServiceLocator не должен встречаться в тестируемом коде". S>А такие штуки уже не ловятся классическими юнит-тестами в принципе, только интеграционными. Масштаб не тот слегка. Ну, как танкер в микроскоп проверять.
S>На практике вся разница в том, что мы не воссоздаём всё окружение для каждого теста в отдельности. Вместо этого поднимается инстанс сервиса с включенными отладочными ассертами и дальше он топится пачкой проверок, записанных в виде стандартных юнит-тестов. Как опция, сами тесты подгружаются в инстанс сервиса и используют внутреннее API сервиса, т.е. тот самый servicelocator.
У меня для таких вещей есть специальный конструктор в конфигураторе IOC, куда я могу передавать тестовые сервисы. Тогда поднимается вся инфраструктура, но с выбранными мною сервисами (моками например).
Здравствуйте, ionoy, Вы писали:
S>>На практике вся разница в том, что мы не воссоздаём всё окружение для каждого теста в отдельности. Вместо этого поднимается инстанс сервиса с включенными отладочными ассертами и дальше он топится пачкой проверок, записанных в виде стандартных юнит-тестов. Как опция, сами тесты подгружаются в инстанс сервиса и используют внутреннее API сервиса, т.е. тот самый servicelocator.
I>У меня для таких вещей есть специальный конструктор в конфигураторе IOC, куда я могу передавать тестовые сервисы. Тогда поднимается вся инфраструктура, но с выбранными мною сервисами (моками например).
Мы о разных вещах тут говорим, если я тебя правильно понял
Во-первых, моки не спасут в ситуации, когда у тебя с десяток сервисов и нужно убедиться, что вся эта кухня работает _вместе_. Что обычно и интересует клиента
Во-вторых, поднимать всю инфраструктуру в отдельности на каждый тест — затея малополезная. Во-первых, тесты начинают работать раз в 5 дольше, во-вторых, вместо того, чтоб проверять код в приближенных к боевым условиям, мы опять создаём чистую комнату и тем самым исключаем из тестирования целый класс ошибок типа испорченного конфига или поломанного авторазворачивания базы.
Ну и в-третьих, IOC-фреймворк начинает только мешать, если тестируемый код начинает создавать объекты, которые тоже надо заполнить из IOC-контейнера (и на них цепочка только начинается). В конечном итоге необходимость протаскивать контейнер по всей цепочке превращает IOC в ещё одну импровизацию на тему serviceLocatior.