Наибольшей проблемой при правильной реализации внедрения свойств является перемещение всех классов, имеющих зависимости, в корень компоновки.
Распространенная ошибка — требование, чтобы все зависимости имели конструктор с определенной сигнатурой. Это вытекает из желания обеспечить динамическое связывание, чтобы зависимости можно было бы определять во внешнем файле конфигурации и, следовательно, изменять без перекомпиляции приложения.
1 2 3 4 | var connectionString = ConfigurationManager.ConnectionStrings["SomeObject"].ConnectionString; var someRepositoryTypeName = ConfigurationManager.AppSettings["someRepositoryType"]; var someRepositoryType = Type.GetType(someRepositoryTypeName, true); var reposirory = (SomeRepository)Activator.CreateInstance(someRepositoryType, connectionString); |
Имея Type, вы можете создать его экземпляр с использованием класса Activator. Метод CreateInstance вызывает конструктор типа, поэтому необходимо передать корректные параметры конструктора, чтобы предотвратить генерацию исключительного события. Для этого применяется строка соединения. Если вы ничего не знаете о приложении, кроме фрагмента кода выше, вы должны сейчас удивиться тому, что строка соединения передается как аргумент конструктора в неизвестный тип. В таком случае вы имеете неявное требование, чтобы любая реализация SomeRepository имела конструктор с одним строковым параметром на входе.
Неявное ограничение, что конструктор должен иметь один строковый параметр, оставляет нам огромную степень свободы, так как мы можем закодировать в строке различную информацию и декодировать ее позже для использования в самых разных целях. А ведь мы можем и неправильно декодировать информацию! Представьте, наоборот, что параметры конструктора ограничены типами TimeSpan и int, и можете себе вообразить, насколько узки будут возможности вашей реализации.
Влияние
Если одна и та же зависимость требуется в нескольких классах, мы, возможно, захотим использовать один экземпляр зависимости между всеми требующими ее классами. Это возможно, только если мы внедрим этот экземпляр извне. Хотя мы можем написать в каждом из этих классов код для чтения информации о типе из файла конфигурации и применения Activator.CreateInstance для создания экземпляра корректного типа, мы так и не сможем совместно использовать экземпляр. Вместо этого мы получим несколько экземпляров одного и того же класса, что приводит к расходу лишней памяти.
То, что внедрение зависимостей позволяет нам совместно использовать один экземпляр между несколькими потребителями, не означает, что мы всегда должны так поступать. Разделение экземпляра сохраняет память, но может привести к появлению проблем взаимодействия, в частности, проблем с потоками. Вопрос — захотим ли мы разделять экземпляр? — тесно связан с концепцией жизненного цикла объекта.
Вместо введения неявных ограничений на способы создания объектов, следует реализовать корень компоновки таким образом, чтобы он мог работать с любым конструктором или методом фабрики, который мы поместим в него.
Рефакторинг в направлении внедрения зависимостей
Что делать с конструкторами компонентов, не имеющими ограничений, если необходимо динамическое связывание?
По существу, абстрактная фабрика таким образом превращается в абстрактный корень компоновки, который определяется в сборке отдельно от основного приложения. Хотя, в общем-то, это жизнеспособный подход, обычно оказывается, что намного проще использовать универсальный контейнер внедрения, который может делать то же самое, но без использования файлов конфигурации.
Антипаттерн Ограниченное конструирование реально применяется, только когда реализуется динамическое связывание, поскольку когда реализуется раннее (статическое) связывание, сам компилятор гарантирует, что мы никогда не сможем использовать неявные ограничения способов конструирования компонентов.
Заключение
Не стоит использовать данный антипаттерн, так как он влечет за собой много проблем. Вместо этого используете готовый контейнер внедрения зависимостей и радуйтесь жизни.