Последнюю неделю (или даже чуть больше) мы занимаемся разбором кода пришедшего нам WPF-проекта, а также поиском и корректировкой архитектурных недоработок. Проекту где-то год-полтора, но работа над ним велась не слишком активно (долгое время — одним разработчиком), поэтому кодовая база не слишком велика.
Собственно, предыдущий топик родился как раз под влиянием беглого просмотра сложившейся ситуации. По прошествии недели мысли слегка улеглись, и хочется обобщить предложенные решения.
Итак, что у нас имеется? Допустим, есть модуль, который призван искать подключенные к компьютеру устройства — SD-карты с определенным содержимым, или приборы, подключенный по USB.
Как бы мы хотели видеть использование данного класса?
var detector = new DeviceDetector(); var devices = detector.Detect();
И если вторая строчка определяется контрактом класса и фактически так и выглядит, то вот конструктор класса DeviceDetector выглядит совсем по-другому:
public class DeviceDetector { public DeviceDetector(IComPortSearcher searcher, IUsbValidator validator, IDriveSearcher driveSearcher, IDeviceValidator validator) { } } var devices = detector.Detect();
С одной стороны — ничего плохого, наоборот. Четкое следование принципам инверсии зависимостей.
Однако простое создание класса становится несколько проблематичным. Отмечу, что IComPortSearche, Validator и прочие классы относятся в первую очередь к «внутренней кухне» детектора, и снаружи никогда не понадобятся (кроме как собственно в составе детектора).
В защиту конструкции служит еще и то, что при повсеместном использовании IoC-контейнеров с прямым созданием классов иметь дело практически не приходится. Подразумевая, что реализации всех этих интерфейсов уже зарегистрированы в контейнере, простой вызов var detector = _container.Resolve
Однако, хотелось бы поговорить о минусах такой конструкции.
- Первым минусом видится то, что для «самостоятельности» данного класса необходима публичная видимость не только самих интерфейсов, но и их реализаций — чтобы была возможность инстанциировать подобный класс.
Открытия реализаций, правда, можно избежать, используя шаблон Wpf-Prism шаблон IModule, который даёт возможность модулю зарегистрировать свои классы в контейнере при инициализации (вызове IModule.Initialize). Однако это ведет нас ко второму минусу - Завязка на использование IoC-контейнера и засорение его «лишними» типами. При попытке вручную инстанциировать все зависимости класса DeviceDetector вы проклянете всё на свете :) Поэтому использование IoC становится неизбежным, что привносит сложности в использовании вашего компонента — при ознакомлении неизбежен оверхэд на понимание, «что же это за интерфейсы и как с ними работать». Ну и даже если контейнер уже используется и вам с ним легко — вы вынуждены будете «засорить» его классами и интерфейсами из данного модуля. Это может быть проблематично, если в вашем приложении уже используется, допустим IDeviceValidator, однако с другой реализацией.
- Ну и последнее — использование данного класса выглядит лёгким и простым только пока он органично вписан в ваше приложение. Извлечь его из проекта и использовать где-то еще очень сложно — потому что ничто не заставляет вас структурировать его зависимости в одном месте и они могут быть разбросаны по всему приложению. Данный факт увеличивает вероятность переписывания модуля с нуля, если «извлечение и помещение в другой проект» доверят не вам, а коллегам, которые с кодом текущего проекта незнакомы. Ну хотя увеличение «завязки» проекта на себя для многих может минусом не являться :)
Аналогичную ситуацию можно наблюдать и с UserControl’ами. В силу активной пропаганды использования модели MVVM, тот факт, что View или Юзерконтрол представляют из себя совокупность нескольких классов (как минимум — View и ViewModel) стал уже абсолютно привычным. Соответственно, и для создания View недостаточно просто сказать Form.Controls.Add(new DeviceView()); (вспоминая WinForms :)), а надо как минимум разрезолвить еще и вью-модель (если у неё, вдруг, нет еще каких зависимостей :)): new DeviceView(new DeviceViewModel(new Service… ));. Впрочем, обычно этим также занимается IoC-контейнер, поэтому глаза этот факт не мозолит, и выглядит вполне прилично (не вызывает изначального отторжения и желания выпилить :)).
Однако данный факт также вредит модульности и четкому разделению, и при росте проекта грозит тем, что во всем многообразии UserControls, Views и прочего в едином неразделенном пространстве запутаться будет очень легко.
Объем поста начинает меня реально пугать, поэтому вариант решения будет очень скоро и отдельной записью :) Опять же, вполне возможно, что постановка проблемы многим будет и неинтересна.