Pull to refresh

Охота на мифический MVC. Построение пользовательского интерфейса

Reading time27 min
Views40K

Детектив по материалам IT. Часть вторая


В этой части я покажу как изначально выглядело деление пользовательского интерфейса и что из себя представляли Вид и Контроллер. Попробую рассказать почему в современных GUI библиотеках используется их объединение и какие вообще интересные решения можно найти в этой области на сегодняшний день. Ссылки на первоисточники приведены в начале первой части.


Начну с Вида. Не смотря на то, что Вид определяется как модуль, отображающий Модель – "а view is a (visual) representation of its model", на практике к Виду, как правило, просто относят все графические элементы GUI, то есть Видом считается все то, что мы видим на экране ЭВМ.


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


View or Controller


Кроме того, поскольку разумно считается что в графических элементах не хорошо «прописывать логику», то отсюда часто делается вывод, что Вид должен быть тонким и тупым (dumb View) и соответственно логику работы интерфейса можно обнаружить в самых неожиданных местах, вплоть до доменной модели (даже Фаулер пишет о «загрязнении» Модели настройками интерфейса GUI Architectures).


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


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


  1. выводить и удобно отображать пользователю информацию о системе
  2. вводить данные и команды пользователя в систему (передавать их системе)

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


View Controller Separation


Как видно пользовательский интерфейс довольно естественно и логично делится на два относительно независимых функциональных модуля. Причем такое функциональное деление, основанное на решаемых задачах, универсально. Не важно звуковой это интерфейс, графический или сенсорный в нем должен быть модуль, отвечающий за Ввод управляющих команд и данных (отсюда и название Controller — управление), и модуль отвечающий Вывод и представление информации о системе и о том что в ней происходит (View — Вид, представление).


А теперь давайте посмотрим, как же деление Вид/Контроллер выглядело на практике. И для того чтобы это сделать нам потребуется небольшой экскурс в историю. Дело в том, что вначале термина CONTROLLER вообще не существовало. Вместо него Реенскауг использовал термин EDITOR (MODEL-VIEW-EDITOR) и писал о разделении пользовательского интерфейса на View и Editor. "The hardest part was to hit upon good names for the different architectural components. Model-View-Editor was the first set."(Trygve Reenskaug). Попробую объяснить почему.


Не смотря на наличие технических возможностей (растровые экраны) во времена создания MVC и SmallTalk пользовательские интерфейсы преимущественно все еще оставались командными (Command-driven Interface) и представляли собой по сути обычные текстовые редакторы. В SmallTalk интерфейс так и назывался – Editor. И вот как он выглядел:


SmallTalk Editor


Можно сказать что Editor объединял в себе функции Контроллера и Вида. Он давал возможность относительно удобно вводить команды и данные (отображая нажатые клавиши и обрабатывая ввод с клавиатуры), и одновременно выводил информацию о выполнении команд и вообще о том что происходит в системе.


Лишь постепенно этот Editor преобразовывался в то, что мы сейчас привыкли понимать под GUI.


Вначале возникла простая но весьма плодотворная идея – разделить единое окно на множество панелей. Парадигма многопанельности появились существенно раньше MVC (многопанельные браузеры точно присутствовали уже в Smalltalk-76) и была значительным продвижением сама по себе. Она до сих пор активно используется практически во всех текстовых интерфейсах.


Реенскауг ее, естественно, тоже использовал. Тут нужно иметь в виду, что Dynabook, вокруг которого в Xerox Parc создавались и smallTalk и графические интерфейсы и, в частности, MVC, задумывался как «детский компьютер». Поэтому стояла задача сделать работу с компьютерными программами доступной любому неподготовленному пользователю, в частности ребенку. Реенскауг исходил из того, что пользователь может вообще ничего не знать о программе и о том как она устроена. И для того чтобы он мог с программой взаимодействовать нужно каким-то образом отобразить ему основную информацию о системе и доменной модели, лежащей в ее основе, чтобы пользователь понимал с чем имеет дело.


Выводилась такая информация на отдельных панелях, которые собственно и стали называться View. Как правило доменная модель отображалась при помощи нескольких различных Видов: "MVC задумывался как общее решение, дающее возможность пользователям контролировать большие и сложные наборы данных… Он особенно полезен тогда, когда пользователю нужно видеть Модель одновременно в разных контекстах и/или с разных точек зрения." И поскольку Реенскауг считал, что графическое представление информации нагляднее чем текстовое, то в основном виды у него представляли собой разного рода графики и диаграммы.


