27 февраля в 15:39

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

Детектив по материалам 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.

@cobiot
карма
79,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (31)

  • –1
    Немного оффтопа: пример с осциллографом улыбнул;)
    • 0
      Доводилось писать аналог. Пример почти «из жизни» :)
  • 0
    Спасибо.

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

    Страница та выпадала в гугле по какому-то жутко умному запросу про MVC и не могла быть найдена просто так — просто была мусором в сети с кучей комментариев в стиле «хватит агрить парень, мир какой он есть!»

    А сейчас вот, куча фамилий, сотни диаграм, на выходе вывод — ребята, вы неправильно понимаете MVC. Пора прекратить класть файлики в папки views, а нужно просто писать модули, которые имеют самостоятельный html/js, ну то есть вас пару лет за нос водили, теперь идем в другую сторону.

    Радость то! Пришло таки :)

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

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

    exports он разумеется есть, но чтобы добиться полной независимости, очень любопытно увидеть пример. на любом языке.
    • 0
      спасибо за «наводку» на пример «контроллер для «умного дома»»… Именна эта мысль меня и занимала последнее время — делать фасад как набор «команд». Интересно почитать как это реализовано у других. У меня тоже фасад довольно хорошо «разваливается» на независимые команды. Но чтобы понять почему у вас это не так… трех строчек что вы написали мало… нужно видеть весь ваш пример
    • 0
      Любопытно взглянуть на простейший пример нотации такого приложения, в котором логика контроллера внешнего модуля отвечающего за layout (имеющего вещи которые нужно сделать при инициализации) — не связана с модулем какой-нибудь драной кнопки или выпадающего меню, и как это сделать вне связей их друг с другом.

      Я не очень понял, что вы имеете ввиду. Мы разрабатываем так, что есть корневой модуль, он внутри себя содержит "layout" модуль (или сразу от него наследуется), в который помещает модули экранов, которые может провязать друг с другом, через реактивные свойства. Например:


      $my_app $mol_view
          sub /
              <= Layout $mol_book
                  pages /
                      <= Menu $my_task_menu
                          selected => task_selected
                      <= Details $my_task_details
                          task <= task_selected -

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

  • 0
    Ну меня больше интересует сайт (или как сейчас модно — приложение), которое доступно в URL в интернете.

    Если раскрывать вопрос по демонстрации: ну вот в качестве примера написать на этой позиции обычный такой landing-site с одной стороны на 2 страницы (чтобы была первая и вторая), с другой стороны с отправкой на email письма например, с третьей стороны с поддержкой «тем» и «языков»,
    Ну то есть в каждой из сфер по два примера, и самое обидное — связать их так, чтобы была минимальная связность.

    Обычно моя проблема в том, что модуль, который я внедряю в чей-то битрикс-говнокод так или иначе требует взаимодействия с самим битриксом. И начинаются связи и невозможность разделить его на маленькие модули — загоны в стиле — а где хранить ссылки на уже созданные объекты? если их хранить в каждом дочернем модуле, то это затраты памяти, делать их через статику — трудности в поддержке потом, когда видишь Class::method() и понимаешь что походу пора идти читать, что там в классе и когда писали.

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

    Где баланс? Показал бы только пример кода.
    Рассмотреть контроллер под конкретные случаи типа — контроллер шаблона. Это контроллер вообще? Нет наверное. С другой стороны ему нужно подготовить css/js под конкретный модуль, а родительскому — под себя и еще забрать у всех потомков и так рекурсией хрен знает сколько раз, чтобы была одна точка контроля на каждом уровне. Вот где самый расколбас.

    Написать api на контроллерах, который возвращает json и принимает get/post/head быстро и легко — и поддерживать потом тоже легко. А вот когда начинается игра в «продвижение сайта» — начинается веселуха. Вот у тебя скрипты в модуле лежат — ты их слил в один красиво так, родительский модуль подхватил… и тут понимаешь, что jquery был в одном модуле и в другом — оппа уже dependency. А если депы, то значит контроль версий. А если контроль версий — то нужна шина — глобальный модуль, отвечающий за другие модули.

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

    Опять же — примеры нужны, хоть какие-нибудь. Не картинки, а код — с определением в стиле «давайте представим что вот этот обьект — это команда».
  • 0
    Наверное самый разумный подход, это когда действительно есть обьект app хранящий экземпляры каждого модуля с возможностью доступа к ним в виде app.module.submodule.submodule.action();

    И он же следит за dependency, и он же сжимает код и в общем-то молодец даже
  • 0
    http://patternlab.io предлагают думать о GUI в терминах Атом -> Молекула -> Организм -> Шаблон -> Страница:

    • 0
      БЭМ от яндекса давно разделило на блок-элемент-модификатор, при условии что одна DOM-нода может олицетворять несколько того и другого.

      Это чертовски удобно, когда пишешь яваскрипт код под такую верстку, и в стилях вложенности по минимуму, и редактировать легко.

      Молекулы? Что?
      • 0

        БЭМ от Яндекса — это костыль, который пытается бороться с отсутствием вложенности и namespace'ов в СSS. Туда же все эти OOCSS и прочие извращения.


        Любить button button--state-danger или там .media__img--rev — это стокгольмский синдром.


        То, что предлагает http://patternlab.io ортогонально BEM'у. Это подход к проектированию интерфейсов:


        • выделяем из интерфейса самые мелкие самодостаточные переиспользуемые компоненты: кнопки, поля ввода и т.п. Это «атомы»
        • Эти мелкие компоненты объединяются в более крупные блоки, которые тоже можно произвольно использовать заново: «поле поиска с кнопкой поиска» (состоящее из «поле ввода», «кнопка»), «меню» (состоящее из «кнопка», «список»(который состоит из «элемент для показа текста»)). Это «молекулы»
        • и так далее: «молекулы» компонуются в более сложные структуры. Эти более сложные структуры в еще более сложные и т.п.

        Более подробно в Atomic Design: http://atomicdesign.bradfrost.com/table-of-contents/

        • 0

          То что предлагает http://patternlab.io не ортогонально а диаметрально BEM'у. Это правильный подход к проектированию интерфейсов. Я об этом все уши прожжужал. За это мне накидали минусов в карму, но мнение о БЭМ у меня от этого не поменялось. БЭМ — это шлак которому место на помойке.

          • 0

            Я так и не понял, почему вы противопоставляете "матрёшку" БЭМ-у. БЭМ — это один из способов реализовать ту самую "матрёшку", когда более крупные компоненты состоят из более мелких. И для этого вовсе не надо вводить странную номенклатуру "Атом -> Молекула -> Организм -> Шаблон -> Страница". Любой компонент ("блок" в терминологии БЭМ) может состоять из любых других компонент ("элемент" в терминологии БЭМ), которые также могут состоять из компонент и тд.


            Все ваши претензии упираются в правила именования, которые можно выбирать любые. Мы, например, автоматически генерируем такие бэм-аттрибуты: my_sign_in_submit mol_view mol_button mol_button_type="major" — они позволяют точно понять где и зачем объявлен тот или иной стиль.


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


            [mol_view] {
                transition: all ease .1s;
            }

            А следующий задаёт особый размер нашей кнопке сабмита на форме входа:


            [my_sign_in_submit] {
                font-size: 2rem;
            }

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

          • 0
            И чего же сразу на помойке.
            Если провести аналогию на жизнь — то каждый человек называет вещи своими именами отличными от других человеков (блоков в нашем случае).

            Таким нехитрым образом блоков могут быть тысячи (человеки) — элемент (дом-нода) для каждого из них свое имя имеет. А модификатор — это пометка, что элемент имеет вариации в пределах карты мира данного человека.

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

            block-block__element-element_modif, и все классы в таком виде.
            Вложенность только там, где есть _modif, т.к. в пределах вариации дочерние элементы могут отличаться внешне.
            Вроде все просто?

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

              @gzhegow усложенение структуры совсем не бесконечное и определяется исключительно пулом запросов на которые данная структура должна адекватно "отвечать". В случае с html + сss это множество страниц, которые данная структура позволяет отрендерить. Вы разбираете странички или запросы на компоненты и пересобираете их в соответствии со структурой. Именно результат парсинга запросов или страничек являются источником данных и ссылок для компонент… Скрипт вы не переписываете. Он универсален. Если такой подход есть или будет реализован в "текущем решении" я буду это приветствовать. Пока этого нет...

              • 0
                Спасибо, но мне не удалось понять вашего объяснения, потому что я не увидел ни одной аналогии и живого примера. К тому же я обратил внимание, что вы не поставили никакой оценки моему высказыванию и просто его проигнорировали (я не про лайки, я про оценку наличия в нем смысла)

                Я конечно ожидаю, что вы можете сейчас отреагировать не так как я ожидаю, но в этом случае сможем завершить дискуссию
                • 0

                  @gzhegow cмысл есть как в Ваших высказываниях, так и в реализации БЭМ. Кого то еще (вас и др. человеков) концепция БЭМ устраивает. Но я хочу увидеть другой подход.И всё. Относительно "живых примеров". Их нет. Я могу отослать вас к Model Entities в Drupal, но опять же связывание сущностей там делается ручками и только потом Вам система генерирует код, который Вы опять же ручками вставляете в темплейт. А хочется без ручек. Нет никакой дискуссии. Это как в сказке — собери то — не знаю что. Я считаю, что структура как объект должна имплементировать все возможные варианты интерфейса который вы хотите создать на её основе.

                  • 0

                    Вы говорите о композиции компонент управляемой данными. Реализуется это достаточно не сложно. Но при чём тут БЭМ и вообще CSS?

                    • 0
                      Вы говорите о композиции компонент управляемой данными.

                      Именно.


                      Реализуется это достаточно не сложно

                      В прямом смысле слова достаточно сложно. Даже без учёта ссылок на одинаковые компоненты. (Ссылки определяются при анализе структуры и используются при синтезе)


                      my_app $mol_view
                          sub /
                              <= Layout $mol_book
                                  pages /
                                      <= Menu $my_task_menu
                                          selected => task_selected
                                      <= Details $my_task_details
                                          task <= task_selected 
                      

                      Попробуйте кодом собрать структуру же из своих же компонент
                      или


                      Дано — "разобранные" компоненты


                      
                       "a":{"parent":"b"}
                       "с":{"parent":"b"}
                      
                       "d":{"from":"a"}
                       "d":{"from":"c"}
                      

                      Можно в нотации Tree


                      Должны получить:


                      "b":{"children":{"a":"d",
                                       "c":"d"}
                           }

                      Скрипт которым Вы композируете структуру должен оставаться одним и тем же.


                      Но при чём тут БЭМ и вообще CSS?

                      Контент нод в структуре может быть любым. В реале это byteArray.


                      @gzhegow я не выкладываю код, чтобы избежать ненужной критики. Вполне достаточно Ваших ценных замечаний. Относительно призыва работать. Это Вы своего президента процитировали или меня нанимают на работу?

                      • 0

                        Так работает, например, визуализатор маркдауна. В результате парсинга получается AST, по которому собирается дерево компонент.


                        Контент нод в структуре может быть любым.

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

                      • 0
                        В прямом смысле слова достаточно сложно.
                        Дано — «разобранные» компоненты
                        Должны получить

                        Я может чего-то не понимаю, но вроде ничего сложного тут нет:
                        var data = [
                         {"a":{"parent":"b"}},
                         {"c":{"parent":"b"}},
                        
                         {"d":{"from":"a"}},
                         {"d":{"from":"c"}}
                        ];
                        
                        
                        var components = {};
                        var imports = {};
                        
                        // prepare
                        for (var i in data) {
                          var item = data[i];
                          var componentName = Object.keys(item)[0];
                          var itemData = item[componentName];
                        
                          if (itemData.parent != undefined) {
                            parentComponentName = itemData.parent;
                        
                            components[componentName] =
                              {parentName: parentComponentName, children: {}};
                        
                            components[parentComponentName] = {parentName: null, children: {}};
                          }
                        
                          if (itemData.from != undefined) {
                              var fromComponentName = itemData.from;
                              imports[fromComponentName] = componentName;
                          }
                        }
                        
                        // make
                        for (var i in data) {
                          var item = data[i];
                          var componentName = Object.keys(item)[0];
                          var itemData = item[componentName];
                        
                          if (itemData.parent != undefined) {
                              parentComponentName = itemData.parent;
                              components[parentComponentName].children[componentName] =
                                imports[componentName];
                          }
                        }
                        
                        // clean
                        for (var i in components) {
                            if (components[i].parentName != null) {
                              delete components[i];
                            } else {
                              delete components[i].parentName;
                            }
                        }
                        
                        console.log(JSON.stringify(components, null, 2));
                        
                        /*
                        {
                          "b": {
                            "children": {
                              "a": "d",
                              "c": "d"
                            }
                          }
                        }
                        */
                        
                        • 0

                          Спасибо! Отличный пример.
                          Я немного изменил


                          "data": {
                              "relationship": ["source", "key", "target"],
                              "apply": [
                                  [
                                      ["a", "c"], "parent", "b"
                                  ],
                                  [
                                      "d", "from", ["a", "c"]
                                  ]
                              ]
                          }

                          Хорошо бы модернизировать так, чтобы key принимал произвольное имя, а не только parent, children,to или from

                          • 0
                            Будет аналогично, только появятся внутренние циклы и доступ по индексам, а не по свойствам. Произвольное имя задать вряд ли получится, так как обработка определяется его семантикой. Можно попробовать сделать одноименные функции и вызывать по названию.
                            • 0

                              @michael_vostrikov я прикинул, что. для хранения любого графа или дерева в базе потребуется всего 5 таблиц в которых будут храниться


                              • свойства ноды
                              • названия ключей
                              • отношения между нодами
                              • адреса нод (use)
                              • контент нод

                              В случае если таблицы хранить в БД, то массив компонент можно поднять SQl запросом и после полученный результат обработать твоим скриптом.
                              Тарантул отдыхает :)))

                  • 0
                    Та я ж не доказываю, я пытаюсь вашу точку зрения понят, но я не вижу ничего кроме `Model Entities` и `Drupal`, которые тоже не вижу, потому что не знаю что это, и до ваших примеров, что это действительно решение многих проблем — даже знать не хочу что это.

                    Вы можете теперь сослаться на то, что я лентяй или как угодно поступить, но я правда ожидаю от вас примера из жизни как это работает, и почему это легче для каких задач. Пока вижу голые фразы, намеки на почитать — но зачем мне читать бескрайние берега? Разводил мало в жизни? Давайте уже работать, а не демонстрировать, если хотите программирование на новый уровень.
  • 0

    @vintage если вопрос был ко мне, то я ничего не имею против генерации кейспейса. Я "имею против" способа сборки компонент. Как они будут обрабатываться тем или иным браузерам совершенно вторично. Сборка должна идти снизу вверх. Представьте, что у вас компоненты разбросаны по разным дизайнерам. Они знают только имя родительского компонента. Требуется собрать всё компоненты по цепочкам. Собственно какой ключ будет сгенерирован в результате сборки не важно. Не имею ничего против бэм атрибутов. В конце концов каждый одноименный атрибут обрабатывается как последний в списке текущий ноды или сss .

    • 0

      Мы похоже на разных языкх разговаривем.


      Что такое "сборка компонент"? и зачем дизаинерам знать имя родительского? Что такое "цепочки"? что за "ключи" генерируются и зачем?

      • 0

        Дано:
        Множество нод
        ["a","b","c"]


        "a":{ключ: значение}
        "b":{ключ: значение}
        "c":{ключ: значение}


        Требуется:
        Собрать структуру по ключу.
        В частном случае. Если имя ключа может задано как "parent" или "super", то компонент будет собираться по родительской цепочке и т.д.

        • 0

          Понятней не стало :-) Что значит "собрать структуру по ключу", зачем её собирать и при чём тут компоненты?

  • 0

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


    "b":{"children":["a"]}
     "a":{"parent":"b"}
    
     "a":{"to":"c"}
     "c":{"from":"a"} 
    • 0

      Это вы описываете какое-то своё решение не понятно какой задачи. Если нужно "соединять независимые компоненты друг с другом", то это легко делается через биндинги. Пример, я приводил выше.

  • 0

    Про биндинги сверху вниз я знаю. Относительно "непонятно какой задачи". Мы в топике про мифический MVC. Ну так вот. Кey-value атрибуты — это модель или данные. Универсальный скрипт выступает в роли контроллера. А в роли вида или представления выступает структура. Структура может динамически перестраиваться в зависимости от изменения состава компонент из которых она собирается.Начальная структура может быть пустым множеством {}

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.