Сообщений 32    Оценка 231 [+1/-2]         Оценить  
Система Orphus

Обобщенный Model-View-Controller

Каркас на основе шаблона проектирования MVC в исполнении Generic Java и C#

Автор: Сергей Рогачев
Филиал ООО "Лукойл-Информ" в г. Пермь
Опубликовано: 23.03.2007
Исправлено: 10.12.2016
Версия текста: 3.2

1 Model-View-Controller
1.1 Java
1.2 .NET
1.3 Оригинальный MVC
2 Обобщенный MVC
2.1 Активная модель и представление
2.2 Представление и контроллер
2.3 Каркасы и шаблоны проектирования
3 Реализация на Java
3.1 Модель
3.2 Контроллер
3.3 Представление
4 Реализация на C#
4.1 Отличия
4.2 Модель
4.3 Контроллер
4.4 Представление
5 Примеры
5.1 Java SWT
5.2 Windows Forms
5.3 ASP.NET
6 Внедрение
6.1 Интеллектуальный контроллер
6.2 Клонирование моделей
6.3 Уровень доступа к данным
6.4 Контроль корректности данных
7 Источники

Прошлые поколения оставили нам не столько готовые решения вопросов,
сколько самые вопросы.
Настанет время, когда наши потомки будут удивляться,
что мы не знали таких очевидных вещей.
Сенека

Демонстрационные программы: Java SWT | WinForms

СОВЕТ

В статье рассматривается вариант реализации шаблона проектирования Model-View-Controller в виде каркаса приложения на основе обобщенного программирования языков Java и C#. В описании предлагаемого решения, кроме того, будут рассмотрены шаблоны проектирования Mediator, Observer и Command и показаны варианты их применения в рассматриваемой реализации Model-View-Controller. Предполагается наличие у читателя знания базовых шаблонов проектирования, языка UML, диаграммами которого будут сопровождаться описания, а также одного из указанных языков программирования. В любом случае перед прочтением статьи рекомендуется ознакомиться с публикациями [1-2, 4-5].

1 Model-View-Controller

Шаблон проектирования Model-View-Controller (далее просто MVC) лег в основу архитектурного решения первой среды программирования с графическим интерфейсом пользователя – Smalltalk-80. Впервые MVC описал еще в 1978 году норвежец Трюгве Ринскауг, работавший некоторое время в лаборатории Xerox PARC [3]. Реализацию шаблона в Smalltalk-80 описал несколько позже Стив Бурбек [4].

ПРИМЕЧАНИЕ

Шаблон проектирования MVC предполагает разделение данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: модель, представление и контроллер – таким образом, что модификация каждого компонента может осуществляться независимо. Модель (Model) предоставляет данные предметной области представлению и реагирует на команды контроллера, изменяя свое состояние. Представление (View) отвечает за отображение данных предметной области (модели) пользователю, реагируя на изменения модели. Контроллер (Controller) интерпретирует действия пользователя, оповещая модель о необходимости изменений.


Рисунок 1. Окно программы-примера MVC.

Рассмотрим простой пример MVC, который впоследствии будет реализован в презентационных программах, прилагаемых к статье. Модель может представлять собой объект, реализующий переключатель. В простейшем случае данная модель характеризуется состоянием: выключен или включен – и, кроме того, позволяет изменять его – объект модели имеет метод изменения состояния: выключить и включить. Представление отображает на дисплее состояние переключателя с помощью определенной текстовой или графической формы. Например, представление может отображать текстовую метку, которая при изменении состояния модели переключателя отобразит соответствующий текст: «выключен» или «включен». Помимо отображения представление позволяет пользователю изменять состояние переключателя с помощью графических примитивов, к примеру, двух кнопок с надписями: «Включить» и «Выключить». Представление умеет только отображать состояние модели переключателя, для изменения представление обращается к контроллеру. Контроллер представляет собой объект, который в нашем случае умеет только изменять состояние модели переключателя.


Рисунок 2. Шаблон проектирования MVC.

Полный цикл работы данной MVC-триады: модели, представления и контроллера переключателя – можно описать следующим образом. При инициализации представления пользователем оно обращается к модели и устанавливает текст метки в соответствии с текущим состоянием переключателя. Пользователь инициирует изменение переключателя, нажимая на определенную кнопку. При этом представление отправляет соответствующую команду контроллеру: включить или выключить. Контроллер интерпретирует команду и изменяет модель. Представление регистрирует изменение модели: по этому событию оно изменяет текст метки для соответствия новому состоянию модели переключателя.


Рисунок 3. Диаграмма последовательности MVC.

Предложенное программистами Smalltalk-80 решение оказалось настолько эффективным, что по прошествии уже почти 30 лет с момента своего появления шаблон проектирования MVC до сих пор является стандартом настольных и Интернет-приложений. В этом легко убедиться – достаточно рассмотреть, насколько MVC представлен в популярных платформах программирования.

1.1 Java

Java 2 Enterprise Edition (J2EE) отличается от других платформ тем, что определяет шаблон модели – Beans. Представлением является Java Server Page или, как альтернатива, код сервлета, который генерирует представление. Контроллером можно считать сервлет.

Java Swing, в отличие от других платформ, не только предоставляет интерфейс разработки на основе шаблона MVC, но и сам реализован на его основе. Представлением является класс – наследник класса Frame. Вследствие организации событийной модели Java на интерфейсах, контроллер представляет собой набор анонимных классов обработки соответствующих событий. Как и остальные платформы, Swing предоставляет разработку модели программисту.

1.2 .NET

ASP.NET определяет представление и контроллер, а разработку модели отдает на откуп разработчику. Представлением являются наборы файлов ASPX и ASCX. Класс представления наследуется от класса контроллера в отличие от оригинальной реализации MVC в Smalltalk, в которой представление и контроллер разделены и связаны перекрестными ссылками. Контроллером является реализация классов Page и Control, а также обработчики событий, описанные разработчиком.

Windows Forms (WinForms), как и ASP.NET, задает только представление и контроллер. Класс, наследуемый от классов Form или Control, является представлением. В отличие от ASP.NET, которое использует наследование представления от контроллера, в WinForms данные компоненты компилируются в один класс. Контроллером являются выполняемые на уровне операционной системы генерация и транспорт событий, а также прием и перенаправление событий классами Form и Control в соответствующие обработчики, определенные разработчиком.

1.3 Оригинальный MVC

Как видно, компонентный уровень MVC в языках программирования имеет ряд отступлений от оригинального MVC. Во-первых, отсутствует четкое разделение между представлением и контроллером, в особенности это касается .NET. Во-вторых, в отличие от J2EE, остальные реализации MVC не требуют выделения бизнес-логики приложения в отдельный компонент – модель. Программист самостоятельно принимает решение: тратить изрядные усилия по выделению в приложении модели или же сразу описывать в контроллере обработку данных, к примеру, обращения к базе данных.

Второе решение приводит к проблеме проектирования, для борьбы с которой и был создан шаблон MVC: если не выделить отдельную сущность – модель, избежать зависимости бизнес-логики от контроллера невозможно. Зачастую ему следуют неопытные программисты, которые в силу незнания считают его единственно возможным.

ПРИМЕЧАНИЕ

MVC не описывает взаимодействие модели с данными – уровень доступа к данным рассматривают другие шаблоны.

И все же перечисленные расхождения с оригиналом не являются нарушениями основной идеи шаблона.

ПРЕДУПРЕЖДЕНИЕ

MVC задает не столько правила разделения приложения на отдельные компоненты, сколько правила их взаимодействия. В то время как представление и контроллер зависят от модели, модель не зависит ни от представления, ни от контроллера. Это ключевая особенность разделения, которая позволяет работать с моделью, а значит, и с бизнес-логикой приложения, независимо от визуального представления.

Помимо поддержки реализации MVC со стороны стандартного комплекта средств разработки (SDK, Software Development Kit) языков программирования, существует достаточно много программных платформ, которые предлагают готовые решения по реализации приложений на основе данного шаблона, например, Spring Framework.

СОВЕТ

Следует четко разделять MVC компонентного уровня, по-другому – framework, и уровня приложения. К примеру, Swing является решением идеи MVC на компонентном уровне. Но при проектировании приложения на основе Swing можно также воспользоваться преимуществами MVC, выделив бизнес-логику приложения в модель, и построив представление и контроллер на основе соответствующих классов Swing.

В данной статье рассматривается реализация библиотеки классов, использование которой упростит следование шаблону проектирования MVC на уровне приложений. В отличие от описанных выше компонентных решений, будет приведен пример реализации, максимально близкой к предложенной программистами Smalltalk-80, которая, во-первых, четко разграничит модель, представление и контроллер, а, во-вторых, не будет допускать частичного использования компонентов MVC-триады – будет требовать определения программистом модели. Реализация идеи приводится на языках программирования Java и C#.

2 Обобщенный MVC

Представление напрямую управляет отображением информации пользователю. Сама эта информация в терминах шаблона проектирования MVC называется моделью. Контроллер сотрудничает с представлением, гарантируя интерпретацию действий пользователя над данными, опять же – отображением модели. Поэтому представление и контроллер могут быть ограничены типом данных (моделью), с которым они работают. Это ограничение позволит использовать представление и контроллер совместно для работы с одним типом данных – моделью.