Далее. Так как пользователь ничего (или почти ничего) не знает о системе, а видит лишь всевозможные View, удобно и наглядно отображающие нужную ему информацию, то соответственно и управление системой должно было выглядеть так, как если бы пользователь управлял непосредственно самими View и тем что на них отображено. Поэтому для ввода команд стал использоваться не один «general Editor», а целое множество специализированных редакторов, каждый из которых был связан со своим Видом и был заточен на ввод команд (взаимодействие с доменной моделью) лишь в контексте этого Вида – permits the user to modify the information that is presented by the view.


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


Первый доклад Реенскауга так и назывался: "THING-MODEL-VIEW-EDITOR. An Example from a planning system". В нем была представлена первая реализация MVC на примере системы планирования и управления неким большим проектом (своего рода task-manager). Доменной моделью являлась сеть (network) «активностей», которая описывала что должно быть сделано (активность), в каком порядке, за какое время, кто в каких активностях участвует и какие ресурсы для каждой активности требуются. Пример призван был показать что “одна Модель может быть отображена с помощью многих различных Видов” и вот как там выглядел пользовательский интерфейс:


Reenskaug MVC


Доменная модель отображалась с помощью трех различных диаграмм (Видов). Нужно отметить, что виды и у Реенскауга и в SmallTalk вовсе не были "пассивными", они самостоятельно обрабатывали относящиеся к ним действия пользователя, в частности позволяли делать скроллинг и выделение элементов: “A ListView has fields where it remembers its frame, a list of textual items and a possible selection within the list. It is able to display its list on the screen within its frame, and reacts to messages asking it to scroll itself.” Это важный момент и я еще к нему вернусь.


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


Вторая диаграмма – GanttView (календарный или временной график Гантта) показывает расположение проекта и его активностей во времени. Эта диаграмма также позволяет выбрать некую активность. Редактор, связанный с GanttView дает возможность "to pass on operations on the network and its activities that are related to this particular View". В частности он позволяет изменить запланированную дату начала и окончания выбранной активности, а также осуществлять планирование и управление проектом/сетью как целым.


Третья диаграмма — это диаграмма ресурсов, требуемых для осуществления активностей, в зависимости от времени. Любопытно то, что с этой диаграммой связан не редактор, а список и в зависимости от того какая активность в этом списке выбрана, диаграмма ресурсов отображает только те ресурсы, которые относятся к выбранной активности. Такое сочетание двух видов, один из которых "управляет" отображаемой информацией другого характерно для многих интерфейсов. А идея использовать для «управления» не текстовый редактор а список ляжет в основу большинства контроллеров в SmallTalk-80.


Термин Controller возник практически перед самым уходом Реенскауга из Xerox PARC "After long discussions, particularly with Adele Goldberg". И из-за этого оригинальные работы Реенскауга бывает сложно читать, поскольку одним и тем же термином«Editor» он называет и весь интерфейс целиком (и в этом случае пишет о первичном разделении приложения на Model и Editor) и панели-редакторы которые связывались с соответствующими View для ввода команд и «управления View» и которые впоследствии собственно и стали Контроллерами – “Контроллер из Smalltalk-80 у меня назывался Editor” (Реенскауг)


Также к обязанностям Контроллера Реенскауг относил управление самим интерфейсом и в частности множеством входящих в него Видов: "Controller was responsible for creating and coordinating its subordinate views". Соответственно иногда он пишет, что Контроллер это связь между пользователем и системой, а иногда что это связь между пользователем и Видами и что Контроллер передает команды Видам.


Тем не менее, как видно из примера, Реенскауговский «Editor-Controller» был вполне себе видим и имел графическое представление.


Посмотрим как дело обстояло в SmallTalk-80.


Первое: в SmallTalk-80 текстовые редакторы, которые Реенскауг использовал для ввода команд, «официально» стали Контроллерами.


«ParagraphEditor» и «TextEditor», обладающие стандартными функциями ввода и редактирования текста, в SmallTalk-80 являлись потомками класса «Controller»:


Поскольку, как уже упоминалось, редакторы объединяли в себе функции Ввода и Вывода, то они также использовались для отображения текстовой информации во многих Видах – TextView, TextEditorView.


