У меня интерфейс называется IContext
. Для целей этого не имеет значения, что он делает, за исключением следующего:
T GetService<T>();
Этот метод просматривает текущий DI-контейнер приложения и пытается разрешить зависимость. Я думаю, что он довольно стандартный.
В моем приложении ASP.NET MVC мой конструктор выглядит следующим образом.
protected MyControllerBase(IContext ctx)
{
TheContext = ctx;
SomeService = ctx.GetService<ISomeService>();
AnotherService = ctx.GetService<IAnotherService>();
}
Поэтому вместо того, чтобы добавлять несколько параметров в конструктор для каждого сервиса (потому что это будет очень раздражающим и трудоемким для разработчиков, расширяющих приложение), я использую этот метод для получения сервисов.
Теперь он чувствует себя не так . Однако, способ, которым я в настоящее время оправдываю это в своей голове, состоит в следующем - я могу издеваться над этим .
Я могу. Нетрудно создать макет IContext
для тестирования контроллера. Я должен был бы в любом случае:
public class MyMockContext : IContext
{
public T GetService<T>()
{
if (typeof(T) == typeof(ISomeService))
{
// return another mock, or concrete etc etc
}
// etc etc
}
}
Но, как я уже сказал, это неправильно. Любые мысли / оскорбления приветствуются.
источник
public SomeClass(Context c)
. Этот код вполне понятен, не так ли? В нем говорится,that SomeClass
зависит отContext
. Эээ, но подождите, это не так! Он зависит только от зависимости, которуюX
он получает из Context. Это означает, что каждый раз, когда вы вносите изменения,Context
они могут сломатьсяSomeObject
, даже если вы изменили толькоContext
sY
. Но да, вы знаете, что вы толькоY
не изменилисьX
, так чтоSomeClass
все в порядке. Но написание хорошего кода - это не то, что вы знаете, а то, что знает новый сотрудник, когда он смотрит на ваш код в первый раз.Ответы:
Наличие одного вместо множества параметров в конструкторе не является проблемной частью этого проекта . Пока ваш
IContext
класс - не что иное, как фасад службы , специально предназначенный для предоставления зависимостей, используемыхMyControllerBase
, а не общий локатор служб, используемый во всем коде, эта часть вашего кода, IMHO, в порядке.Ваш первый пример может быть изменен на
это не будет существенным изменением дизайна
MyControllerBase
. Хороший или плохой дизайн зависит только от того, хотите ли выTheContext
,SomeService
иAnotherService
всегда все инициализируются фиктивными объектами, или все из них с реальными объектамиТаким образом, использование только одного параметра вместо 3 в конструкторе может быть вполне разумным.
Проблема, которая является проблематичной, это
IContext
разоблачениеGetService
метода на публике. ИМХО, вам следует избегать этого, вместо этого оставляйте «фабричные методы» явными. Так будет нормально реализоватьGetSomeService
иGetAnotherService
методы из моего примера , используя локатор службы? ИМХО это зависит. ПокаIContext
класс продолжает использовать простую абстрактную фабрику для конкретной цели предоставления явного списка сервисных объектов, это ИМХО приемлемо. Абстрактные фабрики, как правило, представляют собой просто «склеивающий» код, который не должен сам тестироваться модулем. Тем не менее, вы должны спросить себя, в контексте таких методов, какGetSomeService
, действительно ли вам нужен локатор службы или явный вызов конструктора не будет проще.Так что будьте осторожны, когда вы придерживаетесь дизайна, в котором
IContext
реализация представляет собой просто обертку вокруг общедоступного универсальногоGetService
метода, позволяющего разрешать любые произвольные зависимости произвольными классами, тогда все применимо к тому, что @BenjaminHodgson написал в своем ответе.источник
GetService
метод. Рефакторинг к явно названным и типизированным методам лучше. Еще лучше было быIContext
четко изложить зависимости реализации в конструкторе.IValidatableObject
?Этот дизайн известен как Service Locator *, и мне он не нравится. Есть много аргументов против этого:
Сервисный локатор соединяет вас с вашим контейнером . Используя регулярное внедрение зависимостей (где конструктор явно прописывает зависимости), вы можете напрямую заменить ваш контейнер на другой или вернуться к
new
-expressions. С тобойIContext
это не реально возможно.Сервисный локатор скрывает зависимости . Как клиент, очень сложно сказать, что вам нужно для создания экземпляра класса. Вам нужно что-то вроде
IContext
, но вы также должны установить контекст, чтобы вернуть правильные объекты, чтобы сделатьMyControllerBase
работу. Это совсем не очевидно из подписи конструктора. При обычном DI компилятор говорит вам именно то, что вам нужно. Если у вашего класса много зависимостей, вы должны чувствовать эту боль, потому что это подтолкнет вас к рефакторингу. Сервисный локатор скрывает проблемы с плохим дизайном.Service Locator вызывает ошибки во время выполнения . Если вы позвоните
GetService
с неверным параметром типа, вы получите исключение. Другими словами, вашаGetService
функция не является полной функцией. (Итоговые функции - идея из мира FP, но в основном это означает, что функции всегда должны возвращать значение.) Лучше позволить компилятору помочь и сообщить вам, когда вы неправильно поняли зависимости.Сервисный локатор нарушает принцип подстановки Лискова . Поскольку его поведение варьируется в зависимости от аргумента типа, сервисный локатор можно рассматривать так, как будто он имеет бесконечное количество методов в интерфейсе! Этот аргумент подробно изложен здесь .
Сервисный локатор сложно проверить . Вы привели пример подделки
IContext
для тестов, и это хорошо, но, конечно, лучше не писать этот код в первую очередь. Просто внедрите свои поддельные зависимости напрямую, не заходя в локатор службы.Короче, просто не делай этого . Это кажется соблазнительным решением проблемы классов с множеством зависимостей, но в конечном итоге вы просто сделаете свою жизнь несчастной.
* Я определяю Service Locator как объект с помощью универсального
Resolve<T>
метода, который способен разрешать произвольные зависимости и используется во всей кодовой базе (а не только в корне композиции). Это не то же самое, что Service Facade (объект, который связывает некоторый небольшой известный набор зависимостей) или Abstract Factory (объект, который создает экземпляры одного типа - тип Abstract Factory может быть универсальным, но метод - нет) ,источник
MyControllerBase
он не связан с конкретным DI-контейнером, и это не «настоящий» пример для анти-паттерна Service Locator.GetService<T>
метод. Устранение произвольных зависимостей - это настоящий запах, который присутствует и корректен в примере ОП.GetService<T>()
Метод разрешает запрашивать произвольные классы: «Что делает этот метод, так это просматривает текущий DI-контейнер приложения и пытается разрешить зависимость. Я думаю, это довольно стандартно». , Я отвечал на ваш комментарий вверху этого ответа. Это 100% сервисный локаторМарк Симанн ясно изложил лучшие аргументы против анти-паттерна «Сервисный локатор», поэтому я не буду слишком вдаваться в то, почему это плохая идея - это учебное путешествие, которое нужно потратить на то, чтобы понять это самостоятельно (я также рекомендую книгу Марка ).
Хорошо, чтобы ответить на вопрос - давайте переформулируем вашу актуальную проблему :
Существует вопрос, который решает эту проблему в StackOverflow . Как уже упоминалось в одном из комментариев:
Вы ищете неправильное место для решения вашей проблемы. Важно знать, когда класс делает слишком много. В вашем случае я сильно подозреваю, что нет необходимости в «Базовом контроллере». На самом деле, в ООП почти всегда нет нужды в наследовании . Различия в поведении и общей функциональности могут быть полностью достигнуты за счет надлежащего использования интерфейсов, что обычно приводит к более качественному и инкапсулированному коду - и нет необходимости передавать зависимости конструкторам суперкласса.
Во всех проектах, над которыми я работал, где есть базовый контроллер, это было сделано исключительно для целей совместного использования удобных свойств и методов, таких как
IsUserLoggedIn()
иGetCurrentUserId()
. СТОП . Это ужасное злоупотребление наследством. Вместо этого создайте компонент, который предоставляет эти методы, и возьмите зависимость от него там, где он вам нужен. Таким образом, ваши компоненты останутся тестируемыми, и их зависимости будут очевидны.Помимо всего прочего, при использовании шаблона MVC я всегда рекомендовал бы тощие контроллеры . Вы можете прочитать больше об этом здесь, но суть паттерна проста: контроллеры в MVC должны делать только одно: обрабатывать аргументы, передаваемые инфраструктурой MVC, делегируя другие проблемы другому компоненту. Это опять-таки принцип единой ответственности в действии.
Это действительно помогло бы узнать ваш вариант использования, чтобы сделать более точное суждение, но, честно говоря, я не могу придумать ни одного сценария, в котором базовый класс предпочтительнее, чем обоснованные зависимости.
источник
protected
однострочных делегаций новым компонентам. Тогда вы можете потерять BaseController и заменить этиprotected
методы прямыми вызовами зависимостей - это упростит вашу модель и сделает все ваши зависимости явными (что очень хорошо в проекте с сотнями контроллеров!)Я добавляю ответ на этот вопрос, основываясь на вкладе всех остальных. Огромное спасибо всем. Сначала вот мой ответ: «Нет, в этом нет ничего плохого».
Ответ Дока Брауна «Service Facade» Я принял этот ответ, потому что то, что я искал (если ответ был «нет»), было некоторыми примерами или некоторым расширением того, что я делал. Он представил это, предположив, что А) у него есть имя и Б), возможно, есть лучшие способы сделать это.
Ответ Бенджамина Ходжсона «Сервисный локатор» Как бы я ни ценил полученные здесь знания, я не являюсь «Сервисным локатором». Это «Фасад обслуживания». Все в этом ответе правильно, но не для моих обстоятельств.
Ответы USR
Я рассмотрю это более подробно:
Я не теряю никаких инструментов и не теряю никакой «статической» печати. Фасад службы вернет то, что я настроил в своем контейнере DI, или
default(T)
. И то, что он возвращает, «напечатано». Отражение заключено в капсулу.Это конечно не "редкость". Поскольку я использую базовый контроллер, каждый раз, когда мне нужно изменить его конструктор, мне, возможно, придется менять 10, 100, 1000 других контроллеров.
Я использую инъекцию зависимости. В этом-то и дело.
И, наконец, комментарий Жюля об ответе Бенджамина: я не теряю никакой гибкости. Это мой служебный фасад. Я могу добавить столько параметров,
GetService<T>
сколько захочу, чтобы различать разные реализации, так же, как это делается при настройке контейнера DI. Так, например, я мог бы изменить,GetService<T>()
чтобыGetService<T>(string extraInfo = null)
обойти эту «потенциальную проблему».В любом случае, еще раз спасибо всем. Это было действительно полезно. Приветствия.
источник
GetService<T>()
Метод способен (попытка) Решимость зависимостей произвольной . Это делает его Сервисным Локатором, а не Фасадом Сервиса, как я объяснил в сноске моего ответа. Если бы вы заменили его небольшим наборомGetServiceX()
/GetServiceY()
методов, как предлагает @DocBrown, то это был бы Фасад.IContext
и внедрять это, и что особенно важно: если вы добавите новую зависимость, код все равно будет скомпилирован - даже если тесты не пройдут во время выполнения