Напротив, модель не должна иметь ограничений. Ограничение на тип объекта, который может функционировать в качестве модели, в свою очередь ограничит рамки использования триады MVC. Необходимо, чтобы любой объект мог быть моделью. К примеру, число с плавающей точкой может выступить моделью для отображения температуры, а строка – моделью текстового редактора.

С другой стороны, модель может быть ограничена типом данных, который она представляет, и который наследуется от суперкласса для всех возможных типов – класса Object. Тип данных, которым оперирует модель, назовем свойством модели.

Впервые подход, при котором под изменением состояния модели понимается изменение ее свойств, которые можно получать и изменять, как и в концепции JavaBeans, с помощью методов getter и setter, предложил Эриан Верми в 2004 году в публикации [5], описывающей пример обобщенного MVC.

При таком подходе модель является посредником между моделью предметной области (Domain Model), в нашей терминологии – свойством модели, и остальными компонентами MVC-триады: представлением и контроллером – иллюстрируя, таким образом, шаблон проектирования Mediator.

Шаблон проектирования Mediator

ПРИМЕЧАНИЕ

Шаблон проектирования Mediator (Посредник) используется в случае, когда необходимо обеспечить взаимодействие нескольких объектов, сформировав при этом слабую связанность и избавив объекты от необходимости явно ссылаться друг на друга. Решение заключается в том, что создается объект (посредник), инкапсулирующий способ взаимодействия нескольких объектов. Таким образом, устраняется связность взаимодействующих объектов и централизуется управление.

Поставленные задачи изящно решаются на основе последнего усовершенствования языков программирования Java и C# – возможности обобщенного программирования (Generic).

ПРИМЕЧАНИЕ

Generic – подход к программированию, заключающийся в построении обобщенных алгоритмов, работающих соответствующим образом с каждым конкретным типом данных. Обобщения предоставляют программисту возможность определения «заполнителей», называемых параметрами типа, для определений классов или методов, которые будут конкретизированы соответственно при создании экземпляра (или наследовании) обобщенного класса или во время вызова обобщенного метода. Возможность обобщенного программирования предоставляют многие языки программирования. Впервые идея общих алгоритмов была реализована в 1970 году на языках CLU и Ada и впоследствии заимствована объектно-ориентированными языками BETA, C++, D, Eiffel, Java, C# и другими.

Ограничения обобщенных алгоритмов, с одной стороны, указывают компилятору, чем является параметризующий тип, с другой – накладывают ограничения на типы, используемые в качестве аргументов типа (подставляемые вместо параметризирующих). В статье будут рассмотрены реализации указанных выше ограничений на языках программирования Java и C#.

СОВЕТ

В цели данной работы не входит ознакомление читателя с обобщенным программированием. Это отдельный вопрос, требующий детального изучения, и в случае недостатка знаний по данному вопросу предлагается подробно ознакомиться с реализацией идеи обобщенных алгоритмов в Java или C# соответственно в статьях [1] или [2].

2.1 Активная модель и представление

В описании оригинальной реализации MVC в Smalltalk упоминается о пассивной и активной модели. Пассивная модель не осведомлена о существовании представления, контроллера, и даже о своем участии в MVC-триаде. Контроллер отслеживает изменения модели и оповещает представление. При этом либо контроллер передает представлению информацию об изменениях, либо представление самостоятельно выбирает данные из модели. Более изящным решением является активная модель. Активность модели проявляется в ее праве самостоятельно оповестить представление об изменении своего состояния. Чтобы не нарушить основное требование MVC о независимости модели от представления и контроллера, механизм оповещения реализуется на основе шаблона проектирования Observer.

Шаблон проектирования Observer

ПРИМЕЧАНИЕ

Шаблон проектирования Observer (Обозреватель), или Publisher/Subscriber (Издатель/Подписчик), используется в случае, когда несколько объектов (подписчики) должны знать об изменениях состояния или некоторых событиях одного объекта (издателя), но требуется поддержать низкий уровень связности с подписчиками. Решение заключается в том, что подписчики регистрируются как обработчики некоторого события, генерируемого издателем. Так реализуется независимость наблюдаемого объекта (издателя) от обозревателей (подписчиков).

Немного отвлечемся от MVC и рассмотрим простую реализацию шаблона проектирования Observer, а также пример его использования. Исходный код данного примера на языке C# прилагается к статье, но детально рассматриваться не будет. Шаблоны описывают общие подходы к проектированию, которые не зависят от специфики конкретных языков программирования. Для понимания принципа вполне хватит и диаграммы классов, которая, правда, продемонстрирует часть кода в виде примечаний.


Рисунок 4. Диаграмма классов пространства имен Rsdn.Observer.

Подписчики, желающие знать о некоторых событиях издателя, должны реализовать интерфейс ISubscriber. То есть подписчики определяют реализацию метода Notification – обработчика события, посланного издателем.

Значительно сложнее логика издателя – абстрактного класса Publisher. Издатель производит подписку и отказ от подписки на оповещение о своих событиях соответственно в методах Subscribe и Unsubscribe. Регистрируясь в издателе, подписчик передает в качестве параметра subscriber метода Subscribe ссылку на себя. Издатель регистрирует подписчика в методе Subscribe, сохраняя ссылку на него в списке подписчиков – скрытом поле subscribers. Подписчик, не желающий более получать уведомления издателя, отказывается от подписки методом Unsubscribe. В данном методе издатель просто удаляет подписчика из списка subscribers и более не беспокоит его. Оповещение подписчиков производится в методе Notify – издатель последовательно вызывает метод Notification у всех подписчиков списка subscribers. Обратите внимание, что абстрактный класс Publisher не инициализирует оповещение подписчиковиздатель позволяет сделать это своим наследникам, для чего метод Notify объявлен защищенным (модификатор protected).

Рассмотрим простой пример использования шаблона проектирования Observer. Представьте, что есть объект, изменение определенного свойства которого является важным событием для некоторой группы других объектов вашего приложения. Например, в изменении текущего времени (свойства объекта Таймер) может быть заинтересован объект, отображающий часы. По данному событию он перерисует, к примеру, текущее положение секундной стрелки.

Издателем в примере выступает класс ConcretePublisher – наследник класса Publisher. Класс ConcretePublisher содержит скрытое поле state, которое он позволяет читать и изменять с помощью свойства State. Предположим, что в изменении данного свойства заинтересованы некоторые объекты. Пример подобного объекта – класс ConcreteSubscriber, реализующий интерфейс подписки ISubscriber. В конструкторе класса ConcreteSubscriber производится подписка на события издателя, ссылку на который подписчик запоминает в поле publisher. В методе Dispose, который класс ConcreteSubscriber наследует от интерфейса System.IDisposable, производится отказ от подписки. Любая попытка изменить свойство State класса ConcretePublisher помимо обновления значения поля state приведет к оповещению всех подписчиков через вызов наследованного от класса Publisher метода Notify (см. реализацию блока set свойства State). При этом подписчики поймут, что поле издателя state изменило свое значение, так как издатель произведет вызов их метода Notification (см. реализацию метода Notify класса Publisher).

ПРИМЕЧАНИЕ

Обратите внимание на ключевой момент шаблона Observer. Издателю вполне хватает минимального количества информации о подписчиках: для организации оповещения о событиях достаточно списка ссылок на реализации интерфейса ISubscriber.

ПРЕДУПРЕЖДЕНИЕ

Желание лаконично и наглядно изложить идею шаблона проектирования Observer привело к максимально простому примеру, приведенному выше, который содержит, хоть и локализованную, и, тем не менее – проектную ошибку. В частности, стоит остерегаться проектирования подписчиков, как показано на примере класса ConcreteSubscriber: нельзя допускать регистрацию подписки в конструкторе подписчика, как, впрочем, и снятие подписки в деструкторе подписчика, так как это может привести к проблемам «недействительного подписчика», утечки памяти и безопасности потоков. Проблемы проектирования, в том числе и упомянутые, шаблона Observer рассмотрел Брайан Гетц в публикации [6]. Не будем излишне его цитировать – предлагаем вам самостоятельно ознакомиться с данным материалом. В приведенном же примере данная проблема была локализована запретом наследования класса ConcreteSubscriber с помощью ключевого слова sealed.

Вернемся к MVC. В нашем случае издателем является модель, об изменениях которой должны узнать представления, ответственные за отображение данных. Наиболее полно описанный подход будет проиллюстрирован в реализации обобщенного MVC на языке программирования Java.

2.2 Представление и контроллер

В задачу контроллера входит преобразование действий пользователя в выполнение определенной операции над моделью. Наиболее подходящим решением является реализация шаблона проектирования Command.

Шаблон проектирования Command

ПРИМЕЧАНИЕ

