вторник, 8 февраля 2011 г.

Графический интерфейс, часть третья

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

Настала пора переходить к решениям.

Сразу скажу: полного и хорошего ответа я не знаю.

Простое решение номер один: Model-View-Presenter

Мартин Фаулер обратил внимание на проблемы парадигмы Model-View-Controller довольно давно.

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

За несколько лет предложенный Фаулером шаблон Model-View-Presenter усложнился и эволюционировал, далеко уйдя от первой (весьма наивной) реализации.

И всё же имеет смысл начинать именно с неё.

  1. Есть модель предметной области (Domain Model). В ней сконцентрированы бизнес-логика, взаимодействие с базами данных и прочие полезные вещи.

  2. Форма GUI или вид (View) отображает эту модель (или её часть) в виде своих виджетов.

  3. Представление (Presenter) осуществляет связь между моделью и видом, реагируя на события пользователя и обновляя вид в ответ на изменения модели.

Модель содержит все данные, необходимые для работы вида.

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

  • value_text, read-write

  • value_enabled, readonly

  • value_color, readonly

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

Затем представление обновляет вид, приводя поле ввода в соответствии с value_text, value_enabled и value_color.

Более сложный сценарий взаимодействия будет в том случае, если отображаемое значение модели может изменятся вне нашей формы (например, другой открытой формой приложения).

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

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

Комплексные модели

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

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

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

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

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

Тестирование

Разнесение вида и модели, проведение взаимодействия между ними только через представление очень помогает при создании автоматических тестов.

Во первых, модели могут и должны работать независимо от представлений (и, тем более, видов). Значит и тестировать их можно независимо.

По вторых, можно создавать тестовые виды, содержащие все необходимые для представления атрибуты - но не являющиеся виджетами. Т.е. тестирование представлений может быть проведено без создания форм GUI.

Недостатки

Куда ж без них?

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

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

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

Это тоже требует дополнительного кодирования.

Есть и более серьёзные проблемы.

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

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

Заключение

Предложенная схема организации модель-вид-представление обладает очевидными достоинствами.

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

При помощи рассмотренного подхода можно создавать интерфейсы любой степени сложности.

Значительно облегчается автоматическое тестирование.

Вместе с тем я перечислил и недостатки. Они не фатальны, но требуют внимательности и дисциплины при проектировании приложения.

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

Небольшое отступление.

Я очень люблю книгу Мартина Фаулера "Refactoring". Купите - не пожалеете.

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

Если вы чуете пятой точкой, что нужно сделать так а не иначе - откройте Refactoring и найдите нужное вам изменение. Покажите страницу начальнику или коллегам. Вероятно, авторитет Мартина позволит вам протолкнуть собственную точку зрения.

Соблюдайте осторожность! Если коллеги внимательно читали принесенную вами в качестве последнего довода книгу - они с легкостью найдут в ней противоположный рецепт.

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

6 комментариев:

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

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

    ОтветитьУдалить
  2. Быть может, использование внешнего DSL для генерации кода по описанию - хорошая идея.

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

    Мне так привычней.

    ОтветитьУдалить
  3. Столкнувшись с Qt\Wx\Tk прихожу в ужас. Может у "бородатых дядек" код вида
    " self.connect(self.slider, QtCore.SIGNAL("valueChanged(int)"),
    lcd, QtCore.SLOT("display(int)"))
    и т.д. и т.п. вызывает детский восторг, но у начинающего программиста это однозначно вызывает панику. В универе я был испорчен ) Delphi 7. Это именно то, что я хочу от IDE, позволяющей создавать GUI. (Ах если б для Python сделали бы нечто подобное)) ) Быстро, удобно, визуально, расширяемо - функционал и "красивости" разделены. Можно за 15 минут накидать рабочую программу, с необходимыми компонентами и обработчиками событий, а потом заниматься внешним видом. За новыми версиями не слежу, но к версии IDE 2002 года, разве что CSS функционал прукрутить бы - и все. Не могу понять , зачем столько ненужных телодвижений совершается в области GUI. Столько запутанных и глючных библиотек создается. И остается лишь неприятный осадок, что все это развивается в неэффективном направлении.

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

    ОтветитьУдалить
  5. А что включают в себя эти задачи, Андрей? К несчастью у меня нет опыта работы в крупном проекте и вот интересно , где проходит эта грань? Ведь кроме монстров вроде фотошопа и текстовых процессоров, существует и море всяких утилиток, кторые шикарно реализовывались визуально: от клиентов к БД и самопальных архиваторов до расчета методом монте-карло и визуального отображения прохождения пучка электронов. И эта ниша в Python - одна зияющая дыра. Пробывал и Boa Constructor, глючный и неразвивающийся, и ещё что-то... и, как ты говоришь - "Не люблю". ) Вся надежда на JetBrains с PyCharm ) - может прикрутят к нему со временем, что-то , напоминающее работу со Swing в NetBeans...

    ОтветитьУдалить
  6. Попробуйте PureMVC для питона есть модуль

    ОтветитьУдалить