Стив Барбек дает по этому поводу довольно подробное разьяснение: "Все контроллеры, которые принимают ввод с клавиатуры, являются наследниками «ParagraphEditor» в иерархии Контроллеров. «ParagraphEditor» предшествовал созданию парадигмы MVC. Он одновременно выполняет две функции – обрабатывает ввод текста с клавиатуры и отображает его на экране. Поэтому в некотором смысле он представляет собой нечто среднее между Видом и Контроллером. Виды, которые используют подклассы «ParagraphEditor» в качестве контроллера, полностью переворачивают стандартные роли – для того чтобы отобразить текст они посылают его своему контроллеру" [All controllers that accept keyboard text input are under the ParagraphEditor in the Controller hierarchy. ParagraphEditor predates the full development of the MVC paradigm. It handles both text input and text display functions, hence it is in some ways a cross between a view and a controller. The views which use it or its subcasses for a controller reverse the usual roles for display; they implement the display of text by sending controller display].


О причине подобных «парадоксов» я постараюсь написать дальше, пока же просто обратите на это внимание


Второе: в SmallTalk-80 с каждым Видом (подвидом) ОБЯЗАТЕЛЬНО был связан свой Контроллер, который давал возможность производить некие операции с той информацией, которую Вид отображает


Соответственно любое множество Видов (подвидов) входящих в состав пользовательского интерфейса на самом деле всегда сопровождалось точно таким же множеством связанных с ним Контроллеров. Опять таки у Стива Барбека этой теме посвящен целый раздел который так и называется — "Communication Between Controllers".


Третье: главное усовершенствование состояло в том, что для ввода команд преимущественно стала использоваться мышь, а не клавиатура.


В SmallTalk-80 помимо контроллеров-редакторов (ParagraphEditor и TextEditor) появляется MouseMenuController, который становится основным средством ввода команд. Пользовательские интерфейсы из Command-driven становятся Menu-driven (User Interfaces)


Доступные команды для каждого Вида формировались явно в виде списка. А связанный с Видом MouseMenuController предоставлял для их отображения и удобного ввода специальное графическое средство – всплывающие pop-up menu, которые появлялись при нажатии соответствующей кнопки мыши. Вот так они выглядели:


SmallTalk menu


Повторю: pop-up menu относились к MouseMenuController. Меню являлись специальным графическим инструментом/средством для удобного отображения и ввода команд (определенных в контексте некоторого вида), который Контроллер, связанный с этим Видом, предоставлял пользователю. Вот что пишет Краснер: "Хотя меню можно рассматривать как пару вид-контроллер, но чаще всего они считаются входными устройствами и следовательно относятся к сфере контроллера… За создание всплывающих меню при нажатии какой-нибудь кнопки мыши отвечает класс MouseMenuController… По умолчанию PopUpMenus возвращают числовое значение которое вызвавший их контроллер использует для того чтобы определить какое действие ему нужно совершить… Из-за широкого использования всплывающих меню большинство контроллеров пользовательского интерфейса являются подклассами MouseMenuController".


Так что основной контроллер SmallTalk-80 (MouseMenuController) тоже имел свою графическую часть – pop-up menu, и разделение пользовательского интерфейса на Виды и Контроллеры стало выглядеть следующим образом (иллюстрация взята из статьи Гленна Краснера):


SmallTalk View Controller Separation


Меню относящиеся к MouseMenuController-ам можно видеть абсолютно во всех SmallTalk приложениях и примерах. Вот так с развернутыми меню выглядят уже упоминавшиеся Workspace и Inspector:


SmallTalk MVC


А вот так выглядит Browser, включающий в себя 5 Видов (подВидов) и соответствующих им Контроллеров


SmallTalk MVC


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


Так что, можем развеять очередной миф:


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

На самом деле и в реализации Реенскауга и затем в Smalltalk-80 большинство Контроллеров имели «графическую составляющую, помогающую пользователю вводить команды и данные». И именно такие Контроллеры в основном использовались в пользовательских приложениях. Хотя, конечно же, были Контроллеры и без графической составляющей, но они в основном применялись для более низкоуровневых системных задач (об этом чуть позже).


Если просуммировать, то получается что Контроллер это часть пользовательского интерфейса, которая отвечает за то чтобы 1) предоставить пользователю удобные средства для ввода команд и данных, а затем 2) действия пользователя перевести в вызовы соответствующих методов Модели и передать их ей.