Шаблон проектирования Command (Команда) (он же Action (Действие) или Transaction (Транзакция)) используется, если необходимо послать объекту запрос, не зная о том, выполнение какой операции запрошено, и кто будет ее получателем. Решение заключается в том, что запрос инкапсулирует объект Команда, который задает интерфейс выполнения операции, определяя тем самым связь между получателем и инициируемым действием. Инициатор создает объект Конкретная Команда и отправляет его в запросе на выполнение, клиент принимает запрос, устанавливает получателя запроса и преобразует объект Конкретная Команда в набор действий над получателем. Шаблон проектирования Command разрывает связь между объектом, инициирующим операции, и объектом, имеющим информацию о том, как операции выполнять. Кроме того, создается объект Команда, который можно расширять через наследование или делегирование.

Мы опять ненадолго отвлечемся от MVC, чтобы рассмотреть шаблон Command на простом примере. Исходный код данного примера на языке C# прилагается к статье, но по тем же причинам, что и в случае шаблона проектирования Observer, детально рассматриваться не будет.


Рисунок 5. Диаграмма классов пространства имен Rsdn.Action.Example.

Вспомните представителей руководящих должностей. С одной стороны, начальник желает управлять всеми подчиненными при помощи простых понятных ему требований. Но с другой стороны, он хотел бы как можно меньше знать, во-первых, о конкретных исполнителях своих указаний, а во-вторых, о том, какие именно действия будут предпринимать сотрудники, чтобы выполнить задание. Отдадим руководителям должное – у них и без того хватает проблем. Руководитель достигает желаемого результата, если строит работу своего отдела по тому же принципу, что и шаблон проектирования Command – в простейшем случае нанимает заместителя или секретаря.

В примере инициатор (класс Initiator) желает управлять некоторыми объектами, выполняющими функции включения и выключения агрегата, но не знает ничего о данных объектах, а значит, и о наборе действий, которые они должны предпринять для включения или выключения агрегата. Инициатор знает только две команды: включить и выключить – соответственно, методы On и Off класса Initiator. Перечисление Command представляет собой известные инициатору команды, соответственно: Command.On, Command.Off.

В этом желании инициатору помогает клиент (интерфейс IClient), в нашем примере реализующий данный интерфейс класс ConcreteClient – пример клиента. Клиент, с одной стороны, понимает командыинициатора, то есть знает, как конкретную командуинициатора преобразовать в конкретные указания исполнителям, а с другой стороны, знает, кто является при этом получателем указаний, то есть умеет определять исполнителя командыинициатора. Получателем в примере является класс Recipient, который позволяет включать и выключать агрегат посредством изменения своего свойства State.

Итак, инициатор передает клиентукоманду, которая известна только им двоим. Клиент устанавливает получателя и, в зависимости от команды (см. реализацию блока switch в методе Execute класса ConcreteClient), передает ему указания. Можно сказать, что клиент пересказывает командуинициатора в терминах получателя.

ПРИМЕЧАНИЕ

В очередной раз обращаем ваше внимание на то, что инициатор добивается выполнения своих простых команд, не имея информации ни о конкретных получателях (исполнителях), ни о наборе действий, которые они производят при этом. В этом заключается достоинство шаблона проектирования Command.

Вернемся к основной теме. Детально применение шаблона проектирования Command в обобщенном MVC будет рассмотрено при описании взаимодействия контроллеров и представлений, управляющих моделью, но не знающих, какие именно команды нужно отдавать модели.

2.3 Каркасы и шаблоны проектирования

Рассматриваемая в работе обобщенная реализация является примером построения каркаса приложения с графическим интерфейсом на основе шаблона проектирования MVC. Этот момент очень важен для четкого представления области применения предлагаемого решения.

Инструментальные библиотеки являются набором взаимосвязанных повторно используемых классов, предоставляющих определенный полезный функционал общего назначения. А каркасы являются набором взаимодействующих классов, составляющих повторно используемую архитектуру конкретного класса приложений. Таким образом, инструментальные библиотеки служат для повторного использования кода, а каркасы для повторного использования дизайна кода. Как следствие, принципиальным отличием является противоположное направление связей между приложением и используемым в его основе программным компонентом: код библиотеки внедряется в код проектируемого приложения, а использование в основе приложения каркаса приводит к тому, что код приложения вызывается из кода каркаса.

Использование соответствующих каркасов при проектировании приложений призвано ускорить процесс разработки за счет повторного использования реализованных в них проектных решений. При этом разработчику приходится в некоторой мере жертвовать свободой творчества, так как используемый каркас четко диктует определенную архитектуру приложения: общую структуру и разделение на классы, основной функционал и взаимодействие классов.

Например, инструментальной библиотекой является набор классов для работы со списками, который позволяет разработчику не изобретать заново повсеместно применяемые функции по обработке списковых данных.

Примером каркаса может являться обобщенная реализация шаблона проектирования. Например, это реализация шаблона проектирования Observer в Java на основе повторно используемых классов JDK (Java Development Kit): java.util.Observable и java.util.Observer. Класс Observable является обобщенной реализацией издателя, который содержит готовый функционал регистрации, снятия регистрации и оповещения подписчиков, реализующих интерфейс Observer. Используется данный каркас при проектировании приложения следующим образом: объект, в изменении состояния которого заинтересована некоторая группа других объектов, наследуется от класса Observable, а заинтересованные объекты реализуют интерфейс Observer. Аналогичным примером является этот же шаблон проектирования, встроенный в .NET: подписчик основывается на специальном функциональном члене – делегате, а издатель проектируется добавлением в класс специального члена – события, по которому компилятор автоматически добавляет в класс стандартный функционал издателя. Подробнее реализация делегатов и событий в .NET рассмотрена в публикации [7].

Несмотря на указанное выше сходство шаблонов и каркасов, между ними существуют принципиальные отличия:

  1. Шаблоны проектирования более абстрактны, нежели каркасы. Шаблон, описывающий принцип решения типичной задачи проектирования, необходимо реализовывать всякий раз, когда в нем возникает необходимость. Каркас же можно реализовать единожды и повторно использовать. В данной работе рассматривается реализация шаблона проектирования MVC в виде каркаса, который можно повторно использовать при написании приложений с графическим интерфейсом пользователя на платформах Java и .NET.
  2. Шаблоны проектирования являются более мелким архитектурным элементом, нежели каркасы. Так каркас может содержать несколько шаблонов, обратное же утверждение неверно. Обобщенный MVC включает шаблоны проектирования: Mediator, Observer и Command.
  3. Каркасы в отличие от шаблонов проектирования всегда создаются для конкретной предметной области. К примеру, достаточно сложно найти приложение, которое не нуждается в решении, которое дает шаблон проектирования Observer. Предлагаемый же пример построения каркаса на основе MVC имеет четкое назначение, и, несмотря на то, что автор постарался описать только общий подход и привести пример максимально универсального каркаса, стоит несколько раз подумать, полностью ли устраивает вас при решении конкретной прикладной задачи диктуемая данным каркасом архитектура.

Подробнее про шаблоны проектирования и каркасы вы можете прочитать в классической книге по шаблонам проектирования [8].

3 Реализация на Java

Благодарю Дениса Жданова за пересмотр кода.

Изначально идея обобщенного MVC была реализована автором в рамках проекта на языке программирования Java с использованием графической библиотеки SWT (Standard Widget Toolkit). Затем, при написании ASP.NET-приложения, библиотека была переписана на языке C#. В такой последовательности мы и пойдем.

СОВЕТ

Приводимый код вовсе не является готовым решением. Для наглядности указания направления, в котором, возможно, стоит двигаться, компоненты реального приложения были максимально упрощены, а демонстрационные примеры программ приведены для подтверждения верности выбранного решения.

Структура пакетов и классов проекта обобщенного MVC на языке Java имеет следующий вид:

Приведенные названия классов и сущностей будут активно использоваться в дальнейшем. Будем надеяться, что данный список поможет вам не запутаться в них.

ПРИМЕЧАНИЕ

Цитируемый ниже исходный код приведен в соответствие с рекомендациями Sun Microsystems по оформлению кода на языке программирования Java [9].

3.1 Модель

3.1.1 Подписчик модели

Подписчик модели – IModelSubscriber.java

Подписчик модели (интерфейс IModelSubscriber<P>) является основой событийной модели обобщенного MVC. Представления (подписчики), заинтересованные в уведомлениях об изменении модели (издателя), должны реализовать интерфейс IModelSubscriber<P>. Таким образом, представления определяют реализацию метода modelChanged – обработчика события, посланного моделью. Интерфейс IModelSubscriber типизирован свойством модели (параметр P).

3.1.2 Модель

Итак, модель должна быть типизирована по свойству модели, соответственно предоставлять возможность его чтения и изменения, и быть издателем, то есть предоставлять возможность подписки на событие о своем изменении и оповещать подписчиков в случае изменения свойства модели. Как вы уже поняли, изменение свойства модели трактуется как изменение модели.

Модель – Model.java

Модель в виде класса Model типизирована по свойству модели (параметр P), содержит свойство модели скрытым полем property и предоставляет возможность по его чтению и изменению соответственно методами getProperty и setProperty.

Свойство модели инициализируется в конструкторе класса Model значением, переданным параметром конструктора. К сожалению, компилятор Java не позволит нам создать свойство модели, то есть экземпляр обобщенного типа P, внутри модели.

