Аннотация:
В статье рассматривается реализация шаблона проектирования Model-View-Controller на основе обобщенного программирования языков Java и C#. В описании предлагаемого решения, кроме того, будут рассмотрены шаблоны проектирования Mediator, Observer и Command. Предполагается наличие у читателя знания базовых шаблонов проектирования, языка UML, диаграммами которого будут сопровождаться описания, а также одного из указанных языков программирования.
Здравствуйте, Al_Shargorodsky, Вы писали:
A_S> Если коротко — контроллер оказывается не нужным в принципе...
Да, там его переименовали в ViewModel... =) Когда контроллер не реализуется в принципе — это паттерн DocumentView. Там логика контроллера реализуется во View, что затрудняет тестирование и обладает рядом других недостатков...
...отсутствует четкое разделение между представлением и контроллером, в особенности это касается .NET...
Ну как сказать, Swing вообще работает только с model + UI, где UI = view + controller;
2. Отсутствуют проверки на null принимаемых аргументов и не решена упоминавшаяся проблема с синхронизацией. Причем не только синхронизированный listeners management, но и синхронизированные доступы к свойству
Пример:
public abstract class Model<P> {
private P property;
...
public Model(P property) {
this.property = property;
}
public P getProperty() {
return property;
}
public void setProperty(P property) {
this.property = property;
notifyListeners();
}
...
Если get() происходит после того, как начался set(), но до того, как он закончился, будут проблемы;
3. finalize() использовать настоятельно не рекомендуется. Почему можно почитать у того же Brian Goetz. Вместо этого используется механизм фантомных ссылок;
4. Для работы с listeners также лучше применять слабые ссылки с целью избежания утечки памяти. Подробнее здесь;
5. Имхо не самая оптимальная работа с дженериками. Я по-быстрому перекидал их:
Model.java
package ru.rsdn.mvc.model;
import java.util.Collection;
import java.util.HashSet;
public abstract class Model<P> {
private P property;
private final Collection<IModelSubscriber<P>> subscribers = new HashSet<IModelSubscriber<P>>();
public Model(P property) {
this.property = property;
}
public P getProperty() {
return property;
}
public void setProperty(P property) {
this.property = property;
notifyListeners();
}
protected void notifyListeners() {
for (IModelSubscriber<P> listener : subscribers) {
notifyListener(listener);
}
}
private void notifyListener(IModelSubscriber<P> subscriber) {
subscriber.modelChanged(this);
}
public void subscribe(IModelSubscriber<P> subscriber) {
assert !subscribers.contains(subscriber) : "Повторная подписка: "
+ subscriber;
subscribers.add(subscriber);
notifyListener(subscriber);
}
public void unsubscribe(IModelSubscriber subscriber) {
assert subscribers.contains(subscriber) : "Неизвестный подписчик: "
+ subscriber;
subscribers.remove(subscriber);
}
public String toString() {
return property.toString();
}
}
IModelSubscriber.java
package ru.rsdn.mvc.model;
public interface IModelSubscriber<P> {
void modelChanged(Model<P> model);
}
ListModel.java
package ru.rsdn.mvc.model;
import java.util.Collection;
import java.util.HashSet;
public class ListModel<P> extends Model<Collection<Model<P>>> implements IModelSubscriber<P> {
public ListModel() {
super(new HashSet<Model<P>>());
}
public void add(Model<P> model) {
getProperty().add(model);
model.subscribe(this);
}
public void modelChanged(Model<P> model) {
notifyListeners();
}
public void remove(Model<P> model) {
model.unsubscribe(this);
getProperty().remove(model);
notifyListeners();
}
}
Controller.java
package ru.rsdn.mvc.controller;
import ru.rsdn.mvc.model.Model;
public class Controller<P> implements IController<Controller.Operations, P, Model<P>> {
public enum Operations {EDIT}
public void execute(Operations operation, Model<P> model, P attribute) {
assert attribute != null : "Не передан атрибут в операцию";
switch (operation) {
case EDIT:
model.setProperty(attribute);
break;
default:
assert false : "Неизвестная операция: " + operation;
}
}
}
IController.java
package ru.rsdn.mvc.controller;
import ru.rsdn.mvc.model.Model;
public interface IController<O, P, M extends Model<P>> {
void execute(O operation, M model, P attribute);
}
ListController.java
package ru.rsdn.mvc.controller;
import ru.rsdn.mvc.model.ListModel;
import ru.rsdn.mvc.model.Model;
import java.util.Collection;
import java.util.Arrays;
public class ListController<P> implements IController<ListController.Operations, Collection<Model<P>>, ListModel<P>> {
public enum Operations {ADD, REMOVE}
public void execute(Operations operation, ListModel<P> listModel, Model<P> attribute) {
execute(operation, listModel, Arrays.asList(attribute));
}
public void execute(Operations operation, ListModel<P> listModel, Collection<Model<P>> attribute) {
assert attribute != null : "Не передан атрибут в операцию";
switch (operation) {
case ADD:
for (Model<P> model : attribute) {
listModel.add(model);
}
break;
case REMOVE:
for (Model<P> model : attribute) {
listModel.remove(model);
}
break;
default:
assert false : "Неизвестная операция: " + operation;
}
}
}
BaseView.java
package ru.rsdn.mvc.view;
import ru.rsdn.mvc.model.IModelSubscriber;
import ru.rsdn.mvc.model.Model;
public abstract class BaseView<P, M extends Model<P>> implements IModelSubscriber<P> {
private M model;
protected M getModel() {
return model;
}
public void setModel(M model) {
unsubscribe();
this.model = model;
subscribe();
}
private void subscribe() {
if (model != null) {
model.subscribe(this);
}
}
private void unsubscribe() {
if (model != null) {
model.unsubscribe(this);
}
}
protected void finalize() throws Throwable {
unsubscribe();
super.finalize();
}
}
View.java
package ru.rsdn.mvc.view;
import ru.rsdn.mvc.controller.Controller;
import ru.rsdn.mvc.model.Model;
public abstract class View<P> extends BaseView<P, Model<P>> {
private final Controller<P> controller = new Controller<P>();
protected void edit(P property) {
controller.execute(Controller.Operations.EDIT, getModel(), property);
}
}
ListView.java
package ru.rsdn.mvc.view;
import ru.rsdn.mvc.controller.Controller;
import ru.rsdn.mvc.controller.ListController;
import ru.rsdn.mvc.model.ListModel;
import ru.rsdn.mvc.model.Model;
import java.util.Collection;
public abstract class ListView<P> extends BaseView<Collection<Model<P>>, ListModel<P>> {
private final Controller<P> controller = new Controller<P>();
private final ListController<P> listController = new ListController<P>();
protected void edit(Model<P> model, P property) {
controller.execute(Controller.Operations.EDIT, model, property);
}
protected void add(Model<P> model) {
listController.execute(ListController.Operations.ADD, getModel(), model);
}
protected void delete(Model<P> model) {
listController.execute(ListController.Operations.REMOVE, getModel(), model);
}
}
6. В текущей реализации для выбора нужной операции используется switch. В перспективе это не самое удобное решение (например, будет не две операции, а двадцать две). Лучше сделать через набор обработчиков операций, имеющих общий интерфейс (пока в джаве нет делегатов) и при необходимости обработать команду, брать из map нужный обработчик и делегировать задачу ему;
контроллер представляет собой набор анонимных классов обработки соответствующих событий
2-5. Ага, так и есть — не хотелось усложнять примеры:
Приводимый код вовсе не является готовым решением. Для наглядности указания направления, в котором, возможно, стоит двигаться, компоненты реального приложения были максимально упрощены, а демонстрационные примеры программ приведены для подтверждения верности выбранного решения.
2. Установить в классе Model cинхронизацию по property в методе setProperty, но, по-моему, смысла в этом мало.
3. Да. Но в данном случае это не страшно: методы finalize используются в классах представлений, которые создаются-собираются не так часто.
4. Спасибо, надо изучить на досуге.
5. Супер, большое спасибо за рефакторинг! Именно так и пытался сделать вначале, но камнем преткновения становился ListController (и ListView), в котором вы предложили такой "финт ушами" сделать, до которого сам не смог додуматься.
PS Статье на самом деле уже больше полугода — Java-код статьи выдирался из рабочих проектов в августе-сентябре 2006 года. Код серьезно уже давно не смотрел, т.к., в принципе, по работе уже отошел от MVC: после статьи IB Model-View-Controller в .Net
Здравствуйте, IB, Вы писали:
IB>Да, там его переименовали в ViewModel... =) Когда контроллер не реализуется в принципе — это паттерн DocumentView. Там логика контроллера реализуется во View, что затрудняет тестирование и обладает рядом других недостатков...
Всетаки ViewModel это не совсем контроллер-презентер. Я бы сказал так — ViewModel можно привести к виду презентера, но это только частный случай. Модифицируем тот же пример
public class Model : INotifyPropertyChanged
{
private double _valueFahrenheit = 32;
private double _valueCelsius = 0;
public event PropertyChangedEventHandler PropertyChanged;
public double valueFahrenheit
{
get { return _valueFahrenheit; }
set
{
_valueFahrenheit = value;
_valueCelsius = (_valueFahrenheit - 32) * 5 / 9;
NotifyPropertyChanged("valueFahrenheit");
}
}
public double valueCelsius
{
get { return _valueCelsius; }
set
{
_valueCelsius = value;
_valueFahrenheit = _valueCelsius * 9 / 5 + 32;
NotifyPropertyChanged("valueCelsius");
}
}
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
где-то в XAML
<DataTemplate DataType=src:Model>
<TextBlock Text={Binding valueFahrenheit}/>
... и так далее
</DataTemplate>
Больше не нужно ничего — всю черновую работу сделает фреймворк. Класс Model здесь — это и ViewModel и DataModel одновременно. Пример, конечно примитивный, но думаю, суть понятна.
Здравствуйте, rsn81, Вы писали:
R>...
R>2. Установить в классе Model cинхронизацию по property в методе setProperty, но, по-моему, смысла в этом мало.
Не, смысл-то есть, единственно что эффективнее будет работать не через старые примитивы синхронизации (использование synchronized), а через AtomicReference. Почему это эффективнее, и вообще о CAS-инструкциях можно почитать у того же Brian Goetz
R>3. Да. Но в данном случае это не страшно: методы finalize используются в классах представлений, которые создаются-собираются не так часто.
Спорить не буду, потому что тут, имхо, уже дело субъективное. Я бы либо не использовал finalize() в примере вообще, либо делал на фантомных ссылках, но хозяин барин
R>5. Супер, большое спасибо за рефакторинг! Именно так и пытался сделать вначале, но камнем преткновения становился ListController (и ListView), в котором вы предложили такой "финт ушами" сделать, до которого сам не смог додуматься.
Здравствуйте, bolshik, Вы писали:
B>Не, смысл-то есть, единственно что эффективнее будет работать не через старые примитивы синхронизации (использование synchronized), а через AtomicReference. Почему это эффективнее, и вообще о CAS-инструкциях можно почитать у того же Brian Goetz
Да, эту статью про неблокирующие алгоритмы CAS уже читал. Вопрос только в том, действительно ли в приложении будет несколько потоков, борющихся за доступ к модели — может чего-то путаю, но вроде SWT и Swing исполняются в одном потоке.
Здравствуйте, Al_Shargorodsky, Вы писали:
A_S>Всетаки ViewModel это не совсем контроллер-презентер.
Да, не совсем — это только часть контроллера.
A_S> Я бы сказал так — ViewModel можно привести к виду презентера, но это только частный случай.
Не совсем так.
В MVP Presenter (контроллер) выполняет, с одной стороны роль медиатора для модели, а с другой управляет набором View.
В случае WPF ребята эти две роли разделили, в результате Presenter распался на две части: DataModel — медиатор между моделью и View, ModelView — управляет View. Модель же и View остались неизменными. Таким образом, паттерн распался на 4 компонента — Model, DataModel, ModelView и View.
Они сами об этом и пишут: "DataModel is responsible for exposing data in a way that is easily consumable by WPF. All of its public APIs must be called on the UI thread only" Иными словами, роль DataModel в том, чтобы представить данные(Model) в том виде, в котором они удобны для WPF, то есть классическое применение паттерна медиатор.
Касательно ViewModel — "A ViewModel is a model for a view in the application (duh!). It exposes data relevant to the view and exposes the behaviors for the views, usually with Commands"
A_S> public class Model : INotifyPropertyChanged
Если Model — это по прежнему модель, то так делать нельзя. Модель по определению ничего не знает о контроллере и его интерфейсах, а INotifyPropertyChanged — это именно интерфейс контроллера, точнее медиатора для View, а не модели. А у одной модели может быть несколько Презентеров.
A_S> Класс Model здесь — это и ViewModel и DataModel одновременно.
Тогда Model переходит в разряд презентеров.
Здравствуйте, rsn81, Вы писали:
R>Да, эту статью про неблокирующие алгоритмы CAS уже читал. Вопрос только в том, действительно ли в приложении будет несколько потоков, борющихся за доступ к модели — может чего-то путаю, но вроде SWT и Swing исполняются в одном потоке.
Насчет SWT не знаю, но Swing да, работает в одном потоке, но в то же время никто не запрещает вызвать сеттер модели из другого потока, не EDT. При этом с одной стороны виноват программист, который вызывает его не из EDT, а с другой стороны удобнее использовать класс, просто вызывая сеттер, чем постя обновление через invokeLater(). Т.к. издержки на производительность при этом незначительные (из-за поддержки CAS-инструкций процессором), кажется, что оно того стоит.
Здравствуйте, rsn81, Вы писали:
R>Model-View-Presenter — успешно переполз на него и радуюсь пассивной модели.
Если интересно, то вот демонстрационный пример Generic MVP — все интересное, что описано в разделе статьи "Внедрение", перенесено и в MVP: Undo/Redo (полная поддержка) и клонирование.
Пару дней пересматривал архитектуру своего текущего проекта и собственные представления в этой области. Пока остался при своем мнении... Буду признателен за дальнейшие разъяснения
A_S>> public class Model : INotifyPropertyChanged IB>Если Model — это по прежнему модель, то так делать нельзя. Модель по определению ничего не знает о контроллере и его интерфейсах, а INotifyPropertyChanged — это именно интерфейс контроллера, точнее медиатора для View, а не модели. А у одной модели может быть несколько Презентеров.
Т.е. чтобы использовать binding в обе стороны, нужно будет делать либо обертку вокруг модели, либо делать свойства модели виртуальными и плодить наследников? Но что мешает сделать несколько DataTemplate и получить несколько Представлений, при этом еще и синхронизированных?
A_S>> Класс Model здесь — это и ViewModel и DataModel одновременно. IB>Тогда Model переходит в разряд презентеров.
Собственно, я и пытаюсь сказать, что разделение на Модель и Презентер в WPF становится не актуальным — данные уже отделены от Представления средствами фреймворка, а логика реализуется либо во ViewModel, либо в классах-обработчиках команд, либо вообще в отдельных блоках, подписанных на тот же PropertyChanged соответствующего объекта.
Здравствуйте, Al_Shargorodsky, Вы писали:
A_S>Т.е. чтобы использовать binding в обе стороны, нужно будет делать либо обертку вокруг модели, либо делать свойства модели виртуальными и плодить наследников?
Не либо. Обертка во круг модели, других вариантов нет.
A_S> Но что мешает сделать несколько DataTemplate и получить несколько Представлений, при этом еще и синхронизированных?
Тем, что это прибъет модель гвоздями к WPF.
A_S>Собственно, я и пытаюсь сказать, что разделение на Модель и Презентер в WPF становится не актуальным — данные уже отделены от Представления средствами фреймворка, а логика реализуется либо во ViewModel, либо в классах-обработчиках команд, либо вообще в отдельных блоках, подписанных на тот же PropertyChanged соответствующего объекта.
Понимаешь, модель может быть представима не только в WPF. Например в моих задачах одну и ту же модель нужно пихать в веб-сервисы, отображать на вебе и WinForms, помимо самого WPF, и еще чего-нибудь хитрое с ней делать. И это скорее правило, нежели исключение.
Поэтому делать из модели WPF-овский презентер — не лучшая идея.
СР>Авторы: СР> Сергей Рогачев
СР>Аннотация: СР>В статье рассматривается реализация шаблона проектирования Model-View-Controller на основе обобщенного программирования языков Java и C#. В описании предлагаемого решения, кроме того, будут рассмотрены шаблоны проектирования Mediator, Observer и Command. Предполагается наличие у читателя знания базовых шаблонов проектирования, языка UML, диаграммами которого будут сопровождаться описания, а также одного из указанных языков программирования.
Честно говоря тема реализации грамотного гуевого фреймворка не раскрыта.
Нуда, есть MVC и все такое, слушатели, подписчики — об этом и так много книг написано.
Только делать слабые связи между обьектами нужно не потому, что об этом написано везде в книжках и что это хорошо, а потому, что это действительно надо.
И с самого начала статьи надо было написать — есть такая то проблема, и она решается так то так. А статья начинается с того, что "В статье рассматривается реализация шаблона проектирования Model-View-Controller" — вот она оказывается какая проблема — хотим MVC использовать, потому что это хорошо.. нде...
Здравствуйте, IB, Вы писали:
IB>Понимаешь, модель может быть представима не только в WPF. Например в моих задачах одну и ту же модель нужно пихать в веб-сервисы, отображать на вебе и WinForms, помимо самого WPF, и еще чего-нибудь хитрое с ней делать. И это скорее правило, нежели исключение. IB>Поэтому делать из модели WPF-овский презентер — не лучшая идея.
Спасибо, вобщем я где-то так и думал. И все-таки — настолько ли сильно реализация INotifyPropertyChanged привязывает к определенному Представлению? Это ведь просто событие — не хочешь, не подписывайся...
В том же Swing'e жавовском MVC в каждом визуальном компоненте — даже у кнопки есть ButtonModel (не говоря уже о таблице), есть контроллер (сам обьект кнопки JButton) и view — ButtonUI. И там как раз JButton подписывается на события изменения модели и вся остальная приблуда тоже присутствует.
и т.п. — мне нравится в этом отношении статья О потерянном уровне. В обсуждаемой же статье, как и обычно, приведены шаблоны как решения типовых задач, то есть четко сказано, где они применяются и какой выигрыш дают. Мне кажется излишним приводить листинг "бездумного" кода, это, как понимаете, в отличие от шаблонов слишком многовариантно, хотя в книге по первой ссылке это и попытались сделать.
Во-вторых, давно уже склоняюсь к мысли, что до необходимости применения шаблонов проектирования на практике разработчик должен дойти своим умом. Пока человек сам не столкнется с проблемой, пока самостоятельно, следуя внутреннему решению, а не указке, не начнет искать готовые решения — его не стоит учить шаблонам. Иначе человек наверняка, во-первых, будет использовать шаблоны, как студент технического ВУЗа методичку — бездумно используя их, где надо и не надо, а во-вторых, навсегда похоронит в себе архитектора: шаблоны — это типовые решения, опыт накопленный другими разработчиками, и используя шаблоны человек не должен переставать думать, и, возможно, изобретать в конкретных специфических задачах свои собственные шаблоны. То есть использовать шаблоны проектирования, конечно, нужно, но тогда, когда подсказывает собственный опыт — иначе есть опасность применения шаблонов в том месте, где они только навредят.
В подтверждение последнего анекдот собственной жизни. В свое время сразу после университета, в ту пору, когда каждый студент думает, что он знает и может все, опробовал на своем собственном лбу n-количество граблей, после чего нашел несколько, как мне тогда показалось, гениальных решений. И каким же было мое изумление, когда позже в период переосмысления своих завышенных в студенчестве возможностей, я узнал, что, оказывается, ничто не ново под Луной. Оказалось, что моим "гениальным решениям" (насколько помню, использовал тогда жуткие собственные интерпретации AbstractFactory, State, Decorator, Adapter, Command, Observer, Singleton, Mapper, Memento и что-то еще, все не упомнить... особенно то, как я сам их называл, хотя скорее всего просто интуитивно применял без формализации и классификации) сто лет в обед, называются они шаблонами проектирования, имеют "умные" названия и классификацию. Жутко тогда расстроился, а потом подумал, посмеялся... и понял, что именно такой путь и нужно проделать на пути проектирования: дойти своими мозгами хотя бы до постановки проблемы, четко ее сформулировать самому себе, а вот только после этого смотреть готовые решения этой проблемы.