Вот как определял Контроллер сам Реенскауг: "Контроллер это связь между пользователем и системой. Он предоставляет пользователю меню и другие средства для ввода команд и данных. Контроллер получает результат таких действий пользователя, транслирует их в соответствующие сообщения и передает эти сообщения" [ A controller is the link between a user and the system. It provides means for user output by presenting the user with menus or other means of giving commands and data. The controller receives such user output, translates it into the appropriate messages and pass these messages on].


Современные графические интерфейсы (GUI) для ввода команд используют весь спектр доступных средств: текстовые и графические меню, кнопки, всплывающие pop-up меню, всевозможные переключатели (как в реальных приборах), текстовые поля для ввода данных (TextEditor свелся к TextField). Все эти элементы служат в основном для управления, а не для отображения информации. По английски они так и называются — controls (элементы управления).


И если продолжить аналогию, то разделение Вид/Контроллер в современных системах выглядело бы примерно следующим образом:


View Controller


Можно сказать, что Контроллер это «панель управления» (Control panel). А Вид это «обзорная панель» или «панель наблюдения за системой» включающая в себя текстовые описания, списки, таблицы, графики, шкалы, световые табло, и всевозможные индикаторы состояния.


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


Реализация Видов и Контроллеров


Что нам это дает? Ну во первых, становится понятно что логика работы Вида никак не может быть помещена в Контроллер:


View Controller


Если всю логику работы GUI вынести в Контроллер, то это нарушало бы сразу несколько принципов:


  • главный принцип определяющий качество декомпозиции – High Cohesion + Low Coupling, который говорит что «резать» на модули нужно так, чтобы связи, особенно сильные, оставались преимущественно внутри модулей, а не между ними
  • Принцип единственной ответственности (Single responsibility principle).

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


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


Последнее время активно разрабатывается и используется концепция Dashboard-ов (информационных панелей), для которых создаются наборы универсальных виджетов, позволяющих наглядно и удобно визуализировать «все что угодно». В отличие от «тупого вида» такие «полноценные блоки визуализации» (инкапсулирующие свою логику, настройки и способные работать самостоятельно) очень востребованы и ценны сами по себе.


Dashboard


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


Как мы выяснили основной Контроллер SmallTalk-80 — MouseMenuController вовсе не являлся «всего лишь обработчиком действий пользователя с мышью и клавиатурой», на самом деле он делал довольно много вещей:


  1. Задавал набор и названия команд, доступных пользователю в контексте некоего Вида,
  2. Определял логику того, как эти команды транслировать в вызовы соответствующих методов Модели
  3. Отображал доступные команды
  4. Обрабатывал низкоуровневые движения мыши и создавал высокоуровневые события, к которым удобно привязывать выполнение команд (Event Driven подход).

SamllTalk Controller


Для таких Контроллеров просто «просилась» MVC архитектура. И в SmallTalk-80 она была использована: "pop-up menu are implemented as a special kind of MVC class" (Краснер).


Тут важно понимать, что Модель в этом «внутреннем» MVC не имеет никакого отношения к доменной модели, это именно внутренняя вспомогательная модель, описывающая «состояние» самого контроллера (в частности то, какая команда выбрана) и логику изменения этого состояния.


Аналогично дело обстоит и с Видом.


Мифы: То, что Вид это всего лишь «графика», является такой же идеализацией как и то, что Контроллер это «исключительно логика».

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


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


Где хранились такого рода дополнительные данные и логика их изменения? В принципе ответ очевиден и, если вы помните, Реенскауг ответил на этот вопрос – конечно же в самом Виде. Но! Не в перемешку с графикой, а в отдельном под-модуле/классе/скрипте, то есть в некоторой внутренней модели.


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


MVC View


Фаулер такие внутренние модели, являющиеся частью представления называет — Presentation Model: Presentation Model pulls the state and behavior of the view out into a model class that is part of the presentation.


И вот тут внимание! В первой части статьи подробно рассказывается о том, как в MVC был «потерян» Фасад и что из-за этого его роль на себе вынуждены брать другие компоненты. Так вот хотя Фаулер и пишет что "Presentation Model is not a GUI friendly facade to a specific domain object" на практике PresentationModel, ViewModel, ApplicationModel не только описывают состояние и поведение представления, но одновременно являются еще и Фасадами к доменной модели.


В примере, который Фаулер подробно разбирает, хорошо видно, что его PresentationModel является именно смесью Фасада и модели представления. Ну а Microsoft прямо пишет: "Presentation model class acts as a façade on the model with UI-specific state and behavior, by encapsulating the access to the model and providing a public interface that is easy to consume from the view" (MSDN: Presentation Model)


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