ПРИМЕЧАНИЕ

Реализация обобщенных типов в Java не позволяет создавать экземпляр обобщенного типа внутри класса, который по данному типу ограничен. Другими словами, экземпляр обобщенного типа должен быть создан извне класса, который по нему ограничен. Это ограничение порождается ограничением Java Generic – нельзя создать объект, используя оператор new, тип которого является параметром Generic.

Модель инкапсулирует список подписчиков – скрытое поле subscribers. В качестве реализации списка выступает класс java.util.concurrent.CopyOnWriteArrayList – Брайан Гетц в публикации [6] рекомендует использовать его в качестве списка подписчиков: «Класс CopyOnWriteArrayList реализует List и является потокобезопасным, но его итераторы не выбросят ConcurrentModificationException и не требуют никаких дополнительных блокировок во время обхода».

Подписчики регистрируются в модели методом subscribe. Регистрация сводится к добавлению подписчика в список subscribers и принудительному его оповещению – вызов метода notifySubscriber. Таким образом, после регистрации в качестве подписчика моделипредставление сразу же получает оповещение и впервые отображает модель.

Кроме того, в методе subscribe осуществляется проверка существования подписки. При попытке повторной регистрации в модели, достаточно частой ошибке при проектировании приложений, в которых задействована событийная система, будет выброшено соответствующее сообщение в виде исключительной ситуации.

Снятие подписки осуществляется в методе unsubscribe – подписчик удаляется из списка и более не оповещается об изменениях данной модели. В методе также присутствует проверка корректной работы с классом.

Обратите внимание, что после установки нового свойства модели в методе setProperty происходит оповещение всех подписчиков об изменении модели вызовом метода notifySubscribers. Таким образом, все заинтересованные представления получают оповещения об изменении модели.

Модель или оповещает конкретного подписчика методом notifySubscriber, или оповещает всех своих подписчиков методом notifySubscribers. Оповещение подписчиков осуществляется последовательным оповещением каждого – опять же методом notifySubscribers. Оповещение подписчика сводится к вызову его метода modelChanged, который он должен реализовывать, будучи наследником подписчика модели IModelSubscriber<P>.

Кроме того, модель переопределяет наследуемый от суперкласса Object метод toString, в котором возвращает результат выполнения одноименного метода свойства модели. В дальнейшем данный метод могут использовать представления для отображения модели с помощью текстовых примитивов.

3.1.3 Модель списка

Бизнес-логика приложений чаще оперирует коллекцией элементов, нежели просто одиночным элементом, поэтому необходимо построить так называемую модель списка. Прежде всего, это модель, то есть наследник класса Model<P>. В качестве свойства модели выступает список моделей. Модель списка, с одной стороны, должна подписываться на оповещения об изменении каждой элементарной модели, входящей в список, а с другой стороны, выступать издателем, то есть предоставлять возможность подписки на событие о своем изменении и оповещать подписчиков в случае изменения какой-либо элементарной модели списка. Таким образом, модель списка будет чувствовать изменение любой элементарной модели списка и транслировать это изменение как собственное. Кроме того, разумеется, модель списка должна уметь добавлять модели в список и удалять их из него.

ПРИМЕЧАНИЕ

Такая архитектура позволит работать со списком моделей как с обычной моделью.

Модель списка – ListModel.java

Модель списка (класс ListModel<P>) является наследником модели (класс Model<P>), которой в качестве свойства модели (параметр P класса Model) подставлена коллекция моделей – Collection<Model<P>>. С помощью такой конструкции мы получаем компонент, обладающий всеми характеристиками модели, в котором свойством модели является коллекция моделей.

Класс ListModel<P> типизирован свойством модели, которая может храниться в списке.

ПРИМЕЧАНИЕ

Обратите внимание, что в данном случае класс ListModel<P>, а в дальнейшем и все классы обобщенного MVC, работающие с моделью (классом Model<P>), перенимают его типизацию по свойству модели (параметр P).

Будучи наследником класса Model<P>, класс ListModel<P> автоматически инициализирует в конструкторе своего предка свойство модели (скрытое поле property) набором моделей – HashSet<M>.

Кроме того, модель списка подписывается на события изменения элементарных моделей своего списка, для чего реализует интерфейс IModelSubscriber<P>. При поступлении события изменения элементарной модели списка (метод modelChanged) ListModel<P> просто инициирует событие о собственном изменении вызовом наследованного от класса Model метода notifySubscribers. Таким образом, все заинтересованные представления получают оповещения об изменении данных списка.

При добавлении модели в список (метод add) модель списка подписывается на прием событий изменения добавленной модели, а также инициирует рассылку события об изменении своего состояния. А при удалении модели (метод remove) модель списка снимает подписку с модели, удаляет эту модель из списка и также оповещает подписчиков о своем изменении. Поэтому представления всегда узнают об изменении структуры списка.


Рисунок 6. Диаграмма классов пакета ru.rsdn.mvc.model.

3.2 Контроллер

Инициатором запроса выполнения операции над моделью выступает часть представления, ответственная за регистрацию действий пользователя над экранной формой, отображающей данную модель. Контроллер принимает запрос и преобразует указанный в нем тип действия в соответствующее изменение модели.

ПРИМЕЧАНИЕ

Далее инициируемые представлением команду или запрос действия над моделью будем называть просто операцией. Под термином действия будем понимать конкретные манипуляции, выполняемые контроллером над моделью.

3.2.1 Абстрактный контроллер

Абстрактный контроллер – IController.java

Абстрактный контроллер в виде интерфейса IController<O, M, P> ограничен по набору операций (параметр O), модели (параметр M) и свойству модели (параметр P).

Набор операций является перечислением операций, которые контроллер знает, как преобразовать в действия над моделью. Итак, интерфейс IController<O, M, P> определяет контроллер, который может выполнять операцию (метод execute) указанного типа (параметр operation) над указанной моделью (параметр model) с дополнительным атрибутом (параметр attribute), являющимся свойством модели.

В приложении все действия над данными можно свести к трем категориям: редактирование, добавление и удаление. Напомним, что такие действия как загрузка или сохранение данных не рассматриваются шаблоном MVC, так как являются действиями, выполняемыми на уровне доступа к данным. В нашем случае редактирование применимо к модели, а добавление и удаление совершаются только над моделью списка. Таким образом, контроллер должен быть ограничен по операции над моделью – редактирование, а контроллер списка – по операциям над моделью списка: добавление, удаление.

Дополнительный атрибут операции (параметр attribute метода execute) будет уточнять производимое действие. При редактировании моделиконтроллер должен знать новое свойство модели. При добавлении моделиконтроллеру необходимо передать свойство модели, которое будет установлено во вновь созданной и добавленной в список модели по умолчанию. А при удалении контроллер должен получить модель, которую необходимо исключить из списка.

3.2.2 Контроллер

Контроллер – Controller.java

Контроллер (класс Controller<P>) реализует интерфейс IController<O, M, P>. В качестве операции (параметр O интерфейса IController<O, M, P>) подставляется перечисление Controller.O с единственным членом EDIT – операция над моделью: редактирование. В качестве модели (параметр M интерфейса IController<O, M, P>) подставляется модель, параметр M уточненный наследованием Model<P>. Как и ранее ограничение по свойству модели (параметр P интерфейса IController<O, M, P>) просто наследуется.

В качестве атрибута операции (параметр attribute метода execute) контроллер ожидает новое свойство модели, которое необходимо установить в редактируемую модель, поэтому класс Controller, реализуя интерфейс IController<O, M, P>, подставляет в качестве типа параметра attribute метода execute тип свойства модели P.

Для исключения неправильного использования контроллера, как и ранее, используются проверки с выбросом соответствующих исключительных ситуаций.

Блок switch осуществляет преобразование типа операции в соответствующие действия над моделью. В случае с контроллером ограниченным по операциям над моделью получается всего одна секция case, в которой описано редактирование моделисвойство модели переписывается новым значением. Достижение секции default возможно, к примеру, в том случае, если программист, расширив набор операций добавлением нового элемента в перечисление Controller.O, забыл задать для новой операции соответствующие действия над моделью в секции case. В этом случае программист будет предупрежден о своей ошибке еще на этапе отладки. Уместность такой относительно громоздкой конструкции станет наиболее наглядна при большем количестве операций – в контроллере списка.

ПРИМЕЧАНИЕ

Напомним, что при установке нового свойства модели (см. реализацию метода setProperty класса Model) происходит генерация события об изменении данной модели. Таким образом, после изменения моделиконтроллером происходит оповещение всех заинтересованных в этом представлений.

3.2.3 Контроллер списка

Контроллер списка – ListController.java

Контроллер списка (класс ListController<P>), как и контроллер (класс Controller<P>), реализует интерфейс IController<O, M, P>. В качестве операции (параметр O интерфейса IController<O, M, P>) подставляется перечисление ListController.O с двумя элементами: ADD и REMOVE – соответственно операции над моделью списка: добавить и удалить. В качестве модели (параметр M интерфейса IController<O, M, P>) подставляется модель списка ListModel<P>, а в качестве свойства модели списка (параметр P интерфейса IController<O, M, P>) коллекция моделей – Collection<Model<P>>.

