Здравствуйте, Дмитрий В, Вы писали:
Что и было отмечено в разделе 1.1:
Java Swing, в отличие от других платформ, не только предоставляет интерфейс разработки на основе шаблона MVC, но и сам реализован на его основе. Представлением является класс – наследник класса Frame. Вследствие организации событийной модели Java на интерфейсах, контроллер представляет собой набор анонимных классов обработки соответствующих событий. Как и остальные платформы, Swing предоставляет разработку модели программисту.
Но, что очень важно в этом отношении, ниже в разделе 1.3 дается такой комментарий:
Помимо реализации MVC на уровне языков программирования, существует достаточно много программных платформ, которые предлагают готовые решения на основе данного шаблона, например, Spring Framework.
СОВЕТ
Следует четко разделять MVC компонентного уровня, по-другому – framework, и уровня приложения. К примеру, Swing является решением идеи MVC на компонентном уровне. Но при проектировании приложения на основе Swing можно также воспользоваться преимуществами MVC, выделив бизнес-логику приложения в модель, и построив представление и контроллер на основе соответствующих классов Swing.
В данной статье рассматривается реализация библиотеки классов, использование которой упростит следование шаблону проектирования MVC на уровне приложений.
Используемая терминология была взята из множества статьей и описаний курсов обучения шаблону MVC, к примеру, вот здесь
Component- vs. Application-level MVC Architecture:
Swing adopts MVC at component level. We use it as a case study toillustrate features of MVC and also help students learn to use the tool for programming projects. To encourage them to use beyound a specific tool, we assign students a project to build a domain-specific framework for GUI applications using MVC at application level.
Именно эту мысль и озвучил в статье.
... << RSDN@Home 1.2.0 alpha rev. 676>>
Здравствуйте, IB, Вы писали:
Таки не дает мне покоя эта тема
С одной стороны, действительно, замыкать архитектуру на конкретный фреймворк нехорошо. С другой, при использовании классического MVC/MVP теряются все преимущества WPF и вообще смысл переходить на него с WinForms... Т.е. если у меня данные
уже отделены от представления на уровне фреймворка, зачем порождать связи Презентер-Представление, имплементировать какой-нибудь IXyzView, вручную его обновлять и т.д.
Опять-таки, не будучи толком знаком с ASP.NET, решил вникнуть, так сказать, поглубже. Нашел там интересный (опять-таки на мой взгляд новичка) ObjectDataSource контрол, использование которого тоже не сильно вяжется с классическим MVP. Возможно, здесь я и ошибаюсь, просто что я хочу сказать — в каждой из этих конкретных технологий есть свои фишки, дающие определенные преимущества, и хотелось бы эти преимущества использовать максимально. Отсюда напрашивается вывод — создание какого-то промежуточного слоя между бизнес-логикой и Представлением для адаптации под конкретный фреймворк. Хотелось бы услышать мнение по этому поводу (просто пока еще есть возможность внести изменения в свой проект...) А может, есть какие-то ссылки на конкретные решения... Заранее спасибо.
Здравствуйте, Al_Shargorodsky, Вы писали:
A_S>С одной стороны, действительно, замыкать архитектуру на конкретный фреймворк нехорошо. С другой, при использовании классического MVC/MVP теряются все преимущества WPF и вообще смысл переходить на него с WinForms...
Как это теряются? Ничего не теряется, наоборот, помимо собственно WPF механики получаем почти готовый презентер.
A_S>Т.е. если у меня данные уже отделены от представления на уровне фреймворка, зачем порождать связи Презентер-Представление, имплементировать какой-нибудь IXyzView, вручную его обновлять и т.д.
Связь презентер-представление и так есть, ее можно использовать в своих целях, главное обучить презентер понимать модель.
A_S>Опять-таки, не будучи толком знаком с ASP.NET, решил вникнуть, так сказать, поглубже. Нашел там интересный (опять-таки на мой взгляд новичка) ObjectDataSource контрол, использование которого тоже не сильно вяжется с классическим MVP.
Вот за ObjectDataSource команду asp.Net-а надо к десяти годам изнурительного кодирования приговаривать. Более кривого решения для байндинга я не припомню.
Другое дело, что это имеет довольно мало отношения к MVC.
A_S> Возможно, здесь я и ошибаюсь, просто что я хочу сказать — в каждой из этих конкретных технологий есть свои фишки, дающие определенные преимущества, и хотелось бы эти преимущества использовать максимально.
ODS — даже не стоит упоминания, там не преимущества а сплошное издевательство, а WPF проектировался именно в рассчете на то, что его будут использовать в MVP.
A_S> Отсюда напрашивается вывод — создание какого-то промежуточного слоя между бизнес-логикой и Представлением для адаптации под конкретный фреймворк.
В случае WPF этот слой называется DataModel.
... << RSDN@Home 1.2.0 alpha rev. 673>>
Здравствуйте, IB, Вы писали:
A_S>>С одной стороны, действительно, замыкать архитектуру на конкретный фреймворк нехорошо. С другой, при использовании классического MVC/MVP теряются все преимущества WPF и вообще смысл переходить на него с WinForms...
IB>Как это теряются? Ничего не теряется, наоборот, помимо собственно WPF механики получаем почти готовый презентер.
Т.е. правильно ли я понимаю? Пусть есть некий MasterObject, содержащий DetailObject и для интереса отдельный SimpleObject. Что я делаю сейчас: созадю MasterView, у которого есть свойство типа MasterObject и, допустим, свойство типа какого-то SimpleView, который содержит SimpleObject (но ведь можно и прямо SimpleObject). Все остальное — в XAMLе.
Чтобы реализовать MVP, между, допустим MasterView и MasterObject нужно вставить MasterPresenter? Или же MasterView унаследовать от, скажем, Window, а SimpleView — от Control? Но в последнем случае как раз и теряется вся прелесть DataTemplate...
IB>ODS — даже не стоит упоминания, там не преимущества а сплошное издевательство, а WPF проектировался именно в рассчете на то, что его будут использовать в MVP.
Тут не силен — просто не довелось пока...
A_S>> Отсюда напрашивается вывод — создание какого-то промежуточного слоя между бизнес-логикой и Представлением для адаптации под конкретный фреймворк.
IB>В случае WPF этот слой называется DataModel.
Это если принять выше первый вариант?
Спасибо за разъяснения
Здравствуйте, Сергей Рогачев, Вы писали:
СР>Статья:
СР>Обобщенный Model-View-ControllerАвтор(ы): Сергей Рогачев
Дата: 23.03.2007
В статье рассматривается вариант реализации шаблона проектирования Model-View-Controller в виде каркаса приложения на основе обобщенного программирования языков Java и C#. В описании предлагаемого решения, кроме того, будут рассмотрены шаблоны проектирования Mediator, Observer и Command и показаны варианты их применения в рассматриваемой реализации Model-View-Controller. Предполагается наличие у читателя знания базовых шаблонов проектирования, языка UML, диаграммами которого будут сопровождаться описания, а также одного из указанных языков программирования.
Материал изложен четко, просто, и может быть весьма полезен, но ряд аспектов значительно ухудшают впечатление.
Далее все примеры кода написаны на псевдо-С++.
1. Гневный памфлет о Command.
Описание шаблона Command в статье дано правильное, но приведенная реализация шаблоном Command не является, что может сбить с пути истинного читателя неискушенного в шаблонах проектирования.
Рассмотрим эту реализацию.
По GoF шаблон Command призван инкапсулировать запрос. Отсюда вытекают два основных признака Команды:
1) клиент Команды абстрагируется от получателя запроса
2) клиент Команды абстрагируется от вызываемой операции
Из этих признаков следует возможность параметризации клиента запросом, что является основным применением шаблона Команда.
Перепишем реализацию, приведенную в статье, в упрощенном виде:
enum O { OPCODE1, OPCODE2, OPCODE3 }
class Controller {
public:
void execute(O operation, P attribute)
{
switch (operation) {
case OPCODE1:
DoSomething1;
case OPCODE2:
DoSomething2;
case OPCODE3:
DoSomething3;
default:
AbortAll();
}
}
}
class View {
public:
void edit1(P attribute)
{ controller->execute(OPCODE1); }
void edit2(P attribute)
{ controller->execute(OPCODE2); }
void edit3(P attribute)
{ controller->execute(OPCODE3); }
private:
Controller* controller;
}
Никто нам не помешает любой кусок кода вынести в отдельную функцию. Преобразуем код еще немного:
enum O { OPCODE1, OPCODE2, OPCODE3 }
class Controller {
public:
void execute(O operation, P attribute)
{
switch (operation) {
case OPCODE1:
f1(attribute);
case OPCODE2:
f2(attribute)
case OPCODE3:
f3(attribute);
default:
AbortAll();
}
}
private:
void f1(P attribute) { DoSomething1; }
void f2(P attribute) { DoSomething2; }
void f3(P attribute) { DoSomething3; }
}
class View {
public:
void edit1(P attribute)
{ controller->execute(OPCODE1, attribute); }
void edit2(P attribute)
{ controller->execute(OPCODE2, attribute); }
void edit3(P attribute)
{ controller->execute(OPCODE3, attribute); }
private:
Controller* controller;
}
Теперь, взглянув незамутненным взором, можно увидеть, что реализация в статье шаблона Command, является всего лишь косвенным вызовом методов класса с помощью кодов этих методов. Для простоты так и будем дальше называть этот прием "вызов методов по их коду". В качестве кода метода может использоваться что угодно — enum, int, строка и т.д.
Посмотрим выполняются ли признаки Command для этой реализации:
1) клиент НЕ абстрагируется от получателя запроса, т.к класс View хранит прямую ссылку на контроллер и вызывает его операции. То, что контроллер является не конечным получателем, а всего лишь посредником, не важно. Мы обсуждаем шаблон Command, и является ли получатель Mediator'ом или еще какой фабрикой, нас не должно заботить.
2) клиент НЕ абстрагируется от вызываемой операции, т.к View вызывает конкретные методы контроллера, используя конкретный код этого метода. Какой метод контроллера вызвать решает View и только View.
Обобщая дальше, вспомним, что открытый интерфейс класса — это по сути набор внешних сообщений, которые может обрабатывать экземпляр класса. К примеру, набор открытых (public) методов класса View edit1, edit2, edit3+ их сигнатуры является открытым интерфейсом класса. Но таким образом, набор кодов методов в виде OPCODE1, OPCODE2, OPCODE3 + метод execute и ее сигнатура являются открытым интерфейсом класса Controller.
Поэтому назвать обращение экземпляров класса View к экземплярам класса Controller через его открытый интерфейс "шаблоном проектирования" язык не поворачивается. И косвенный вызов методов по их кодам можно без всяких последствий заменить прямым вызовом методов.
class Controller {
public:
void f1(P attribute) { DoSomething1; }
void f2(P attribute) { DoSomething2; }
void f3(P attribute) { DoSomething3; }
}
class View {
public:
void edit1(P attribute)
{ controller->f1(attribute); }
void edit2(P attribute)
{ controller->f2(attribute); }
void edit3(P attribute)
{ controller->f3(attribute); }
private:
Controller* controller;
}
Почему же был использован в статье косвенный вызов вместо прямого? Может быть этот прием имеет какие-то преимущества? Статья ответа на эти вопросы читателю не дает. Пока мы видим только недостатки такого подхода:
— ошибка вызова несуществующего метода по несуществующему коду обнаружится только во время запуска программы ( в секции default switch'а в контроллере), тогда как прямой вызов несуществующего метода обнаруживается уже на этапе компиляции (а в некоторых редакторах — на этапе написания программы)
— неоправданное усложнение реализации
Но если начать изобретать применение этому подходу, то один плюс у него все-таки есть. Мы можем параметризовать View вызываемой операцией:
enum O { OPCODE1, OPCODE2, OPCODE3 }
class View {
public:
O code;
void edit1(P attribute)
{ controller->execute(code, attribute); }
private:
Controller* controller;
}
....
//где-то на этапе инициализации
View* view = new View;
view->code = OPCODE2;
view->setController(controller);
....
В результате View абстрагируется от вызываемой операции, и можно менять поведение экземляров View без переписывания самого класса. Но View все еще зависит от контроллера и может выполнять только операции с одним параметром, поэтому запрос окончательно не инкапсулирован.
Однако в статье такое использование не упоминается, и для читателя применение косвенных вызовов методов контроллера вместо прямых вызовов остается загадкой.
Обратите внимание, что представление и контроллер независимы, несмотря на ограничение по модели.
!true. Как было показано выше, View зависит от контроллера, так как обращается к контроллеру через его открытый интерфейс. Стоит изменить открытый интерфейс — имя кода метода в switch'е метода execute контроллера, либо удалить код, то придется изменять и View. Это ли не зависимость между ними?
2. Реализация истории команд
Во-вторых, продемонстрируем, как легко расширяется функциональность контроллера, который представляет реализацию шаблона проектирования Command – научим контроллер хранить историю изменений и предоставлять возможность последовательно отменять последние произведенные действия.
Как уже было сказано "вызов методов по их коду" можно легко заменить на эквивалентный прямой вызов методов:
class Controller
{
public:
public void Edit(Model<P> model, P attribute)
{
assert(attribute != null);
if (!model.getProperty().equals(attribute)) {
history.push(model.getProperty());
model.setProperty(attribute);
}
}
public void Undo(Model<P> model, P attribute)
{
assert(attribute != null);
if (!history.empty()) {
P property = history.pop();
if (!model.getProperty().equals(property))
model.setProperty(property);
}
}
private:
Stack history = new Stack();
}
Почему в статье утверждается, что "легко расширяется функциональность контроллера, который представляет реализацию шаблона проектирования Command", если ту же самую функциональность можно еще легче реализовать прямым вызовом методов контроллера представлением, читателю остается непонятным.
3. В качестве обзора MVC и небольшого экскурса в generic-программирование, статья может быть полезна, но стоит представить использование обобщенного MVC в слегка более сложном приложении, чем в приведенном примере, то возникает множество вопросов, на которые статья не отвечает.
Объекты класса Switch в примере очень простые. Они не обладают поведением, на них не ссылаются другие объекты, и поэтому всю работу с ними можно свести к простой замене одного объекта другим с измененным состоянием, как и делается в примере.
Добавим к Switch поведение. Допустим, наш переключатель символизирует переключатель на каком-то приборе. Переключатель может быть переведен в состояние "включено", только если прибор подключен к электросети, иначе этого сделать нельзя.
Где мы должны реализовать это ограничение? В представлении или контроллере мы этого сделать не можем, потому что тогда подключая к модели другой контроллер и представления, мы должны дублировать код из старых. Реализуя это ограничение непосредственно в модели, мы должны порождать отдельную модель для каждого "свойства модели", отличающегося поведением, и таким образом теряем то, к чему стремились при написании обобщенного MVC — порождения модели автоматически путем передачи параметра "свойство модели", без необходимости писать модель вручную. Значит нам остается только одно — реализовать это поведение непосредственно в "свойстве модели" классе Switch.
class Switch {
public:
enum State { On, Off };
Switch(bool Powered) { this->Powered = Powered; }
void setOn()
{ if(Powered) state = On; }
void setOff()
{ state = Off; }
State state;
private:
const bool Powered;
}
Кто будет непосредственно вызывать функции setOn, setOff? Если это будет делать модель, то она должна предоставить контроллеру интерфейс для этого, следовательно мы опять приходим к необходимости написания отдельной модели для каждого "свойства модели", избавляясь от обобщенного подхода. Если это будет делать контроллер, вынимая "свойство модели" через model.getProperty(), то контроллеру нужен способ вызвать обновление всех представлений после выполнения операций над "свойством модели". Как он будет это делать? Ведь метод модели _notifyAll() объявлен как protected, и контроллер не содержит ссылок на представления.
И независимо от того, будет ли вызов методов "свойства модели" производиться в контроллере или модели, перед нами встает дилемма:
обновлять все представления каждый раз после выполнения любого метода "свойства модели", в предположении что state "свойства" изменился, тем самым производя частые холостые обновления, либо
проверять каким-либо способом изменилось ли состояние "свойства", а так как мы не знаем чтО может изменить в "свойстве" его метод, то сделать это можно только копированием всего "свойства" перед вызовом его метода и сравнения его со "свойством" после вызова (а если "свойство" весит 1Гб? а если "свойство" — всего лишь proxy к базе данных?).
Таким образом без поддержки самого "свойства" проверить изменилось ли оно мы не можем. Значит просто так взять любой класс и передать его в обобщенный MVC как параметр "свойство модели" без модификации класса нельзя. В статье этот аспект покрыт мраком.
4.
Модель или оповещает конкретного подписчика методом _notify, или оповещает всех своих подписчиков методом _notifyAll. Оповещение подписчиков осуществляется последовательным оповещением каждого – опять же методом _notifyAll.
Модель никогда не должна оповещать конкретного подписчика. Для нее все подписчики на одно лицо, и оповещать может либо всех, либо ни одного. Если было б иначе, то модель бы зависела от представлений.
В последнем слове цитаты видимо опечатка — должно быть не _notifyAll, а _notify.
Здравствуйте, ALSK, Вы писали:
Спасибо за такой развернутый анализ!
1-2. Критика реализации Command верная. Чуть выше bolshik уже намекал на это же: конкретные действия над получателем в статье инкапсулированы в контроллер (клиент), в то время как в теории это должно производится в конкретной команде, а клиент должен только получать команду и устанавливать получателя (модель). Как вариант, можно, к примеру, сделать следующий рефакторинг:
package com.rogachev.patterns.behavioral.command;
public enum COMMAND {
COMMAND1() {
@Override
public void execute(Receiver receiver) {
...
}
},
COMMAND2() {
@Override
public void execute(Receiver receiver) {
...
}
};
public abstract void execute(Receiver receiver);
}
Теперь потребность в блоке switch в контроллере отпадает, т.е. контроллер уже не зависит от набора команд. Ньюанс в том, что в отличие от Java, C#-перечисления сделать подобное не позволят — как вариант, вообще отказаться от перечислений и использовать обычные классы.
3. Как вариант, можно поступать следующим образом... Класс свойства модели должен реализовать интерфейс копирования (не клонирования):
package com.rogachev.patterns.clone;
public interface IDuplicable<T extends IDuplicable<T>> {
void duplicate(T obj) throws ...;
}
Теперь модель изменяет свойство следующим образом:
package com.rogachev.patterns.mvc.model;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;
public class Model<P> {
...
public void setProperty(P _property) {
assert _property != null;
// property = _property;
try {
property.duplicate(_property);
_notifyAll();
} catch (... e) {
}
}
...
}
Ну, а собственно ограничение вы описываете в свойстве модели, как и вы решили — в методе интерфейса IDuplicable. В итоге объект свойства модели становится persistence, что упрощает работу с некоторыми библиотеками т.н. DAL Persistence objects, вроде db4o.
4.
ALS>Модель никогда не должна оповещать конкретного подписчика.
Издатель (модель) может оповестить конкретного подписчика (представление или модель списка), это упрощает инициализацию представлений по умолчанию:
Подписчики регистрируются в модели методом subscribe. Регистрация сводится к добавлению подписчика в список subscribers и принудительному его оповещению – вызов метода _notify. Таким образом, после регистрации в качестве подписчика модели представление сразу же получает оповещение и впервые отображает модель.
ALS>В последнем слове цитаты видимо опечатка — должно быть не _notifyAll, а _notify.
Видимо просто несколько сумбурно сказано. Имелось в виду, что оповещение подписчиков производится в методе _notifyAll посредством последовательного вызова метода _notify у каждого подписчика.
Здравствуйте, rsn81, Вы писали:
R>3. Как вариант, можно поступать следующим образом... Класс свойства модели должен реализовать интерфейс копирования (не клонирования):
R>package com.rogachev.patterns.clone;
R>public interface IDuplicable<T extends IDuplicable<T>> {
R> void duplicate(T obj) throws ...;
R>}
R>Теперь модель изменяет свойство следующим образом:
R>package com.rogachev.patterns.mvc.model;
R>import java.util.Collection;
R>import java.util.concurrent.CopyOnWriteArrayList;
R>public class Model<P> {
R> ...
R> public void setProperty(P _property) {
R> assert _property != null;
R> // property = _property;
R> try {
R> property.duplicate(_property);
R> _notifyAll();
R> } catch (... e) {
R> }
R> }
R> ...
R>}
R>Ну, а собственно ограничение вы описываете в свойстве модели, как и вы решили — в методе интерфейса IDuplicable. В итоге объект свойства модели становится persistence, что упрощает работу с некоторыми библиотеками т.н. DAL Persistence objects, вроде db4o.
Для простых свойств (например для базовых типов) возможно это будет работать отлично, но если "свойство модели" громоздко, содержит кучу аттрибутов, то такое решение будет страшно непроизводительным. Ведь мы должны перебрать и скопировать все аттрибуты, даже если операция пользователя привела к изменению только какого-то одного аттрибута. Причем такой полный перебор аттрибутов происходит дважды: в equals в контроллере, и в duplicate в модели.
Вообще я бы разделил для такого MVC типы объектов на два вида: простые (без поведения, легковесные, нельзя иметь ссылку на такой объект) и сложные (без ограничений). Простые можно смело оборачивать в "модель" и общаться с ними таким незамысловатым образом как двумя операциями equals и duplicate, а со сложными обращаться каким-то другим механизмом.
R>4.
R>Издатель (модель) может оповестить конкретного подписчика (представление или модель списка), это упрощает инициализацию представлений по умолчанию:
R>Подписчики регистрируются в модели методом subscribe. Регистрация сводится к добавлению подписчика в список subscribers и принудительному его оповещению – вызов метода _notify. Таким образом, после регистрации в качестве подписчика модели представление сразу же получает оповещение и впервые отображает модель.
При таком подходе, верно. Это я упустил.
Просто в моей реализации MVC представление может быть подписано на модель до того, как будет полностью проинициализировано и сконфигурировано. Например, в конструкторе базового класса, или до передачи "целевого" контрола в представление. Таким образом вызов notify для такого представления во время подписки приведет к ошибке. Выглядит конечно не очень безопасно (вдруг модель вызовет обновление подписчиков раньше чем нужно), но зато избавляет разработчика от необходимости обеспечивать подписку строго после полного "укомплектования" представления.