В отличие от него в Java подобного смешения стараются не допускать. В Java Swing «application-data models» и «GUI-state models» разделены гораздо более четко. Все, наверное, читали или слышали что Java Swing компоненты реализованы в виде MVC. И большинство наверняка уверены в том, что M в этой триаде это тот самый интерфейс к доменным данным. И по сути это правильно, почти…


Давайте внимательно посмотрим на список JList. У него действительно есть модель, обеспечивающая доступ к доменным данным – ListModel. Но кроме нее у списка имеется еще одна модель — ListSelectionModel, которая отвечает исключительно за внутреннюю логику выделения элементов списка (item selection). И вот эта модель как раз и является в чистом виде внутренней моделью представления – «GUI-state model»:


"The models provided by Swing fall into two general categories: GUI-state models and application-data models.


GUI state models are interfaces that define the visual status of a GUI control, such as whether a button is pressed or armed, or which items are selected in a list. GUI-state models typically are relevant only in the context of a graphical user interface (GUI).


An application-data model is an interface that represents some quantifiable data that has meaning primarily in the context of the application, such as the value of a cell in a table or the items displayed in a list. These data models provide a very powerful programming paradigm for Swing programs that need a clean separation between their application data/logic and their GUI" (см статью от создателей A Swing Architecture Overview).


У таблицы JTable помимо application-data модели, обеспечивающей доступ к доменным данным – TableModel, имеются целых две внутренних GUI-stateмодели — ListSelectionModel и TableColumnModel.


А вот у кнопки JButton имеется лишь GUI-state модель – ButtonModel. Что в принципе и логично. Кнопка не отображает доменных данных, это в чистом виде Контроллер с внутренней моделью, которая определяет состояние кнопки (нажата/не нажата).


При использовании графических компонент нам, в основном, приходится иметь дело с application-data моделями, через которые собственно и осуществляется взаимодействие домена и интерфейса. С GUI-state моделями мы сталкиваемся лишь тогда, когда возникает необходимость изменить дефолтное поведение компонента. Поэтому это нормально и правильно что многие даже не знают о наличие GUI-state моделей.


MVC JavaSwing GUI-state models


Видно, что в таблице некоторые модели отмечены одновременно и как GUI и как data. Особых пояснений не дается, написано что это зависит от контекста использования модели. Я могу высказать по этому поводу лишь предположение.


Все такого рода промежуточные модели относятся к компонентам, которые являются либо разновидностью скроллбара либо разновидностью переключателя (кнопка с состоянием). Такие компоненты предназначены прежде всего для управления и по сути представляют собой контроллер (в SmallTalk скроллбар и был контроллером — ScrollController) но с некоторым внутренним состоянием. Соответственно у таких компонент имеется лишь GUI-state модель и в дефолтной реализации состояние этих компонент никак не связано с состоянием доменной модели, а зависит лишь от действий пользователя, то есть от того, в какое состояние пользователь этот контрол/кнопку перевел.


Но при этом выглядит состояние таких контроллеров так, как если бы оно было согласовано с состоянием доменной модели: мы перевели переключатель в состояние «On», в доменную модель передалась соответствующая команда, там что-то включилось… и доменная модель тоже перешла в некое состояние «On». Достигается такая псевдо-согласованность как правило "сама собой", автоматически.


Тем не менее возможны ситуации когда может понадобиться настоящее реальное согласование доменной модели и состояния переключателя. В этом случае будет написана реализация ButtonModel, в которой метод isSelected() перестанет зависеть от действий пользователя с кнопкой, а вместо этого будет напрямую отображать состояние домена (некий его флаг). Кнопка при этом становится отчасти видом, а ButtonModel перестает быть внутренней GUI-state и становится application-data


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


MVC PresentationModel


Я стараюсь первого варианта избегать. Мое ИМХО что подобное смешение хоть и соблазняет своей кажущейся простотой но на практике приводит к тому что PresentationModel превращается в большую "свалку". Представьте что есть реальный сложный интерфейс. Если использовать подход (2), то мы этот интерфейс разобьем на ui-модули, и каждый модуль будет инкапсулировать свою логику в виде небольшой внутренней ui-state модели. А вот если использовать подход (1), то логика работы всего этого большого интерфейса будет свалена в кучу вперемешку с логикой фасада в PresentationModel. Пока такая PresentationModel или ApplicationModel создается и управляется автоматически неким фреймворком все хорошо. Но если подобное писать ручками… то мозг начинает ломаться и часто это приводит к тому, что логика представления рано или поздно просачивается через фасад в доменную модель. Даже у такого гуру как Фаулер, в его детском примере это таки произошло (интересно, кто-нибудь еще заметил это место?). В архитектуре, где фасад и GUI-state модели разделены, вероятность такого рода ошибок значительно ниже.


