В своей статье хочу рассмотреть пример неправильного, на мой взгляд, использования Dependency Injection принципа и попробовать отыскать мотивацию для других разработчиков команды (а может и кому еще сгодится) писать новый код лучше, а также по мере сталкивания в рамках рабочих активностей с чужим кодом, написанным неграмотным образом, делать рефакторинг.
Итак, суть проблемы. На проекте мы используем OData WebApi и все контроллеры наследуются от базового, используют метод GetService из базового класса который вытягивает зависимости через статический класс ApiControllerScopeContextMediator.
public abstract class ODataControllerBase : ODataController { protected T GetService<T>() { return ApiControllerScopeContextMediator.GetService<T>(this); } } internal static class ApiControllerScopeContextMediator { internal static T GetService<T>(ApiController controller) { return (T) controller.Configuration.DependencyResolver.GetService(typeof (T)); } }
А в Global.asax конфигурируем подтягивание зависимостей для OData через StructureMap:
GlobalConfiguration.Configuration.DependencyResolver = new StructureMapDependencyResolver(container);
Во всех action у контроллеров повсеместно используется метод GetService, как, например, здесь:
public class DisconnectedAppsController : ODataControllerBase { public IHttpActionResult Get() { var query = GetService<IQuery<IQueryable<DisconnectedAppDomain>, DisconnectedAppFilter>>(); } }
Но почему? Ведь можно было бы просто использовать constructor injection:
public DisconnectedAppsController(IQuery<IQueryable<DisconnectedAppDomain>, DisconnectedAppFilter> query){ _query = query; }
Так что же все-таки: «Таити, Таити» (Constructor Injection) или «нас и здесь неплохо кормят» (GetService)?
Какие проблемы с этим кодом я вижу:
- Контроллеры нельзя покрыть unit тестами (без инициализации IoC контейнера)
- Согласно принципу KISS нам не нужна дополнительная сложность, а согласно YAGNI дополнительная завязка на System.Web.Http.Configuration.DependencyResolver, которая вылезет нам боком если мы захотим перейти с MVC5 на MVC6, у которого в корне изменится работы с Dependency Injection, а ApiController в бета версии уже лишился Configuration
- Взглянув на класс мы не видим явно все зависимости класса
- Использование GetService у DependencyResolver — это по сути использование реализации IoC при помощи Service Locator, что является анти-паттерном
- а Service Locator нарушает принципы SOLID
- Мы нарушаем фундаментальный принцип ООП инкапсуляцию
Какие аргументы «против» мне довелось услышать:
- В идеале контроллеры должны быть слишком тупы, чтобы их покрывать тестами, нам это никогда не понадобится, если и покрывать их, то интеграционными тестами
- Отказываться от текущей реализации основываясь на ссылках в гугле слишком идеалистично, это не принесет пользы
- Удобно для разработчика, меньше кода писать
- Изменение кода, который используется подобным образом во многих местах, внесет энтропию в проект
- Метод YAGNI в другой плоскости — а зачем нам менять что-то, если нет очевидной ежеминутной выгоды
- Constructor injection в контроллерах банально неудобен
Пару лет назад прочитал книгу Марка Симана «Dependency Injection». Сижу и думаю, так что у меня с DI: любовь или брак по расчету?
Использованные материалы:
Mark Seeman «Dependency Injection»
Mark Seeman’s blog
Microsoft MVC6 github open source project
SOLID wiki page
YAGNI wiki page
KISS wiki page
ссылка на оригинал статьи http://habrahabr.ru/post/271853/
Добавить комментарий