Абстракция - это хорошо, но важно помнить, что в какой-то момент что-то должно знать одну или две вещи о вещи или двух, иначе мы просто будем иметь кучу красивых абстрагированных легос, сидящих на полу вместо того, чтобы их собирали в дом.
Инверсия управления / внедрение зависимостей / flippy-dippy-upppy-down-независимо от того, что мы называем этим на этой неделе контейнер, такой как Autofac , действительно может помочь в этом вместе.
Когда я собираю приложение WinForms, я обычно получаю повторяющийся шаблон.
Я начну с файла Program.cs
, который настраивает контейнер Autofac, а затем извлекает из него экземпляр MainForm
и показывает MainForm
. Некоторые люди называют это оболочкой, рабочим пространством или рабочим столом, но в любом случае это «форма», которая имеет строку меню и отображает либо дочерние окна, либо дочерние пользовательские элементы управления, а когда она закрывается, приложение закрывается.
Далее - вышеупомянутый MainForm
. Я делаю базовые вещи, такие как перетаскивание некоторых SplitContainers
и MenuBar
с и так далее в визуальном конструкторе Visual Studio, а затем начинаю фантазировать в коде: мне будут вводить определенные ключевые интерфейсы в Конструктор MainForm
, так что я могу использовать их, чтобы моя MainForm могла управлять дочерними элементами управления без особого знания о них.
Например, у меня может быть интерфейс IEventBroker
, который позволяет различным компонентам публиковать или подписываться на «события», такие как BarcodeScanned
или ProductSaved
. Это позволяет частям приложения реагировать на события в слабосвязанной форме, не полагаясь на подключение традиционных событий .NET. Например, EditProductPresenter
, который идет вместе с моим EditProductUserControl
, может сказать this.eventBroker.Fire("ProductSaved", new EventArgs<Product>(blah))
, а IEventBroker
проверит свой список подписчиков на это событие и вызовет их обратные вызовы. Например, ListProductsPresenter
может прослушивать это событие и динамически обновлять ListProductsUserControl
, к которому оно прикреплено. Конечным результатом является то, что если пользователь сохраняет продукт в одном пользовательском элементе управления, ведущий другого пользовательского элемента управления может реагировать и обновлять себя, если он оказывается открытым, без какого-либо элемента управления, который должен знать о существовании друг друга, и без MainForm
необходимость организовать это событие.
Если я разрабатываю приложение MDI, у меня может быть MainForm
, реализующий интерфейс IWindowWorkspace
, который имеет методы Open()
и Close()
. Я мог бы внедрить этот интерфейс в мои различные докладчики, чтобы они могли открывать и закрывать дополнительные окна, не зная непосредственно о MainForm
. Например, ListProductsPresenter
может потребоваться открыть EditProductPresenter
и соответствующий EditProductUserControl
, когда пользователь дважды щелкает строку в сетке данных в ListProductsUserControl
. Он может ссылаться на IWindowWorkspace
- что на самом деле является MainForm
, но ему не нужно это знать - и вызывать Open(newInstanceOfAnEditControl)
и предполагать, что элемент управления был как-то показан в соответствующем месте приложения. (Реализация MainForm
, предположительно, поменяет элемент управления где-нибудь на панели.)
Но как, черт возьми, ListProductsPresenter
может создать этот экземпляр EditProductUserControl
? Фабрики делегатов Autofac - это настоящая радость, поскольку вы можете просто вставить делегата в презентатор, и Autofac автоматически подключит его, как если бы он был фабрикой (псевдокод следует):
public class EditProductUserControl : UserControl
{
public EditProductUserControl(EditProductPresenter presenter)
{
// initialize databindings based on properties of the presenter
}
}
public class EditProductPresenter
{
// Autofac will do some magic when it sees this injected anywhere
public delegate EditProductPresenter Factory(int productId);
public EditProductPresenter(
ISession session, // The NHibernate session reference
IEventBroker eventBroker,
int productId) // An optional product identifier
{
// do stuff....
}
public void Save()
{
// do stuff...
this.eventBroker.Publish("ProductSaved", new EventArgs(this.product));
}
}
public class ListProductsPresenter
{
private IEventBroker eventBroker;
private EditProductsPresenter.Factory factory;
private IWindowWorkspace workspace;
public ListProductsPresenter(
IEventBroker eventBroker,
EditProductsPresenter.Factory factory,
IWindowWorkspace workspace)
{
this.eventBroker = eventBroker;
this.factory = factory;
this.workspace = workspace;
this.eventBroker.Subscribe("ProductSaved", this.WhenProductSaved);
}
public void WhenDataGridRowDoubleClicked(int productId)
{
var editPresenter = this.factory(productId);
var editControl = new EditProductUserControl(editPresenter);
this.workspace.Open(editControl);
}
public void WhenProductSaved(object sender, EventArgs e)
{
// refresh the data grid, etc.
}
}
Таким образом, ListProductsPresenter
знает о наборе функций Edit
(т. Е. Редакторе презентатора и редактировании пользовательского элемента управления) - и это прекрасно, они идут рука об руку - но это не так необходимо знать обо всех зависимостях набора функций Edit
, вместо этого полагаясь на делегата, предоставленного Autofac для разрешения всех этих зависимостей для него.
Как правило, я нахожу, что у меня есть взаимно-однозначное соответствие между «представителем / моделью представления / контролирующим контроллером» (давайте не будем слишком зацикливаться на различиях, поскольку в конце дня они все очень похожи) и "UserControl
/ Form
". UserControl
принимает модель / контроллер презентатора / представления в своем конструкторе и привязывает данные в зависимости от обстоятельств, максимально откладывая до презентатора. Некоторые люди скрывают UserControl
от докладчика через интерфейс, например IEditProductView
, что может быть полезно, если представление не является полностью пассивным. Я склонен использовать привязку данных для всего, поэтому связь осуществляется через INotifyPropertyChanged
и не беспокоить.
Но вы сделаете свою жизнь намного проще, если ведущий беззастенчиво привязан к представлению. Разве свойство в вашей объектной модели не связано с привязкой данных? Выставьте новое свойство, так оно и есть. У вас никогда не будет EditProductPresenter
и EditProductUserControl
с одним макетом, а затем вы захотите написать новую версию пользовательского элемента управления, которая будет работать с тем же докладчиком. Вы просто отредактируете их обоих: они предназначены для всех целей и целей, это одно устройство, одна функция, презентатор существует только потому, что его легко проверить на устройстве, а пользовательский контроль - нет.
Если вы хотите, чтобы функция была заменяемой, вам необходимо абстрагировать всю функцию как таковую. Таким образом, у вас может быть интерфейс INavigationFeature
, с которым общается ваш MainForm
. Вы можете иметь TreeBasedNavigationPresenter
, который реализует INavigationFeature
и потребляется TreeBasedUserControl
. И у вас может быть CarouselBasedNavigationPresenter
, который также реализует INavigationFeature
и потребляется CarouselBasedUserControl
. Пользовательские элементы управления и докладчики по-прежнему идут рука об руку, но ваш MainForm
не будет заботиться о том, взаимодействует ли он с древовидным или карусельным представлениями, и вы можете поменять их без MainForm
быть мудрее.
В заключение легко запутаться. Каждый педантичен и использует немного другую терминологию, чтобы передать свои тонкие (и зачастую неважные) различия между похожими архитектурными паттернами. По моему скромному мнению, внедрение зависимостей делает чудеса для создания компонуемых, расширяемых приложений, так как связь не поддерживается; разделение функций на «презентаторы / модели представлений / контроллеры» и «представления / пользовательские элементы управления / формы» создает чудеса качества, поскольку большая часть логики перенесена в первую, что позволяет легко тестировать модули; и объединение двух принципов, кажется, действительно то, что вы ищете, вы просто запутались в терминологии.
Или я мог бы быть полон этого. Удачи!