Контроллер списка (класс ListController<P>) в отличие от контроллера (класса Controller<P>) в качестве атрибута операции (параметр P интерфейса IController<O, M, P>) ожидает коллекцию моделей, то есть при реализации интерфейса IController<O, M, P> подставляет в качестве типа параметра attribute метода execute тип коллекции моделей Collection<Model<P>>. Контроллеру списка можно командовать на выполнение операции и над одной моделью, для этого мы перегрузили метод execute (см. второй вариант метода).

Исключение неправильного использования контроллера производится как и ранее.

Для преобразования типа операции в соответствующие действия над моделью также используется блок switch. В случае добавления в секции case, соответствующей операции ADD, модель, указанная параметром attribute, добавляется в модель списка model. Казалось бы, удобнее передать контроллерусвойство модели: контроллер списка создал бы новую модель, установил в нее переданное свойство модели по умолчанию и добавил инициализированную подобным образом модель в модель списка. Но, как уже отмечалось ранее, в Java подобное реализовать невозможно вследствие специфики реализации обобщенных типов: внутри контроллера списка мы не сможем создать модель – экземпляр обобщенного типа M.

ПРИМЕЧАНИЕ

Обратите внимание, что при добавлении модели, переданной параметром attribute, в модель списка model (см. реализацию метода add класса ListModel<P>), кроме прочего, model подписывается на события изменения модели и принудительно вызывает оповещение о собственном изменении. Таким образом, после добавления модели произведенного контроллером списка происходит оповещение всех заинтересованных в этом представлений.

В случае удаления ожидается модель, которую необходимо исключить из списка. В секции case, соответствующей операции REMOVE из модели списка model удаляется указанная параметром attribute модель.

ПРИМЕЧАНИЕ

Обратите внимание, что при удалении модели, переданной параметром attribute, из модели списка model (см. реализацию метода remove класса ListModel<P>), кроме прочего, model снимает подписку на события изменения удаляемой модели и принудительно вызывает оповещение о собственном изменении. Таким образом, после удаления модели произведенного контроллером списка, во-первых, модель списка перестает чувствовать изменения удаленной модели, а во-вторых, происходит оповещение всех заинтересованных в этом представлений.


Рисунок 7. Диаграмма классов пакета ru.rsdn.mvc.controller.

3.3 Представление

Представление должно реагировать на изменения модели для чего подписывается на прием от нее уведомлений. Характер реакции представления на изменение модели описывает разработчик в конкретном экземпляре представления. Кроме того, представление должно уметь корректно переключаться с отображения одной модели на отображение другой. К примеру, представление, отображающее содержимое каталога в Проводнике Windows, при выборе в древовидном списке каталога переключается с отображения предыдущего выбранного каталога на текущий. Вследствие того, что представление ограничено по модели, представление может переключаться на отображение модели только того же типа.

3.3.1 Базовое представление

Базовое представление – BaseView.java

Базовое представление в виде класса BaseView<M, P> ограничено по модели (параметр M) и свойству модели (параметр P). Представление содержит модель скрытым полем model и предоставляет возможность по его чтения и изменения соответственно с помощью методов getModel и setModel.

При установке модели в методе setModel прежде всего представление снимает подписку с ранее установленной модели методом unsubscribe, если такая была, и подписывается на уведомления новой модели методом subscribe.

В методе dispose представление снимает подписку с модели. Данный метод необходимо вызвать перед завершением работы представления для продолжения корректной работы модели.

Чтобы иметь возможность подписки на события модели класс BaseView<M, P> наследует интерфейс IModelSubscriber<P>, но не определяет реализацию метода modelChanged данного интерфейса: характер реакции представления на изменения модели будет описан разработчиком конкретного экземпляра представления – для этого класс BaseView<M, P> объявлен абстрактным (модификатор abstract).

ПРИМЕЧАНИЕ

Обратите внимание, что, подписываясь на события модели в методе subscribe, представление автоматически получает уведомление от модели методом modelChanged (см. реализацию метода subscribe класса Model<P>), что дает возможность представлению впервые отобразить желаемые данные.


Рисунок 8. Диаграмма классов базового представления, модели и подписчика модели.

3.3.2 Представление

Представление – View.java

Представление (класс View<P>) демонстрирует совместную работу компонентов триады MVC. При наследовании класса BaseView<M, P> класс View<P> в качестве модели (параметр M класса BaseView<M, P>) подставляется Model<P>. Как и ранее ограничение по свойству модели (параметр P класса BaseView<M, P>) просто наследуется.

Класс View<P> содержит контроллер (класс Controller<P>) скрытым полем controller, которое инициализируется при создании экземпляра класса представления и используется на протяжении всей его жизни.

Контракт представления и контроллера обуславливает всего одно действие, которое может инициировать представление – редактирование. Запрос на редактирование модели описан в методе edit: представление обращается к контроллеру, указывая в запросе операции ее тип – EDIT, целевую модель полученную наследованным от предка методом getModel и новое значение свойства модели, которым модель должна быть обновлена. Новое значение свойства модели должно быть сформировано разработчиком в реализации конкретного представления значением по умолчанию или запрошенным у пользователя.


Рисунок 9. Диаграмма классов представления, контроллера и модели.

3.3.3 Представление списка

Представление списка – ListView.java

Представление списка (класс ListView<P>) демонстрирует совместную работу компонентов MVC-триады, работающих со списками: модели списка и контроллера списка. Для этого класс ListView<P> наследуется от класса BaseVie<M, P> с подстановкой ограничений: в качестве модели (параметр M класса BaseView<M, P>) модель списка ListModel<P>, а в качестве свойства модели (параметр P класса BaseView<M, P>) коллекция моделей – Collection<Model<P>>. Такое наследование обуславливает то, что представление списка инкапсулирует модель списка.

Представление, работающее со списком, должно уметь не только добавлять и удалять элементы списка, но и редактировать их. Поэтому ListView<P> содержит и контроллер (скрытое поле controller), и контроллер списка (скрытое поле listController). Представление инициализирует контроллеры единожды – при инициализации.

Запрос на редактирование осуществляется аналогично подобному запросу в представлении (см. реализацию метода edit в классе View<P>) за одним исключением: представление должно само определить редактируемую модель. В метод, кроме нового значения свойства модели передается и целевая модель. Критерием определения редактируемой модели обычно выступает выделение элемента графической формы списка, которая является отображением одной модели из списка.

Запрос на добавление описывается в методе add. Представление обращается к контроллеру списка, указывая в запросе операции ее тип – ADD, модель списка, в которую будет добавляться новая модель, и саму модель. Модель можно инициализировать по умолчанию или на основании указанного пользователем значения свойства модели в определенном диалоге программы.

Запрос на удаление описывается в методе delete. Представление обращается к контроллеру списка, указывая в запросе операции ее тип – REMOVE, модель списка, из которой будет исключаться модель, и удаляемую модель списка. Как и в случае редактирования, представление определяет удаляемую модель самостоятельно, критерий обычно такой же.


Рисунок 10. Диаграмма классов представления списка, контроллера списка и модели списка.

Итак, мы получили компоненты классического MVC: модель (класс Model<P> и интерфейс IModelSubscriber<P>), контроллер (интерфейс IController<O, M, P>) и представление (класс BaseView<M, P>). Остальные классы являются примерами использования описанного подхода, которые автор попытался для наглядности сделать максимально простыми – в реальности данные классы требуют значительной доработки.

Повторим основные тезисы, заложенные в разработку.

Контракт представления и контроллера, которые желают работать с одним и тем же типом модели, реализуется на основе ограничения по модели, то есть, во-первых, исключается возможность частичного использования компонентов MVC – представление и контроллер не могут работать без модели, а во-вторых, модель выступает посредником между представлением и контроллером с одной стороны и данными предметной области, которые представляет свойство модели, с другой стороны. Обратите внимание, что представление и контроллер независимы, несмотря на ограничение по модели.

Событийный уровень взаимодействия модели с представлением «скрыт» в реализации базового представления, то есть по умолчанию разработчик не обременяется заботами достаточно сложного событийного уровня MVC. Разработчику достаточно описать в представлении обработку события изменения модели (реализацию метода modelChanged) и передать представлению ссылку на модель – готово.

4 Реализация на C#

Благодарю Андрея Корявченко за советы по технологии привязки данных .NET.
СОВЕТ

Надеемся, что Java-разработчики не обделят вниманием и данный раздел статьи, в котором, прежде всего, будут рассмотрены отличия платформ Java и .NET в инструментарии решения поставленной задачи. Кроме того, предполагается, что .NET-разработчики хотя бы бегло ознакомились с ключевыми моментами предыдущего раздела – комментарии по коду на C# будут касаться в основном только отличий.

4.1 Отличия

В целом реализация на C# отличается от приведенной выше большей лаконичностью кода. Рассмотрим основные различия подробно.

4.1.1 Привязка данных

Благодаря тому, что при построении обобщенного MVC на Java мы использовали только стандартный инструментарий JDK версии 1.5, решение можно использовать совместно с любой графической библиотекой, будь то Swing, SWT или иная. Такая гибкость очень важна для богатого opensource-проектами мира Java.