Объединенный ВидКонтроллер. «Упрощенный MVC»


Раз уж мы коснулись Java Swing, то нужно сказать еще об одной его важной особенности – в отличие от SmallTalk-80 где и скроллбар и pop-up меню были реализованы в виде полноценного MVC (с внутренней моделью, внутренним низкоуровневым контроллером и видом) Swing в реализации базовых gui компонент использует «упрощенный MVC», в котором Вид и Контроллер объединены в единый компонент, который одновременно отображает данные и обрабатывает действия пользователя. Обычно он так и называется: объединенный ViewController или UI-object, IU-delegate. Вот еще одна статья, где это подробно описывается: MVC meets Swing.


MVC


Самое интересное и неожиданное заключается в том, что такой «упрощенный MVC» де-факто используется в большинстве GUI библиотек и фреймворках пришедших на смену SmallTalk-80: VisualAge Smalltalk от IBM, Visual SmallTalk, VisualWorks SmallTalk, MacApp… (Smalltalk, Objects, and Design стр 124-125).


Фактически классический вариант MVC, с обязательным разделением Видов и Контроллеров, особенно на низком уровне, только в SmallTalk-80 и был реализован. Почему? Опять таки я могу высказать лишь предположение.


Основное отличие SmallTalk-80 от всех последующих систем заключалось в том что он работал без операционной системы. Поэтому весь объем низкоуровневой работы по отслеживанию движений мыши а также нажатий клавиш на клавиатуре приходилось выполнять Контроллеру. Контроллер в тех условиях был необходим, фактически он выполнял роль драйвера входящих устройств и кроме ссылок на Модель и Вид обязательно содержал ссылку на «сенсор». Соответсвенно именно эта сторона его деятельности акцентировалась и выходила на первый план.


После того, как эту работу взяли на себя операционные системы, низкоуровневая «обработка действий пользователя» в большинстве случаев становится достаточно простой для того чтобы с ней мог справится сам Вид.


И для простых компонент это оказывается плюсом, потому как разделение функций ввода и вывода хорошо работает для интерфейса в целом или для сложных компонент. А в случае создания базовых ui-компонент, как пишут создатели Swing: "this split didn't work well in practical terms because the view and controller parts of a component required a tight coupling (for example, it was very difficult to write a generic controller that didn't know specifics about the view)".


