Лошадь уже давно мертва, но некоторые до сих пор хотят на ней поездить, так что я пну эту лошадь ещё раз. Годами я предпринимал попытки объяснить почему Service Locator это антипаттерн (например, он нарушает SOLID), но недавно меня осенила мысль, что большая часть моих аргументов фокусировалась на симптомах, упуская из внимания фундаментальную проблему.
В качестве примера для рассмотрения симптомов, в моей статье-первоисточнике, я описывал как ухудшаются возможности использования IntelliSense из-за применения Service Locator. В 2010 году мне и в голову не приходило, что проблема, лежащая в основе, заключается в нарушении инкапсуляции.
Рассмотрим мой исходный пример:
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);
}
}
}
Пример написан на C#, но он будет похожим и на Java и на любом другом сравнимом статически типизированном языке.
Предусловия и постусловия
Одно из главных преимуществ инкапсуляции это абстрагирование: освобождение от бремени понимания каждой детали реализации в каждом кусочке кода в исходниках. Правильно спроектированная инкапсуляция даёт возможность использовать класс, не зная деталей реализации. Это достигается за счёт установления контракта взаимодействия.
Как объясняется в книге Object-Oriented Software Construction, контракт состоит из набора пред и пост-условий для взаимодействия. Если клиент удовлетворяет предусловиям, то объект обещает удовлетворять постусловиям.
В статически типизированных языках, таких как C# или Java, многие предусловия могут быть выражены самой системой типов, как я демонстрировал ранее.
Когда вы смотрите на публичный API класса OrderProcessor, как вы думаете, какие у него предусловия?
public class OrderProcessor : IOrderProcessor
{
public void Process(Order order)
}
Как видно, тут не так много предусловий. Единственное предусловие, которое видно из API заключается в том, что перед тем как вызывать метод Process у вас должен быть объект типа Order.
Да, если вы попытаетесь использовать OrderProcessor, учитывая только это предусловие, то ваша попытка провалится в run-time.
var op = new OrderProcessor();
op.Process(order); // throws
Вот настоящие предусловия:
- требуется объект типа Order
- требуется экземпляр сервиса IOrderValidator в некой глобальной директории локатора
- требуется экземпляр сервиса IOrderShipper в некой глобальной директории локатора
Два из трёх предусловий невидимы в compile-time.
Как видите, Service Locator нарушает инкапсуляцию, потому что этот паттерн скрывается предусловия для корректного использования объекта.
Передача аргументов
Несколько человек шутливо определяли Dependency Injection как разрекламированный термин вместо «передачи аргументов», и, возможно, в этом есть частичка правды.
Наипростейшим способом сделать предусловиями очевидными было бы использование системы типов для выражения требований. В конце концов, мы уже поняли, что нам требуется объект типа Order. Это было очевидно, потому что Order является типом аргумента метода Process.
Можем ли мы сделать необходимость в IOrderValidator и IOrderShipper такой же очевидной, как и необходимость в объекте типа Order, используя ту же самую технику? Может следующий код является решением?
public void Process(
Order order,
IOrderValidator validator,
IOrderShipper shipper)
При некоторых обстоятельствах это всё, что может понадобится сделать — теперь все три предусловия равнозначно очевидны.
К сожалению, часто, такое решение оказывается невозможным. В данном случае, OrderProcessor реализует интерфейс IOrderProcessor.
public interface IOrderProcessor
{
void Process(Order order);
}
Поскольку сигнатура метода Process уже определена, вы не можете добавить в неё аргументы. Вы всё же можете сделать предусловия видимыми через систему типов, требуя от клиента передачи требуемых объектов через аргументы, просто вам нужно передать их через какой-то иной член класса.
Конструктор — самый безопасный способ:
public class OrderProcessor : IOrderProcessor
{
private readonly IOrderValidator validator;
private readonly IOrderShipper shipper;
public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
{
if (validator == null)
throw new ArgumentNullException("validator");
if (shipper == null)
throw new ArgumentNullException("shipper");
this.validator = validator;
this.shipper = shipper;
}
public void Process(Order order)
{
if (this.validator.Validate(order))
this.shipper.Ship(order);
}
}
С таким дизайном, публичное API стало выглядеть так:
public class OrderProcessor : IOrderProcessor
{
public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
public void Process(Order order)
}
Теперь стало очевидным то, что все три объекта необходимы для вызова метода Process. Последняя версия класса OrderProcessor продвигает свои предусловия через систему типов. Вы даже скомпилировать клиентский код не сможете до тех пор, пока не передадите аргументы в конструктор и метод (сюда можно передать null, но это другая история).
Заключение
Service Locator — антипаттерн в статически типизированных, объектно-ориентированных языках, поскольку он нарушает инкапсуляцию. Причина заключается в том, что данный антипаттерн скрывает предусловия для корректного использования объекта.
Если вам требуется доступное введение в инкапсуляцию, вы можете посмотреть мой курс Encapsulation and SOLID на Pluralsight.com. Если вы хотите более подробно изучить Dependency Injection, вы можете почитать мою книгу (получившую награду) Dependency Injection in .NET.
Комментарии (72)
garex
03.11.2015 08:52В данном примере другой антипаттерн показан — чтобы использовать В надо дернуть/засетить/заинжектить А. К сервис-локатору это никак не относится.
Сервис локатор отвечает за то, чтобы вернуть обработчик заказов и здесь уже ответственность либо самого локатора, чтобы такой обработчик был у него создан, либо ответственность того, кто помещает в сервис локатор этого обработчика — design by contract.
Сервис-локатор похож скорее на один большой глобальный god-mode объект. Вот здесь его явный минус. Тот же code-completion можно «залочить» с помощью интерфейса который в одном себе отнаследует интерфейс каждого метода и тогда будет хотя бы видно, какие сервисы данный локатор предоставляет. Единственный минус — при очередном изменении надо будет это повторять, да и не всем о всех надо знать.m_z
03.11.2015 09:37> К сервис-локатору это никак не относится.
Если обработчик заказов использует другой сервис, например склад сервис, то согласно паттерну SL он должен будет сделатьLocator.Resolve<IWarehouseService>()
.
Аргументы в статье валидные, но и были уже давно описаны Фаулером в популярной статье, Inversion of Control Containers and the Dependency Injection pattern.
EngineerSpock
03.11.2015 09:43В данном примере другой антипаттерн показан — чтобы использовать В надо дернуть/засетить/заинжектить А.
В вашем коде объекты не зависят от других объектов?garex
03.11.2015 10:10В любом коде они зависят. Но если мне объект предоставляет интерфейс АйСделатьХорошо, то этот объект ДОЛЖЕН по своему интерфейсу уже делать мне хорошо. Я — клиент и я не должен зависеть от деталей реализации того, кто за интерфейсом. Иначе смысл теряется в-принципе в интерфейсах, DI и SL.
EngineerSpock
03.11.2015 12:09-1Если объекту А для предоставления своих услуг нужен другой объект B, и только объект A знает как воспользоваться B так, чтобы предоставить свои услуги корректно, то что в этом не так?
garex
03.11.2015 12:14Клиенту объекта А это должно быть фиолетово ибо инкапусуляция в том и есть, что интерфейс уже гарантирует, что с той стороны всё будет работать.
Проблемы имплементации — это её личные проблемы и они не должны выходить за интерфейс. Если получается так, что клиенту объекта А надо задумываться над деталями имплементации, то уже что-то не так в архитектуре и точно не сервис-локатор виноват, а автор такого объекта/интерфейса, который не может гарантировать себя.
lair
03.11.2015 12:15+3Если вы программируете на интерфейсах, и в вашем приложении используется инверсия зависимостей, вы, скорее всего, практически нигде не будете создавать объект, реализующий какой-то интерфейс. Таким образом, проблема «надо что-то заинжектить» вас коснуться и не должна — вы получаете готовый работоспособный объект.
AEP
03.11.2015 09:07-2Почему-то слова Dependency Injection в статье расположены слишком далеко от примера с конструктором
vedenin1980
03.11.2015 12:00Я может чего-то не понимаю, но разве любые Service Locator, DI, аспект-ориентированное программирование, почти любая ORM в теории и так нарушают инкапсуляцию просто по факту своего существования, там как практически все они построены на рефлексии и изменении значений в том числе и приватных полей классов (что в теории считается явным нарушением инкапсуляции)?
EngineerSpock
03.11.2015 12:05Поэтому теперь надо повсюду, даже если вы не пишите монструозный EF, использовать рефлекшн для изменения приватных полей и прикрываться тем, что существуют популярные фрэймворки, которые так делают?
vedenin1980
03.11.2015 13:17Я этого не говорил, рефлекшен кроме orm и т.п. библиотек зло почти всегда. Я говорил о том, что говорить что Service Locator нарушает инкапсуляции просто потому что у него такой принцип работы, как и у DI и AOP в общем случае.
lair
03.11.2015 12:18+3Вы чего-то не понимаете. И Service Locator, и Dependency Injection можно реализовать вообще не используя рефлексию (просто будет много однотипного кода). Если же рефлексию разрешить, то выясняется, что для решения типовых задач инверсии зависимостей никакого нарушения инкапсуляции (в значении «обхода областей видимости») не нужно: параметры конструктора — публичны, свойства, через которые делается вбрасываение — тоже публичны, ну и так далее.
vedenin1980
03.11.2015 12:59Можно вопрос не в этом. Не знаю как в Net, но в Java почти любой DI, аспекто-ориентированный фреймворк и ORM может работать с приватными полями, причем практически любого класса, даже если автор класс на это вообще не рассчитывал. Например, можно взять и аспектами изменить работу всех методах всех классов, добавив логирования к любому не финальному методу. С одной стороны, да это очень удобно, с другой стороны, о теоретической «чистой» инкапсуляции тут уже сложно говорить.
lair
03.11.2015 13:05+3А давайте отделять мух от котлет?
Во-первых, давайте сразу выкинем из обсуждения AOP и ORM — они не имеют отношения к статье. Остаются DI и SL.
Для того, чтобы реализовать DI, все, что нужно — это умение создавать объекты. Более того, это (вместе с трекингом жизненного цикла) более-менее и описывает всю необходимую функциональность DI-фреймворка. Если кто-то решил встроить туда AOP и для этого лезет в инкапсулированную область — это личное дело того, кто решил так сделать, к DI (или SL) как шаблону это отношения не имеет.
Поэтому нет, утверждение «любые Service Locator, DI, [...] и так нарушают инкапсуляцию просто по факту своего существования» — неверно.EngineerSpock
03.11.2015 13:37Спасибо, поскольку вы всё доступно объяснили, от себя комментировать ничего не буду.
symbix
03.11.2015 13:20+2Я очень не люблю инъекцию в приватные поля в стиле Autowired.
При constructor injection сразу видно все зависимости класса. И сразу очевидны случаи, когда их слишком много. С приватными же полями очень легко внести 20 зависимостей и этого не заметить.Lure_of_Chaos
03.11.2015 13:47-2> С приватными же полями очень легко внести 20 зависимостей и этого не заметить.
Это и минус, и плюс инъекции. С одной стороны да — не хочется иметь много зависимостей. С другой — не хочется заботиться о них вообще.
Но по мне, так лучше 200 мелких зависимостей, чем один монолит. А потому, что 200 зависимостей сделают ровно ту задачу, которая от них требуется, чем монолит, который на 70% использоваться никогда не будет.
В итоге, вопрос упирается в грамотную архитектуру.symbix
03.11.2015 13:54+2200 зависимостей — это по факту тот же монолит, только раздробленный в рандомную окрошку. Реюзабельность и тестируемость на нуле. В чистом виде самообман.
Заботиться надо не о зависимостях, а об архитектуре. Аббревиатура SOLID придумана не просто так. А если следовать этим принципам, много зависимостей — это четкий признак нарушения как минимум SRP.Lure_of_Chaos
03.11.2015 14:08В рандомную или нет — опять же, зависит от продуманности архитектуры. Можно и нашпиговать зависимостями так, что их никак не перекомпоновать, но разве это беда инъекции?
Много зависимостей — как и много параметров функции, требует перепланирования архитектуры. Но, в целом, в большом проекте может быть ооочень много транзитивных зависимостей.
И особенно четко об этом сигнализирует круговая транзитивная зависимость (circular dependencies).symbix
03.11.2015 14:16Так никакой разницы между инъекцией в конструктор и инъекцией в приватные поля тут не будет. За тем исключением, что с инъекциями в конструктор намного очевиднее, насколько все запущено. Особенно полезно на code review: добавление приватного поля с autowired-аннотацией в диффе особо ни о чем не сигнализирует, а аргументы конструктора — вот они, все 25 :)
Lure_of_Chaos
03.11.2015 14:29Все же есть — есть явное и неявное. Кроме того, аргументы конструктора требуются немедленного разрешения, при создании экземпляра, а инъекция в поля может быть отложенной, если не использовать особых хитрых техник проксирования.
Поскольку все плюсы и минусы есть, и они более или менее очевидны, не вижу состава спора — пусть кто-то выбирает себе по вкусу. Опять же, все зависит от трактовки «зависимость».symbix
03.11.2015 17:20Насчет отложенной — многие реализации DI умеют делать такие прокси.
По вкусу — разумеется, тут больше о том и речь.
Bonart
04.11.2015 02:33+1Кроме того, аргументы конструктора требуются немедленного разрешения, при создании экземпляра, а инъекция в поля может быть отложенной, если не использовать особых хитрых техник проксирования.
Второе — антипаттерн, так как сконструированный объект в данном случае не является полноценным.
А первое — просто неверно. И не надо никаких «хитрых техник», достаточно правильно указать тип аргумента:
- Task<T> — зависимость может долго инициализироваться и может быть еще не готова к с использованию на момент конструирования
- Lazy<T> — зависимого ресурса на момент конструирования может вообще не быть, но после обращения к Value — он обязательно будет
- Func<T> — можно сделать себе столько экземпляров ресурса, сколько нужно
- Func<P...,T> — можно создавать параметризованные экземпляры ресурса.
- Owned<T> — можно детерминированно очищать неуправляемые ресурсы, требуемые реализацией зависимости прямо или косвенно (уже через свои зависимости)
- И все остальное что может прийти в голову вам или другому разработчику
mird
05.11.2015 12:38+2
— можно сделать себе столько экземпляров ресурса, сколько нужноFunc<T>
Вообще-то тут нет гарантии, что будет много экземпляров а не один. Func единственное что гарантирует, что при вызове вернется объект типа T, а будет ли при каждом вызове новый объект или же каждый раз будет возвращаться один и тот же сказать однозначно нельзя.Bonart
05.11.2015 13:05+1Если этот один экземпляр будет вести себя иначе, чем ожидается клиентом от многих, то это нарушение соглашения со стороны компоновщика.
Когда клиент действительно ждет ровно один экземпляр — достаточно Lazy.
А так чисто технически любая фабрика может каждый раз одно и то же возвращать, но клиент ни в коем случае не должен на это закладываться: это не его проблемы, а инжектора.mird
05.11.2015 15:01Клиент не должен закладываться и на то что там каждый раз новый экземпляр тоже. То есть нужно получить экземпляр, использовать его в соответствии с интерфейсом и ничего больше с ним не делать (не освобождать ресурсы, не диспозить и т.п.).
Bonart
05.11.2015 15:22Клиент не должен закладываться и на то что там каждый раз новый экземпляр тоже.
Клиент собственно не то что закладывается, а требует определенную семантику.
Для освобождения ресурсов есть Owned и его эквиваленты.
Для единственного ресурса с отложенным запросом есть Lazy.
Но если в конструкторе указан параметр типа Func, то это подразумевает семантику фабрики, а не синглтона. В частности, если интерфейс включает состояние, то для результатов разных вызовов Func оно должно изменяться независимо. Если просят фабрику, а инжектится синглтон, то это и ответственность, и проблема только того кто инжектит.lair
05.11.2015 15:26+2Но если в конструкторе указан параметр типа Func, то это подразумевает семантику фабрики, а не синглтона.
Почему? Скажем, мы черезFunc
инжектилиHttpContext
, чтобы когда пользователю он нужен, он доставался текущий (при этом сам класс-потребитель был синглтон). Как следствие, между двумя вызовами он мог быть один, а мог быть разный.
В норме, за исключениемOwned
, класс-потребитель не должен делать никаких предположений о жизненном цикле вброшенной в него зависимости.
(фабрика, кстати, тоже не обязана отдавать каждый раз новый экземпляр)Bonart
05.11.2015 16:27-1Скажем, мы через Func инжектили HttpContext, чтобы когда пользователю он нужен, он доставался текущий (при этом сам класс-потребитель был синглтон). Как следствие, между двумя вызовами он мог быть один, а мог быть разный.
А этот контекст имел изменяемое потребителем состояние? Если не имел, то ваша реализация вполне нормальная, а вот если имел, то получается нарушение инкапсуляции — у потребителя возникали обязательства по работе с контекстом, которые никак не следовали из типа зависимости.
(фабрика, кстати, тоже не обязана отдавать каждый раз новый экземпляр)
Не обязана, все верно. Но то, что она возвращает, обязано вести себя так, как будто каждый вызов возвращает отдельный экземпляр.
В норме, за исключением Owned, класс-потребитель не должен делать никаких предположений о жизненном цикле вброшенной в него зависимости.
Я полагаю, не совсем так. Скорее потребитель не обязан делать никаких предположений о зависимости, кроме основанных на ее публичном интерфейсе. Т.е. если Owned — надо грохать ручками, если Func — можно повторно дергать для получения новой реализации. Собственно Func с параметрами делает эту семантику еще более явной.lair
05.11.2015 16:33+1А этот контекст имел изменяемое потребителем состояние?
Конечно, имел, как и полагаетсяHttpContext
.
Но то, что она [фабрика] возвращает, обязано вести себя так, как будто каждый вызов возвращает отдельный экземпляр.
Почему?
если Func — можно повторно дергать для получения новой реализации.
Почему же новой?
Собственно Func с параметрами делает эту семантику еще более явной.
Угу. Autofac (про который вы, как мне кажется, знаете), явно пишет:
However, if you register an object as
SingleInstance()
and call theFunc<X, Y, B>
to resolve the object more than once, you will get the same object instance every time regardless of the different parameters you pass in.
Наверное, если бы они не предполагали семантики «создай фабрику, которая возвращает синглтон», они бы запретили ее реализацию?
Bonart
06.11.2015 11:48-1Конечно, имел, как и полагается HttpContext.
Тогда вы имеете нарушение инкапсуляции. Ибо если получить контекст через делегат дважды, то изменение первого полученного может поменять второй… а может и не поменять. В результате объект-потребитель получает требования от Composition Root, чего быть не должно.
Почему же новой?
Да потому, что если новая реализация не нужна, то можно, не вызывая повторно делегат, просто сохранить старую и пользоваться ей.
Наверное, если бы они не предполагали семантики «создай фабрику, которая возвращает синглтон», они бы запретили ее реализацию?
Я считаю данное конкретное решение ошибкой дизайна отличного DI-контейнера Autofac. Лично мне и моим коллегам по работе оно не раз создавало проблемы и никогда не помогало. Игнорирование переданных параметров во всех вызовах, кроме первого по времени — очень сложно придумать что-нибудь хуже,lair
06.11.2015 11:52+2В результате объект-потребитель получает требования от Composition Root, чего быть не должно.
Нет, объект-потребитель всегда получает то, что хотел: текущийHttpContext
. И если мы в одном поменяли, а другой увидел — значит, они в одном контексте, а если поменяли, а не увидел — значит, в разных. И это — правильное, ожидаемое поведение.
Да потому, что если новая реализация не нужна, то можно, не вызывая повторно делегат, просто сохранить старую и пользоваться ей.
Кроме «старой/новой» еще есть дихотомия «текущий/устаревший», и вот она интереснее.
Понимаете, объект за псевдо-фабрикой — это не обязательно сервис, это может быть контекст (а у них другое поведение), это может быть сущность (у них тоже свое поведение). Например, за «фабрикой» может быть identity map, который на каждый вариант параметра возвращает свой объект, но далее для того же варианта будет тот же объект.Bonart
06.11.2015 12:40Нет, объект-потребитель всегда получает то, что хотел: текущий HttpContext.
А, вот оно в чем дело. Но тогда лучше не Func, а IObservable или IChangeable какой-нибудь — здесь, в отличие от делегата, и семантика доступа именно к текущему контексту понятна, и момент смены контекста наблюдаем.
Понимаете, объект за псевдо-фабрикой — это не обязательно сервис, это может быть контекст (а у них другое поведение), это может быть сущность (у них тоже свое поведение). Например, за «фабрикой» может быть identity map, который на каждый вариант параметра возвращает свой объект, но далее для того же варианта будет тот же объект.
Это я с самого начала говорил: за Func может стоять что угодно, но обязанное обеспечить семантику фабрики. Т.е. если между параметризованными объектами с одинаковыми параметрами разницы нет никакой, то да, кеширование рулит.lair
06.11.2015 13:32А, вот оно в чем дело. Но тогда лучше не Func, а IObservable или IChangeable какой-нибудь — здесь, в отличие от делегата, и семантика доступа именно к текущему контексту понятна, и момент смены контекста наблюдаем.
А мне не надо видеть момент смены, мне достаточно получить текущий.
Это я с самого начала говорил: за Func может стоять что угодно, но обязанное обеспечить семантику фабрики.
Для контекстов нет семантики фабрики. Даже для доставания объектов из реестра нет семантики фабрики.Bonart
06.11.2015 13:51А мне не надо видеть момент смены, мне достаточно получить текущий.
Для этого тем более Func не нужен.
Для контекстов нет семантики фабрики. Даже для доставания объектов из реестра нет семантики фабрики.
И Func в обоих случаях оказывается лишним.lair
06.11.2015 13:52Для этого тем более Func не нужен.
И Func в обоих случаях оказывается лишним.
А как мне это сделать безFunc
?
mird
06.11.2015 12:26Семантика у вас не в фабрике, а в типе получаемого из фабрики объекта. То есть наименование типа объекта должно говорить что это
— сервис и тогда нам не важно что внутри его состояния и мы его состояние снаружи менять не должны;
— контекст и тогда мы можем менять его состояние и рассчитывать, что его состояние сохранится пока контекст не переключится
— объект данных и тогда мы вправе рассчитывать на все время новый проинициализированный объект.Bonart
06.11.2015 12:51пока контекст не переключится
Вот с этого места поподробнее, пожалуйста.
Я для «переключаемых» сущностей использую вот это
public interface IChangeable<T> { T Value { get; } IObservable<T> Changed { get; } }
И никакого Func тут не нужно от слова совсем.
То есть наименование типа объекта должно говорить что это
Так Func и есть наименование типа, которое ясно и четко говорит что это фабрика (семантически).
В вашей трактовке помимо Func надо смотреть еще и на тип-параметр, что усложняет и реализацию, и использование.
Вдобавок это противоречит самому понятию generic, так как ваш Func имеет разную семантику для разных типов-параметров.mird
06.11.2015 13:23Так Func и есть наименование типа, которое ясно и четко говорит что это фабрика (семантически).
В вашей трактовке помимо Func надо смотреть еще и на тип-параметр, что усложняет и реализацию, и использование.
Вдобавок это противоречит самому понятию generic, так как ваш Func имеет разную семантику для разных типов-параметров.
Простите, но Func это семантически не фабрика, а функтор. И не надо в него вкладывать семантику, которой в нем нет.
Генерик параметр в нем говорит что этот функтор возвращает объект такого типа. Больше он ничего семантически не гарантирует.Bonart
06.11.2015 13:59В принципе да, логично завести аналогичный тип Factory и использовать его вместо Func.
Но конкретно для разрешения зависимостей нет ничего плохого в том, чтобы подразумевать фабрику.
Иначе для каждого конкретного случая приходится вводить дополнительные ad-hoc требования. Здесь переключаем, здесь создаем, здесь отдаем одно и то же всегда и т.п., а Func «вообще» снова не при делах.
mird
05.11.2015 16:41+1Вот это вот
если Func — можно повторно дергать для получения новой реализации
непонятно откуда взялось. На основании чего вы вкладываете в делегат семантику отдачи каждый раз нового объекта мне не понятно.
mird
05.11.2015 16:29+2Нет. Func не закладывает такой семантики. Равно как и Lazy не закладывает семантики singleton, на самом деле. Просто класс потребитель сервиса не должен ожидать от поставщика этого сервиса никакого поведения, отличного от описанного контрактом (интерфейсом). То есть он не должен вообще думать о внутреннем состоянии сервиса, который ему сгенерировала фабрика. Все о чем он должен думать — это что сервис выполняет обещанные контрактом операции.
Bonart
06.11.2015 11:58-1Func не закладывает такой семантики.
Без такой семантики Func просто не нужен по построению.
Равно как и Lazy не закладывает семантики singleton, на самом деле.
Разные обращения к свойству Value могут вернуть разные (семантически) объекты? На мой взгляд это антипаттерн, усложняющий жизнь по обе стороны компонента.
То есть он не должен вообще думать о внутреннем состоянии сервиса, который ему сгенерировала фабрика. Все о чем он должен думать — это что сервис выполняет обещанные контрактом операции.
Я не про внутреннее состояние, а вовсе даже про внешнее. Когда интерфейс имеет изменяемое состояние, то разница между возвратом нового и старого экземпляров при каждом вызове Func имеет значение и от этого никуда не деться. В примере с http-контекстом, если сделать два вызова делегата и начать менять состояние обоих полученных контекстов, разница между семантикой синглтона и фабрики всплывет на первом же юнит-тесте.mird
06.11.2015 12:36Без такой семантики Func просто не нужен по построению.
Чего это не нужен? В этом случае мы перекладываем ответственность по управлению инстансами нашего сервиса на эту фабрику. И явно говорим, что нам все равно какой экземпляр нам будет возвращен каждый раз когда мы вызовем фабрику.
То есть мы не привязываем наш класс потребитель к конкретному экземпляру который вбросили в конструктор, а получаем в каждом месте вызова фабрики тот экземпляр, который DI контейнер посчитает нужным нам отдать. А DI кнтейнер пусть управляет временем жизни этого экземпляра так как он настроен.
Я не про внутреннее состояние, а вовсе даже про внешнее. Когда интерфейс имеет изменяемое состояние, то разница между возвратом нового и старого экземпляров при каждом вызове Func имеет значение и от этого никуда не деться. В примере с http-контекстом, если сделать два вызова делегата и начать менять состояние обоих полученных контекстов, разница между семантикой синглтона и фабрики всплывет на первом же юнит-тесте.
А это определяется семантикой которую в себе несет объект. Если он (его название) содержит в себе семантику контекста — он должен сохранять внешнее состояние до смены контекста. Если у него в семантике заложен Cache, соответственно должен вести себя как кеш и т.п.Bonart
06.11.2015 13:02У вас Func<TВася> и Func<TПетя> имеют разную семантику?
Это явно не то, что можно ожидать от любого обобщенного типа, включая Funcmird
06.11.2015 13:25+2Func<TВася> и Func<TПетя> имеют одинаковую семантику. Оба они возвращают объект типа, соответствующего генерик параметру. Ничего сверх того они не обещают.
lair
06.11.2015 13:35Вас же не удивляет, что
Func<T,bool>
иFunc<T,string>
могут иметь разную семантику? Более того, что тот жеFunc<T,bool>
вWhere
иCheckBoxFor
имеет разную семантику?Bonart
06.11.2015 14:05Там оно в разрешении зависимостей не участвует и семантика определяется самим методом (Where, CheckBoxFor)
Паттерн DI, напротив, тяготеет к отделению семантики параметров-зависимостей от конкретного конструктора.
Да, вы можете и обойтись и без этого, но с зависимостями, чья семантика однозначно определяется типом, работать ИМХО проще.lair
06.11.2015 14:20+2Да, вы можете и обойтись и без этого, но с зависимостями, чья семантика однозначно определяется типом, работать ИМХО проще.
Семантика в первую очередь определяется типом самой зависимости, а не вторичного отношения (Func, Owned и т.д.), построенного вокруг нее.
mird
06.11.2015 14:21Я, если мне нужна конкретная семантика завожу специальный тип, а вот используя Func никакой дополнительной семантики не влкадываю.
Lure_of_Chaos
03.11.2015 13:42Собственно, нет никакого нарушения инкапсуляции в транзитивной зависимости. А полностью избавиться от всех зависимостей не получится даже теоретически, потому и используются интерфейсы, чтобы только минимизировать эту зависимость.
Возможно, полностью приблизиться к идеалу можно только в динамично-типизированных языках с обменом «сообщений», как, например, в smalltalk, но это уже не тема топика.symbix
03.11.2015 14:00Избавиться от транзитивных зависимостей можно, например, с помощью Command Bus.
Чем тут поможет динамическая типизация, я не понял.EngineerSpock
03.11.2015 14:05Рассмотрим WPF-app с MVVM в качестве UI-паттерна.
Два вопроса:
1. Как вы считаете, если ViewModel требует в конструкторе две-три зависимости и сама их не использует, а только конструирует Model с помощью них, имеется ли здесь проблема транзитивных зависимостей?
2. Если на первый вопрос ответ положительный, то что такое Command Bus и как это поможет в таком случае?symbix
03.11.2015 14:10Конкретно WPF я ни разу в жизни не видел (ой, это .net хаб же!), но по паттернам понятно, о чем речь.
Нет, в данном случае не думаю, что это проблема. Я скорее имел ввиду application/domain model layers.EngineerSpock
03.11.2015 14:22Ясно. Где порекомендуете почитать про Command Bus? Или просто погуглить?
symbix
03.11.2015 15:53В принципе, погуглить — достаточно, вроде на первой странице выдача приличная.
Наиболее интересно это все выглядит при использовании совместно с CQRS. Тут могу порекомендовать — тем более, раз уж мы в дотнет-хабе — Microsoft-овскую книгу «Exploring CQRS and Event Sourcing» [1]: несмотря на обильную рекламу Azure, тема вполне неплохо раскрыта :)
[1] http://www.microsoft.com/en-us/download/details.aspx?id=34774
Throwable
04.11.2015 12:42В Java шаблон ServiceLocator (java.util.ServiceLoader) используется очень часто, когда нужно подключать различные имплементации публичного API в зависимости от classpath.
С одной стороны это универсальный способ без лишних заморочек использовать различные сервис-провайдеры. Проблемы начинаются когда нужно сконфигурировать конкретный провайдер, ибо для этого не придумано хорошего стандартного способа. Часто это делается либо через system properties, либо через внешний файл конфигурации.
VolCh
Service Locator нарушает инкапсуляцию класса, если он используется непосредственно в классе. Но даже в данном примере, что нам мешает использовать Service Locator для определения параметров вызова конструктора?
И не понял к чему уточнение про статически типизированные? На том же PHP я избегаю зависимости моих классов от Service Locator (даже явной, через параметры), предпочитая получать в конструкторе или сеттерах конкретные инстансы классов/интерфейсов.
В любом случае, даже неявная (с точки зрения публичного API) зависимость класса от Service Locator по-моему лучше чем захардкоженная зависимость от конкретных классов или, пускай, интерфейсов как в примере.
greabock
В целом, внедрения через аргументы вполне достаточно. И коль уж речь зашла о PHP…
Этим я хотел сказать, что вы всегда вызываете авторезолвер явным образом, и вы должны прекрасно понимать, что это не тоже самое, что:
И что зависимости таки будут разрешены. И уже из этого строить логику.
Автор же оригинальной статьи и вовсе притянул проблему за уши, используя сервис-локатор не по назначению.
symbix
Любой DI неизбежно включает в себя возможности сервис-локатора. Отличие больше в паттерне использования.
Если вы в Laravel пишете app(Some::class) только в Service Providers (впрочем, там, скорее, будет $this->app) — это DI, если пишете где попало — это Service Locator. Ларавеловские «фасады» — это, кстати, тот же service locator с теми же проблемами.
EngineerSpock
Не знаю как в PHP, могу сказать за WPF и ASP.NET. В такого рода приложениях мы находим централизованное место и через него и только через него происходит resolve и больше нигде!
symbix
В PHP точно так же, от языка тут вряд ли что-то зависит.
EngineerSpock
Я тоже так думаю. Это называется принципом Голливуда: «не звоните нам, мы сами вам позвоним». Хотя я не очень люблю эту метафору применительно к DI с помощью IoC.
EngineerSpock
Мы решили проблему, перенеся ответственность для клиента и сделали зависимости очевидными. Если клиент будет использовать Service Locator — право клиента, значит сам себе злобный буратино.
Извините, не совсем понял. Чем отличается ваш подход от инжектирования зависимостей через конструктор?
Если это reusable business-object, то чем будет лучше сокрытие зависимостей от клиента?
lair
Тем, что это означает, что вы вызываете конструктор напрямую. В приложении, построенном на инверсии зависимостей, это очень странно.
VolCh
Конструктор вызывает сам же Service Locator :)
lair
Тогда что-то вызвало сервис-локатор для получения этого объекта. Теперь все проблемы, описанные в статье, у вызывающего.
VolCh
Всегда что-то будет вызывать или сервис-локатор, или new. Но мы можем волевым решением сгруппировать эти вызовы в нескольких строго определенных местах типа сервис-локатора и/или фабрик.
lair
Не совсем так.
Скажем, если у меня есть MVC (или WebAPI) приложение, то с прикладной точки зрения точкой входа является метод в классе контроллера. Соответственно, я хочу, чтобы контроллер получал свои зависимости через инъекцию в конструктор, потому что это явный контракт, и это удобно для тестирования.
Дальше инфраструктура предоставляет мне фабрику контроллеров, которую я могу заменить так, как мне удобно. Внутри этой фабрики я могу, скажем, обратиться к DI-контейнеру, и сказать «дай мне экземпляр контроллера». Это не будет вызовом сервис-локатора.
(но вообще, конечно, вы правы в том, что при построении приложения, основанного на инверсии зависимостей, вся логика по созданию объектов концентрируется в одном месте)