В мире .NET в этом отношении у нас практически нет выбора, по разумеющимся причинам в построении графических настольных приложений доминирует графическая библиотека WinForms, а Интернет-приложения строят на основе ASP.NET. Поэтому логично будет сразу обобщенный MVC ориентировать на использование уже готового богатого инструментария .NET, в частности предлагаемой технологии привязки данных (data binding). Благодаря использованию данной технологии, значительно сократится код и станет возможной интеграция обобщенного MVC на C# со стандартными технологиями .NET.

4.1.2 Событийная модель

Событийная модель Java основана на анонимных классах (реализациях интерфейсов), определяющих набор методов-обработчиков, объявленных как ожидаемые события.

В принципе, в C# можно построить событийную модель по такому же принципу, тем не менее, в .NET доминирует модель событий, основанная на специальном функциональном типе – делегате, который предопределяет действия подписки, снятия подписки и оповещения подписчиков – действия, которые в Java мы производили самостоятельно. Поэтому, возможно, в реализации на C# присутствие шаблона проектирования Observer будет не столь явным.

4.1.3 Обобщенное программирование

Компилятор Java для обобщенных типов использует так называемое затирание типов (type erasure): информация о специализирующем типе доступна только компилятору и во время выполнения отсутствует, да и рефлексивно она доступна только для статических объектов. Одно из неприятных следствий этого мы уже обсуждали выше: мы не можем создавать экземпляр обобщенного типа, так как во время выполнения у виртуальной машины отсутствует информация о том, какой это тип. Для runtime-поддержки родовых типов необходимо было расширить набор инструкций виртуальной машины – Sun Microsystems признало это решение неприемлемым: это могло значительно осложнить обновление производителям виртуальных машин Java. Подробнее с особенностями обобщенного программирования Java можно ознакомиться в публикации [10].

Будучи монопольным разработчиком .NET, Microsoft не была связана подобными ограничениями и реализовала более изящную систему обобщенных типов. В .NET 2.0 информация об обобщенных типах полностью сохраняется во время выполнения, более того среда выполнения .NET может даже динамически создавать новые специализации обобщенных типов во время выполнения. Нам, прежде всего, важно то, что .NET позволяет создавать экземпляр обобщенного типа. За счет этой возможности мы оптимизируем код модели и контроллеров.

4.1.4 Отладка

C#, в отличие от Java, поддерживает условную компиляцию с использованием директив препроцессора. Таким образом собирается отладочная сборка (debug), которая может отличаться от окончательной сборки (release), к примеру, наличием проверок допущений, осуществляемых классом System.Diagnostics.Debug. Отличие утверждений (assert) Java в том, что компилятор всегда включает их в байт-код программы: по умолчанию при выполнении виртуальная машина их игнорирует, но они могут быть включены по требованию – запуску программы с соответствующим ключом виртуальной машины.

И последнее. Доступ к полям классов в Java осуществлялся с помощью методов getter и setter. В C# для этого удобнее пользоваться свойствами с соответствующими секциями get и set.

В проекте обобщенного MVC на языке C# пространства имен классов (namespace) строятся аналогично пакетам языка Java (package), которые загрузчик классов по умолчанию жестко привязывает к структуре каталогов, в которых располагаются файлы классов. То есть пространство имен каждого класса соответствует размещению файла класса в каталоге проекта. Структура пространства имен и классов проекта обобщенного MVC на языке C# имеет следующий вид:

ПРИМЕЧАНИЕ

Цитируемый исходный код на языке C# приведен в соответствие с соглашением по оформлению кода, принятым на RSDN [11].

4.2 Модель

Модель – Model.cs

Модель (класс Model<P>) типизирована по свойству модели (параметр P). Модель содержит свойство модели в скрытом поле property и предоставляет возможности его чтения и изменения через свойство Property.

В отличие от соответствующего класса на Java, который принимал значение свойства модели параметром конструктора, здесь при создании экземпляра модели инициализируется свойство модели – создается экземпляр обобщенного типа P. Компилятор C# позволяет создавать экземпляр обобщенного типа внутри класса, параметризованного этим типом, только при указании для него уточнения new. Обратите внимание на уточнение where в объявлении класса Model<P>. Данное уточнение говорит о том, что параметр типа P должен реализовывать публичный конструктор, не имеющий параметров.

ПРЕДУПРЕЖДЕНИЕ

Это означает, что класс, который вы хотите использовать в качестве свойства модели, должен определить публичный конструктор по умолчанию. Иначе компилятор будет выдавать ошибку при попытке подстановки данного класса в качестве параметра класса Model<P>. Кроме того, все участники MVC-триады, работающие с моделью (классом Model<P>), перенимают ее типизацию по свойству модели (параметр P) – и, как следствие, должны приводить для данного параметра (свойства модели) уточнение new.

Шаблон проектирования Observer реализуется на основе интерфейса System.ComponentModel.INotifyPropertyChanged. В принципе, это функциональный аналог интерфейса IModelSubscriber<P>, описанного в реализации на Java, но, что важно – данный класс участвует в связывании данных .NET. Предполагается, что объект, являющийся моделью предметной области, наследует данный интерфейс для участия в связывании данных – определяет критерий возникновения события изменения собственного состояния. Реализованные в .NET так называемые клиенты связывания, прослушивают данное событие и в случае его возникновения производят соответствующие действия, к примеру, обновляют графические элементы, отображающие данные.

Класс Model<P> наследует от интерфейса INotifyPropertyChanged событие PropertyChanged основанное на делегате System.ComponentModel.PropertyChangedEventHandler, который предопределяет все действия по подписке, снятии подписки с модели. Нам остается только определить метод OnPropertyChanged, в котором модель будет оповещать всех своих подписчиков об изменении. Как и ранее модель вызывает данный метод при изменении свойства модели (блок set свойства Property).

4.2.1 Модель списка

За основу модели списка мы возьмем класс System.ComponentModel.BindingList ответственный за связывания списков данных. С одной стороны, BindingList<T> – это типизированная коллекция объектов типа T, то есть реализация интерфейсов System.Collections.ICollection (модификация коллекции), System.Collections.IList (индексированная коллекция – список) и System.Collections.IEnumerable (последовательный перебор элементов коллекции) и других, а с другой стороны, поддерживает связывание списка данных, реализуя интерфейс System.ComponentModel.IBindingList.

Интерфейс IBindingList определяет событие ListChanged, которое возбуждается классом при любом изменении списка. Кроме того, если элемент списка реализует интерфейс INotifyPropertyChanged, то BindingList<T> подписывается на событие PropertyChanged каждого объекта входящего в список и при его возникновении инициирует событие о собственном изменении – ListChanged. Таким образом, при подстановке в качестве параметра класса BindingList<T> – класса Model<P>, полученный класс BindingList<Model<P>> становится полным аналогом модели списка описанной ранее, но с более богатым функционалом в рамках технологии связывания данных .NET.

Модель списка – ListModel.cs

Класс ListModel<P> является наследником BindingList<Model<P>>, который перенимает типизацию класса Model<P> и как следствие обязан указать для параметра уточнение new.


Рисунок 11. Диаграмма классов пространства имен Rsdn.Mvc.Models.

4.3 Контроллер

Контроллер – Controller.cs

Контроллер (класс Controller<P>) практически идентичен аналогичному классу на Java.

4.3.1 Контроллер списка

Контроллер списка – ListController.cs

Контроллер списка (класс ListController<P>) отличается от соответствующего класса на Java несколькими моментами.

При добавлении контроллер списка ожидает получить в параметре attribute новое свойство модели, которое должно быть установлено по умолчанию в созданной и добавленной в список модели. В Java же контроллер списка ожидал уже готовую модель – мы были ограничены невозможностью создания экземпляра модели, как обобщенного типа.

Для создания новой модели в списке используется метод интерфейса IBindingList – AddNew, который выполняет, кроме прочего, действия, которые в Java мы описывали самостоятельно: создает экземпляр класса Model<P>, добавляет его в коллекцию, подписывается на событие PropertyChanged данного объекта.


Рисунок 12. Диаграмма классов пространства имен Rsdn.Mvc.Controllers.

4.4 Представление

Представление – View.cs

Представление (класс View<P>) принципиально ничем не отличается от аналога на Java.

4.4.1 Представление списка

Представление списка – ListView.cs

Представление списка (класс ListView<P>) также повторяет описанный ранее Java-класс.


Рисунок 13. Диаграмма классов пространства имен Rsdn.Mvc.Views.

5 Примеры

Исходный код демонстрационных программ: Java SWT | WinForms и ASP.NET

Итак, мы у цели. Теперь можно рассмотреть примеры использования описанных выше классов при создании графических приложений. Код примеров в отличие от кода ядра обобщенного MVC благодаря проделанной работе тривиален, потому будет приведен лишь обзор соответствующих проектов.