На самом деле и в SmallTalk-80 Виды и Контроллеры тоже были очень тесно связанными: Контроллер всегда содержал ссылку на Вид, а Вид на Контроллер. Кроме того, соответствующие классы Вида и Контроллера обычно еще и разрабатывались совместно: "Поскольку классы вида и контроллера часто разрабатывались совместно, для многих подклассов вида был определен класс контроллера, используемого по умолчанию, и метод для получения его экземпляра – defaultControllerClass. И конкретный контроллер связанный с видом часто создавался автоматически просто как экземпляр такого класса" [Because view and controller classes are often designed in consort, a view's controller is often simply initialized to an instance of the corresponding controller class. To support this, the message defaultControllerClass, that returns the class of the appropriate controller, is defined in many of the subclasses of View – Glenn Krasner ].


Что еще делал Контроллер? Помимо обработки низкоуровневых действий пользователя и создания высокоуровневых событий, Контроллер также содержал "логику перевода этих высокоуровневых событий в соответствующие методы Модели". Но как мы выяснили в первой части статьи, Моделями в SmallTalk-80 являлись не сами доменные объекты а интерфейсы и фасады к ним, причем клиент ориентированные. Такие Модели-фасады изначально формировались (были заточена) под требования клиента, так что команды, которые Контроллер отображал в popup menu, практически всегда однозначно соответствовали методам Модели.


Вот что пишет Краснер: "В конце концов сообщения контроллера почти всегда напрямую передавались модели; это означает что в ответ на выбор пункта меню «aMessage» контроллеру посылалось сообщение aMessage и в результате почти всегда вызывался метод модели, который так и назывался «aMessage»." [Finally, the controller messages were almost always passed directly on to the model; that is, the method for message aMessage, which was sent to the controller when the menu item aMessage was selected, was almost always implemented as ↑model aMessage].


В конце своей статьи Краснер приводит несколько примеров из которых видно, что Контроллеры приложений были «пустыми» и не содержали никакой логики. Их создание сводилось к тому что нужно было указать названия команд, отображаемых в pop-up меню, а затем для каждого названия указать метод модели, который должен был вызываться.


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


Следующий важный шаг заключается в том, что в современных GUI библиотеках исчезла и существовавшая в smallTalk-80 классификация самих базовых ui-компонент по типу Вид или Контроллер. Как выяснилось далеко не все графические элементы были Видами: pop-up меню относились к Контроллеру, скроллбар и TextEdit просто были Контроллерами.


Сейчас такое разделение не делается и мы имеем «ui-компоненты» или «виджеты», которые изначально проектируются так, чтобы быть универсальными – они одновременно могут отображать информацию и создавать высокоуровневые события, к которым удобно привязывать выполнение команд. И таким образом могут играть роль Вида или Контроллера в зависимости от контекста.


Дело в том, что многие ui-компоненты оказались похожи на ParagraphEditor, о котором писал Стив Барбек – они одновременно могут отображать информацию и позволяют ее изменять, объединяя в себе функции Вида и Контроллера. Такими интерактивными элементами являются текстовые поля, формы, всевозможные переключатели (toggle button, radio button, check box) и аналоги скроллбара...


MVC


Для таких компонент грань Вид или Контроллер оказывается размытой. Один и тот же элемент (объект) может быть Видом или Контроллером в зависимости от контекста в котором он используется и от того какую функцию он в данный момент выполняет. Список, используемый для отображения и ввода команд, являлся частью Контроллера (popUpMenu), а тот же список используемый для отображения данных – частью Вида (ListView). Аналогично — текстовое поле.


Как напишет Реенскауг в своих более поздних работах: "Модель, Вид и Контроллер это на самом деле роли, которые могут исполняться объектами" (Model, View and Controller are actually roles that can be played by the objects… – The DCI Architecture: A New Vision of Object-Oriented Programming).


То, что в SmallTalk-80 сами объекты пытались поделить на Модели, Виды и Контроллеры, вызывало лишь ненужную путаницу, которая в частности проявлялось в том, что TextView для отображения посылал текст своему Контроллеру.


Так осталось ли что нибудь от Контроллера? На мой взгляд да. То, что команды в систему вводятся "с помощью мыши и клавиатуры" это ведь тоже своего рода миф. В действительности Контроллер всегда предоставлял пользователю некие средства (как правило графические) помогающие вводить команды. Сначала это были текстовые редакторы, отображающие нажатые пользователем клавиши, затем pop-up меню, отображающие список доступных команд и дающие возможность вводить их «в один клик». Современные gui-библиотеки предоставляют уже целый арсенал средств — кнопки, переключатели, текстовые и графические меню, слайдеры… И вот эта часть работы Контроллера – поиск все более наглядных, удобных и «интуитивно понятных» средств/форм для ввода команд и управления системами, продолжает быть актуальной. И каждый раз когда мы разрабатываем интерфейс или ui-компонент мы явно или неявно ее решаем.


Интерфейс как Composite


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


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


Для обозначения подобных сложно-составных Интерфейсов Реенскауг в своей второй более поздней работе использует специальный термин "Tool" (инструмент пользователя) и у него этой теме посвящен отдельный раздел, который так и называется “Tool as a Composite”.


Reenskaug MVC


На рисунке в качестве Tool приведен уже знакомый нам интерфейс из первого доклада Реенскауга состоящий из трех блоков (которые Реенскауг называет Editor). И вот что по этому поводу пишет Реенскауг:


  • The Model that is responsible for representing state, structure, and behavior of the user’s mental model.
  • One or more Editors that present relevant information in a suitable way and support the editing of this information when applicable.
  • A Tool that sets up the Editors and coordinates their operation. (E.g., the selection of a model object that is visible in several Editors).

Complex Editors may again be subdivided into a View and a Controller.This solution is a composite pattern.


Как видите, шаблон Composite у Реенскауга относится ко всему интерфейсу, а вовсе не к Виду. Откуда же взялась идея что Composite в MVC относится исключительно к Виду?


Все просто – поскольку в SmallTalk-80 с каждым Видом обязательно был связан свой Контроллер, то в явном виде композиция там действительно задавалась только для Видов, а соответствующая композиция (иерархия) Контроллеров просто «вычислялась»: "Since each view is associated with a unique controller, the view/subView tree induces a parallel controller tree within each topView". Это решение было не особо удачным и приводило к хрупкости системы – стоило какой-нибудь Вид оставить без Контроллера и вся система рушилась. Поэтому в SmallTalk-80 был придуман «костыль» – Контроллер, который назывался «NoController». Этот контроллер ничего не делал и по умолчанию связывался с Видами, которые “исключительно отображали информацию” и не нуждались в контроллере. Его единственное назначение состояло в том, чтобы цепочка контроллеров, соответствующих Видам, не прерывалась. (подробно можно почитать У Стива Барбека в разделе “Communication Between Controllers”).


Так что в действительности и в SmallTalk-80 и у Реенскауга пользовательский интерфейс всегда включал в себя не только композицию Видов, но и точно такую же композицию соответствующих им Контроллеров.


И если уж говорить о шаблоне Composite, то, конечно же, более корректно относить его не к Виду а ко всему пользовательскому интерфейсу, как это сделано у Реенскауга и как это делается в современных GUI библиотеках. Интерфейсы больших приложений делятся на «gui-компоненты» или виджеты, которые в свою очередь могут делиться на более простые компоненты и образовывать древовидную структуру.


Каждый такой gui-компонент является автономным модулем, который одновременно отображает некую информацию и обрабатывает относящиеся к нему действия пользователя, порождая высокоуровневые события к которым удобно привязывать выполнение команд (View+Controller). Также он инкапсулирует свою логику работы как правило в виде внутренней GUI-state модели. То есть по сути представляет собой MVC (или «упрощенный MVC»). И вот такие полноценные gui-компоненты уже действительно можно разрабатывать параллельно и независимо, а также переиспользовать.


Следствием шаблона Composite и того, что каждый gui-компонент может быть реализован в виде небольшого MVC, является некая иерархичность или рекурсивность MVC. Из-за того что об этом редко пишут, аналоги этой идеи тоже пере-открываются. Вот известная статья на эту тему – Hierarchical model–view–controller и интересная дискуссия – Recursive Model View Controller. А вот картинка из статьи:


Hierarchical model–view–controller


На практике построение интерфейсов из независимых и полноценных ui-компонент активно использует и развивает ebay. Подробно об этом можно почитать в их замечательной статье Don’t Build Pages, Build Modules: "Когда дело касается view люди все еще мыслят страницами вместо того чтобы строить UI модули. Мы обнаружили что с ростом сложности страниц их становится экспоненциально сложнее поддерживать. Что мы хотим это разделить страницу на маленькие управляемые части, каждую из которых можно разрабатывать независимо. Мы хотим уйти от идеи непосредственно строить страницы. Вместо этого мы разбиваем страницу на логические UI модули и делаем это рекурсивно до тех пор пока модуль не станет FIRST. Это означает что страница строится из высокоуровневых модулей, которые в свою очередь строятся из подмодулей".


MVC


Суммируя


Я вовсе не хочу сказать что «original MVC» единственно правильный. Моя цель состояла лишь в том, чтобы показать, что он был намного сложнее и богаче чем те упрощенные схемки, которые нам обычно преподносятся в качестве MVC. Создавать на их основе реальные приложения это все равно что строить самолет на основе схем из детского конструктора и удивляться что он не летает. С другой стороны, если рассматривать MVC не как схему, а прежде всего, как набор архитектурных идей, то он действительно становится прост, логичен и очень понятен, и буквально выводится из этих идей.


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


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


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


Термин Вид часто используют как синоним термина Пользовательский Интерфейс. Так тоже "можно".


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


Как писал Вирт: "Самой трудной проектной задачей является нахождение наиболее адекватной декомпозиции системы на иерархически выстроенные модули, с минимизацией функций и дублирования кода".


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


А Модель, Вид и Контроллер это всего лишь результат начальной декомпозиции, предложенной нам талантливыми людьми. Также как паттерны Фасад, Наблюдатель, Компоновщик — это просто инструменты, которые ими были использованы для ослабления связанности и уменьшения сложности.


Спасибо всем кто «дотянул» до конца. Будем очень признательны за обоснованную критику. Особенно интересно мнение людей близко знакомых со SmallTalk.

Tags:
Hubs:
+19
Comments31

Articles

Change theme settings