Использование обобщенного MVC сводится к следующим шагам. Во-первых, разработчик определяет свойство модели – любой класс, представляющий данные предметной области и наполненный определенной бизнес-логикой приложения, так называемую модель предметной области (Domain Model). В дальнейшем данный класс будет подставляться в качестве параметра P класса Model – модель готова. Во-вторых, разработчик определяет представление, которое будет отображать данную модель. Класс представления наследуется от класса View, которому в качестве параметра P подставляется свойство модели, определенное на предыдущем шаге. Если необходимо работать со списком данных предметной области, разработчик определяет представление, которое наследуется от класса ListView<P>. В качестве параметра P класса ListView подставляется тот же тип – свойство модели. Остальные задачи: выбор экранной формы отображения данных модели, инициализация ее, а также реакция экранной формы на изменение отображаемой модели методом modelChanged (Java) или реализация привязки данных методом DataBind (C#) – полностью лежат на разработчике.

Свойство модели, которое необходимо модели, программист в любом случае формирует – это данные предметной области и логика разрабатываемого приложения. А предлагаемый framework со своей стороны по умолчанию побуждает разработчика выделять данные предметной области и по необходимости бизнес-логику приложения в отдельный компонент, чем по умолчанию уменьшает связность. С одной стороны, обобщенный MVC оставляет задачи создания визуальных форм или Интернет-страниц представлений программисту, но, с другой стороны, обеспечивает двустороннюю связь отображения с данными приложения (отображение не только может изменять данные, но и обновляется при изменении данных) не обременяя разработчика тонкостями относительно сложной событийной модели MVC.


Рисунок 14. Диаграмма класса Rsdn.Mvc.Example.Switch.

Тестовая модель примера будет реализовывать переключатель, который может находиться в двух состояниях: выключен или включен. Модель (класс Model<P>), ограниченная в рамках описанного выше подхода по свойству модели (в примере класс Switch) представляет модель переключателя: Model<Switch>. Представление переключателя является наследником класса View<Switch>, а представление списка переключателей – наследником класса ListView<Switch>. Отличие использования обобщенного MVC на языке программирования Java только в том, что при создании экземпляра класса Model<Switch> необходимо передать уже сформированный объект Switch – модель в конструкторе инициализирует свойство модели переданным значением.

Каждый из примеров иллюстрирует работу, во-первых, с переключателем, а во-вторых, со списком переключателей. Пример на Java SWT, кроме того, иллюстрируют работу нескольких представлений с одной и той же моделью – задача, с которой MVC справляется наиболее изящно. Там же есть иллюстрации использования, так называемого, интеллектуального контроллера, который позволяет производить отмену произведенных действий – Undo. Интеллектуальный контроллер будет описан ниже – в разделе «Внедрение». Операции над переключателями в примерах сводятся к редактированию переключателя, добавлению и удалению переключателя из списка.

Пример на Java SWT представляет собой проект Eclipse SDK 3.3.0 (Europe) под названием MVC RSDN. Проект включает классы обобщенного MVC (см. раздел «Реализация на Java») и примеры.

Пример на .NET оформлен в виде решения (solution) Microsoft Visual Studio 2008, названного аналогично – MVC RSDN, и содержит следующие проекты: MVC (Обобщенный MVC, см. раздел «Реализация на C#»), Switch (библиотека класса свойства модели переключателя), MVC Forms Example (пример на графических формах Windows) и MVC Web Example (пример в виде ASP.NET-приложения). Кроме того, MVC RSDN включает проекты: Observer и Action – примеры реализации одноименных шаблонов проектирования, которые рассматривались в разделе «Обобщенный MVC».

5.1 Java SWT

Структура пакетов и классов проекта MVC RSDN имеет следующий вид:

Классы SwitchDemo, ListSwitchDemo, TwoListSwitchDemo, IntellectualSwitchDemo и IntellectualListSwitchDemo являются запускаемыми – содержат main методы.

В каталоге doc расположена документация Javadoc.

5.2 Windows Forms

Класс Switch (свойство модели переключателя) размещен в отдельной библиотеке, чтобы обеспечить использование не только в WinForms примере, но также и в примере на ASP.NET. Проект Switch содержит всего один класс:

Структура пространства имен и классов проекта MVC Forms Example имеет следующий вид:

Классы SwitchForm и ListSwitchForm являются запускаемыми – содержат Main методы.

5.3 ASP.NET

Основной код ASP.NET располагается в каталоге App_Code. Структура пространства имен и классов проекта MVC Web Example имеет следующий вид:

Кроме того, в каталоге Controls располагаются Web User Control: SwitchControl.ascx/SwitchControl.ascx.cs и ListSwitchControl.ascx/ListSwitchControl.ascx.cs – каждый из которых подключается соответственно в страницах приложения: Switch.aspx и ListSwitch.aspx.

6 Внедрение

Для наглядности идеи обобщенного MVC и области ее применения были приведены достаточно тривиальные примеры. Построение реального приложения на основе обобщенного MVC поставит перед разработчиком дополнительные задачи. Рассмотрим основные проблемы, которые вставали перед автором статьи при внедрении обобщенного MVC на Java с использованием графической библиотеки SWT, и методы их решения.

СОВЕТ

Решения, что приводятся в виде исходного кода, основаны на примере Java SWT. Советуем прежде ознакомиться с исходным кодом данного примера.

6.1 Интеллектуальный контроллер

Представьте следующий вариант работы примера с переключателем. Переключатель находится во включенном состоянии (текстовая форма представления отображает «Включен»), а пользователь нажимает кнопку «Включить». В данном случае по логике ничего происходить не должно – и, действительно, визуально, казалось бы, ничего не изменяется – метка по прежнему отображает текст «Включен». На самом деле это не так, просто пользователь благодаря инерции своего зрения не успевает заметить мерцания при обновлении текстовой формы на экране. Обновление экранной формы все же происходит – по отработанной в MVC схеме: представление, не задумываясь, шлет команду контроллеру на включение переключателя; контроллер достаточно бездумно выполняет указание; модель, полагая, что изменилась, шлет уведомление представлению – оно обновляет текстовую метку тем же текстом, что был и ранее.

Как видите, это холостой цикл работы, который, тем не менее, требует ресурсов. Кроме того, если бы графическая форма нашего представления была более сложной, к примеру, включала таблицу, то очень вероятно, что мерцание при холостом обновлении мы бы все же заметили.

Действия контроллера над моделью сводятся к изменению ее свойства. Стоит ли изменять свойство модели, если старое свойство эквивалентно новому? Ведь это не просто лишняя операция – изменение свойства модели вызывает реакцию всех элементов MVC-триады: якобы изменившись, модель рассылает уведомления всем заинтересованным представлениям, представления обновляют экранные формы, рассчитывая отобразить пользователю изменение данных. Во-первых, попробуем избавить MVC от выполнения подобных холостых циклов.

Во-вторых, продемонстрируем, как легко расширяется функциональность контроллера, который представляет реализацию шаблона проектирования Command – научим контроллер хранить историю изменений и предоставлять возможность последовательно отменять последние произведенные действия. Назовем новую реализацию контроллера скромно – интеллектуальной.

Первая задача решается несложно. При редактировании контроллер должен проверить эквивалентность текущего свойства модели и указанного нового свойства модели. Только в случае их различия контроллер устанавливает в модель новое свойство.

Чтобы научить контроллер отменять произведенные действия, необходимо, прежде всего, расширить набор его операций, добавив операцию отмены – Undo. То есть контроллер будет уметь только отменять произведенные действия. По аналогии вы можете сами реализовать, к примеру, функционал действия повторения – Redo. Кроме того, контроллер должен будет протоколировать каждое свое действие, формируя, таким образом, историю, которую при необходимости сможет использовать, чтобы последовательно отменять произведенные ранее действия. По разумеющимся причинам протоколировать отмену действия контроллеру смысла нет.

Наиболее удобным вариантом хранилища для истории в нашем простом примере является стек.

ПРИМЕЧАНИЕ

Стек (англ. stack – стопка) – структура хранения данных, подмножество структур типа «список» со специфическим методом доступа к элементам. Метод доступа к элементам стека в одной фразе можно определить как «последним пришел – первым вышел» (LIFO, Last In – First Out) или «первым пришел – последним вышел» (FILO, First In – Last Out). Добавление элемента возможно только в вершину стека (добавленный элемент становится первым в стеке), удаление – также только из вершины стека. Операцию добавления элемента в стек принято называть словом «push», извлечения — «pop».

Перед каждым действием над моделью, исключая откат, контроллер будет класть в стек значение свойства модели до редактирования. Чтобы откатиться на один шаг назад контроллеру потребуется только извлечь из стека старое значение свойства модели и обновить им модель. После этого благодаря устройству стека последним в истории окажется свойство модели предыдущего шага. Таким образом, контроллер может отменять произведенные действия до тех пор, пока стек не окажется пустым – это будет означать, что все произведенные действия были отменены.

Рассмотрим пример реализации интеллектуального контроллера – класс Controller<P>. Кроме того, в демонстрационных программах и исходных кодах на языке программирования Java показано, как можно, к примеру, реализовать интеллектуальный контроллер списка (класс ListController<P>) по аналогии описанному здесь.

Интеллектуальный контроллер – Controller.java

При редактировании контроллер проверяет эквивалентность текущего свойства модели и нового свойства модели переданного параметром attribute. Если они различны, контроллер устанавливает в модель новое свойство. Но в данном виде контроллер все равно каждый раз будет изменять модель. Дело в том, что метод equals, наследованный от класса java.lang.Object, возвращает true только для сравнения объекта с самим собою. Для корректного сравнения необходимо переопределить метод equals в классе свойства модели. Например, метод equals может быть переопределен, как это сделано в классе Switch.

Переключатель с переопределенным методом equals – Switch.java

6.2 Клонирование моделей

В приложении достаточно часто возникает необходимость скопировать данные, чтобы создать на их основе новые. Например, учетную запись со сведениями о человеке проще создать как копию аналогичной учетной записи его родственника и изменить в новой записи только часть данных. Эту задачу разумнее всего переложить на модель, то есть модель должна реализовать возможность клонирования. В случае языка программирования Java класс Model<P> должен реализовывать интерфейс java.lang.Cloneable и переопределить метод clone, наследуемый от класса Object. Метод clone в классе Model<P> необходимо объявить абстрактным – реализация метода будет описана в конкретной модели.

Модель с возможностью клонирования – Model.java

Например, для клонирования переключателя приведенный в примере класс SwitchModel должен выглядеть так, как показано ниже.

Модель переключателя с возможностью клонирования – SwitchModel.java

6.3 Уровень доступа к данным

Напомним, что шаблон MVC не рассматривает уровень доступа к данным: модель всего лишь представляет бизнес-логику приложения. Тем не менее, при написании реального приложения необходимо, во-первых, при запуске программы или по требованию пользователя загрузить модели данными из хранилища, а во-вторых, сохранить измененные данные моделей в хранилище по требованию пользователя. Хранилищем данных может быть любой ресурс: база данных, память, файлы дискового пространства, файлы удаленной машины и т.п.

Желательно максимально снизить нагрузку на обмен данными с хранилищем. Во-первых, сохранять необходимо только измененные данные. Во-вторых, в приложениях, работающих с хранилищами, в которых данные изменяются только с помощью средств приложения и в которых доступ к хранилищу в каждый момент времени получает только один экземпляр приложения – один пользователь, можно оптимизировать и загрузку данных – повторная загрузка по запросу пользователя не должна загружать данные, которые были загружены ранее и не изменялись за все время исполнения программы. Данный вариант работы приложений с хранилищем данных достаточно редкий и все же встречается. Если с хранилищем работают одновременно несколько пользователей, перезагрузку стоит проводить по всему необходимому для функционирования приложения набору данных.

Все это обуславливает необходимость наличия в модели дополнительной информации: модель должна знать, синхронизированы ли ее данные с данными хранилища. Подходящим решением является наделение модели свойствами автомата, то есть, прежде всего, наличием состояний.

Можно выделить следующие состояния модели: Null (пустая модель), Created (созданная модель), Exist (модель, данные которой синхронизированы с хранилищем), Updated (измененная модель), Deleted (удаленная модель). Действия, которые изменяют состояние модели, можно разбить на две категории: рассмотренные нами ранее действия по изменению данных модели (редактирование Edit, добавление Add, удаление Remove) и действия по синхронизации данных модели с хранилищем (загрузка Load, сохранение Save).


Рисунок 15. Граф состояний модели.

При создании экземпляра объекта модель переходит в свое начальное состояние Null. Модель может быть загружена данными из хранилища или добавлена в приложение после инициализации данными по умолчанию. В первом случае данные модели становятся синхронизированными с хранилищем – модель переходит в состояние Exist. Во втором случае модель переходит в состояние Created.

Состояние Created характеризует модель, данные которой рано или поздно должны быть добавлены в хранилище при сохранении. Модель представляет данные, которые необходимо добавить в хранилище. Разумеется, модель уже не может быть загружена данными хранилища. Обратите внимание, что редактирование модели не изменяет ее состояние, а удаление модели приводит к переводу ее в начальное состояние – пока данные модели не добавлены в хранилище, она существует только в рамках приложения. При сохранении модели происходит добавление ее данных в хранилище. Теперь данные модели синхронизированы с хранилищем – состояние Exist.

Состояние модели Exist является отправной точкой для работы с данными хранилища. Данные, которые представляет модель, соответствуют данным хранилища, поэтому сохранение не затрагивает модель в данном состоянии, а загрузка актуальна, как говорилось выше, только для многопользовательского режима работы с хранилищем. Редактирование данных модели переводит ее в состояние Updated, а удаление – в состояние Deleted.

Состояние Updated говорит о том, что данные модели, некогда синхронизированные с хранилищем, были изменены пользователем. Чтобы синхронизировать данные модели с хранилищем можно сохранить измененные данные в хранилище или загрузить модель данными из хранилища, по сути это отменяет изменения данных произведенные пользователем. Удаление измененной модели переводит ее в состояние Deleted.

В состоянии Deleted находится модель, данные которой пользователь хочет удалить из хранилища. При сохранении модели происходит удаление данных из хранилища, а сама модель становится неактуальной в рамках приложения и переходит в состояние Null. Удаление данных можно отменить, загрузив модель данными из хранилища – модель переходит в состояние Exist.

Изменением состояний модели в рамках MVC занимается контроллер. Кроме того, модель изменяет состояние при синхронизации данных с хранилищем (данные переходы отмечены красным цветом).

Кроме того, на основе состояний модели может быть оптимизирована работа представления списка. Модель списка уведомляет представление при изменении любой модели в списке, а также при изменении самого списка (добавление, удаление моделей из списка). При этом представление должно обновить текстовые и графические формы, которые отображают данные модели. Оптимизация заключается в том, что представление может не обновлять формы для моделей в состоянии Exist. Достаточно обновить формы моделей, которые могли изменить свои данные после последнего обновления экранных форм представления.

Представление может также определенным образом отображать модель в различных состояниях. К примеру, удаленные моделипредставление не отображает или же отображает отдельным списком. Созданные и измененные моделипредставление может определенным образом помечать, чтобы пользователь не забыл сохранить данные этих моделей.

6.4 Контроль корректности данных

Почти все данные предметной области, с которыми работает пользователь приложения, имеют ограничения. К примеру, название компании не может быть пустой строкой, а ее ИНН должен представлять десятизначное число. Проверку корректности данных зачастую проводят хранилища. Например, базы данных исключают возможность внесения некорректных данных с помощью ограничений целостности, ограничений на значения полей таблиц, а также с помощью триггеров, проверяющих добавляемые в таблицы данные.

Но зачем приложению терять время, подвергая лишний раз хранилище опасности внесения ошибки? Работу приложения можно оптимизировать, если поручить проверку корректности данных модели. Модель представляет бизнес-логику приложения, поэтому желательно, чтобы модель не только не допускала внесение в хранилище некорректных данных, но и на уровне приложения сигнализировала о некорректности пользователя.

Некорректность данных, кроме того, может быть допустимой и недопустимой. К примеру, не указанный пользователем ИНН компании (пустое значение) является допустимой некорректностью: пользователь просто не знает ИНН компании. С другой стороны, указание в качестве ИНН компании не десятизначного числа, а, к примеру, девятизначного числа недопустимо – это заведомо ошибочные данные. При допустимой некорректности данных необходимо оповестить пользователя, но позволить сохранить эти данные в хранилище. Позволять же сохранять в хранилище данные с недопустимой некорректностью нельзя.

ПРИМЕЧАНИЕ

Как видите, архитектура приложения, построенного на основе обобщенного MVC, не только не мешает расширению функциональности, но даже способствует этому, предоставляя готовые решения.

7 Источники

  1. Generics в Java 1.5, Kobylansky Stanislav, RSDN Magazine #1-2005.
  2. Нововведения в C# 2.0, Владислав Чистяков, RSDN Magazine #6-2003.
  3. MVC. Xerox PARC 1978-79 by Trygve Reenskaug.
  4. Application programming in Smalltalk-80TM: How to use Model-View-Controller (MVC) by Steve Burbeck | Программирование приложений в Smalltalk-80TM: Как использовать Model-View-Controller (MVC), Стив Бурбек, перевод В. А. Савельева.
  5. A Generic MVC Model in Java by Arjan Vermeij.
  6. Теория и практика Java: Будьте хорошим подписчиком (событий). Рекомендации по написанию и поддержке подписчиков событий, Брайан Гетц.
  7. Делегаты и события, Алексей Дубовцев, RSDN Magazine #4-2004.
  8. Приемы объектно-ориентированного проектирования. Паттерны проектирования, Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидес.
  9. Code Conventions for the Java Programming Language by Sun Microsystems, Inc.
  10. Теория и практика Java: Загадки родовых типов (generics). Определение и устранение некоторых пробелов в изучении использования родовых типов (generics), Брайан Гетц.
  11. Соглашения по оформлению кода команды RSDN, RSDN Team, RSDN Magazine #1-2004.
  12. Сравнение C# и Java, Википедия.
  13. Обзор паттернов проектирования, Ольга Дубина.
  14. Паттерны проектирования, RSDN.ru.

Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 32    Оценка 231 [+1/-2]         Оценить