Государственные машины и пользовательский интерфейс работают - есть примеры / опыт?

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

Итак, вопрос в том, думает ли кто-нибудь из вас, программистов пользовательского интерфейса, в своей работе терминами государственных машин? Если да, то как?

спасибо, -Morgan

Ответов (10)

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

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

Вместо того, чтобы мои приложения находились в различных состояниях, представленных флагами состояния и режимами, я использую продолжения, чтобы контролировать, что приложение будет делать дальше. Проще всего объяснить на примере. Допустим, вы хотите открыть диалоговое окно подтверждения перед отправкой электронного письма. Шаг 1 создает электронное письмо. Шаг 2 получает подтверждение. Шаг 3 отправляет электронное письмо. Большинство наборов инструментов пользовательского интерфейса требует, чтобы вы передавали управление обратно в цикл событий после каждого шага, что делает это действительно уродливым, если вы пытаетесь представить его с помощью конечного автомата. С продолжениями вы не думаете о шагах, которые навязывает вам инструментарий - это всего лишь один процесс создания и отправки электронного письма. Однако, когда процессу требуется подтверждение, вы фиксируете состояние своего приложения в продолжении и передаете это продолжение кнопке ОК в диалоговом окне подтверждения. Когда нажата ОК,

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

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

Обновление: вот полный пример Qt в Ruby. Интересные части находятся в ConfirmationButton и MailButton. Я не эксперт по Qt или Ruby, поэтому буду признателен за любые улучшения, которые вы все можете предложить.

require 'Qt4'

class ConfirmationWindow < Qt::Widget
  def initialize(question, to_do_next)
    super()

    label = Qt::Label.new(question)
    ok = ConfirmationButton.new("OK")
    ok.to_do_next = to_do_next
    cancel = Qt::PushButton.new("Cancel")

    Qt::Object::connect(ok, SIGNAL('clicked()'), ok, SLOT('confirmAction()'))
    Qt::Object::connect(ok, SIGNAL('clicked()'), self, SLOT('close()'))
    Qt::Object::connect(cancel, SIGNAL('clicked()'), self, SLOT('close()'))

    box = Qt::HBoxLayout.new()
    box.addWidget(label)
    box.addWidget(ok)
    box.addWidget(cancel)

    setLayout(box)
  end
end

class ConfirmationButton < Qt::PushButton
  slots 'confirmAction()'
  attr_accessor :to_do_next
  def confirmAction()
    @to_do_next.call()
  end
end

class MailButton < Qt::PushButton
  slots 'sendMail()'
  def sendMail()
    lucky = rand().to_s()
    message = "hello world. here's your lucky number: " + lucky
    do_next = lambda {
      # Everything in this block will be delayed until the
      # the confirmation button is clicked. All the local
      # variables calculated earlier in this method will retain
      # their values.
      print "sending mail: " + message + "\n"
    }
    popup = ConfirmationWindow.new("Really send " + lucky + "?", do_next)
    popup.show()
  end
end

app = Qt::Application.new(ARGV)

window = Qt::Widget.new()
send_mail = MailButton.new("Send Mail")
quit = Qt::PushButton.new("Quit")

Qt::Object::connect(send_mail, SIGNAL('clicked()'), send_mail, SLOT('sendMail()'))
Qt::Object::connect(quit, SIGNAL('clicked()'), app, SLOT('quit()'))

box = Qt::VBoxLayout.new(window)
box.addWidget(send_mail)
box.addWidget(quit)

window.setLayout(box)
window.show()
app.exec()

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

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

Честно говоря, это не проблема пользовательского интерфейса.

Я бы сделал следующее:

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

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

Основное преимущество заключается в том, что он позволяет мыслить на более высоком уровне абстракции с более высокой степенью детализации. Вместо того чтобы думать: «Если кнопка A нажата, поле со списком B заблокировано, текстовое поле C очищено, а кнопка D разблокирована», вы думаете: «Нажатие кнопки A переводит приложение в состояние CHECKED» - и вход в это состояние означает, что определенные вещи случаться.

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

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

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

AS3, будучи языком, управляемым событиями, делает этот вариант очень привлекательным.

При обнаружении определенных событий состояния кнопок / экранных объектов автоматически меняются.

Обобщенный набор состояний определенно может помочь избавиться от спагеттификации вашего кода!

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

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

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

Таким образом, конечный автомат во многом применим к пользовательским интерфейсам.

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

-Адам

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

Это изменение от:

DoSomethingToTheFooObject();  
UpdateDisplay1();  // which is the main display for the Foo object  
UpdateDisplay2();  // which has a label showing the Foo's width,
                   // which may have changed  
...  

к:

Foo.DoSomething();  

void OnFooWidthChanged() { UpdateDisplay2(); }  
void OnFooPaletteChanged() { UpdateDisplay1(); }  

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

If you find that, of the 100 UI thingies that may need to be repainted when Foo's state changes, all of them have to be redrawn when the palette changes, but only 10 when the width changes, it might suggest something about what events/state changes Foo should be signaling. If you find that you have an large event handler OnFooStateChanged() that checks through a number of Foo's properties to see what has changed, in an attempt to minimize UI updates, it suggests something about the granularity of Foo's event model. If you find you want to write a little standalone UI widget you can use in multiple places in your UI, but that it needs to know when Foo changes and you don't want to include all the code that Foo's implementation brings with it, it suggests something about the organization of you data relative to your UI, where you are using classes vs interfaces, etc.... Fundamentally, it makes you think more seriously about what is your presentation layer, more seriously than "all the code in my form classes".

-PC

На эту тему есть книга. К сожалению, он распродан, а доступные редкие б / у очень дороги.

Constructing the User Interface with Statecharts
by Ian Horrocks, Addison-Wesley, 1998

Я получил прези-презентацию паттерна, который я назвал «Государство прежде всего».

Это комбинация MPV / IoC / FSM, и я успешно использовал ее в .Net / WinForms, .Net / Silverlight и Flex (на данный момент).

Вы начинаете с написания кода вашего FSM:

class FSM
    IViewFactory ViewFactory;
    IModelFactory ModelFactory;
    Container Container; // e.g. a StackPanel in SL
    ctor((viewFactory,modelFactory,container) {
        ...assignments...
        start();
    }

    start() {
        var view = ViewFactory.Start();
        var model = ModelFactory.Start();
        view.Context = model;
        view.Login += (s,e) => {
            var loginResult = model.TryLogin(); // vm contains username/password now
            if(loginResult.Error) {
                // show error?
            } else {
                loggedIn(loginResult.UserModel); // jump to loggedIn-state
            }
        };
        show(view);
    }


    loggedIn(UserModel model) {
        var view = ViewFactory.LoggedIn();
        view.Context = model;
        view.Logout += (s,e) => {
            start(); // jump to start
        };
        show(view);
    }

Затем вы создаете свои IViewFactory и IModelFactory (ваш FSM позволяет легко увидеть, что вам нужно)

public interface IViewFactory {
    IStartView Start();
    ILoggedInView LoggedIn();
}

public interface IModelFactory {
    IStartModel Start();
}

Теперь все , что вам нужно сделать , это осуществить IViewFactory, IModelFactory, IStartView, ILoggedInView и модели. Преимущество здесь в том, что вы можете видеть все переходы в FSM, вы получаете сверхнизкую связь между представлениями / моделями, высокую тестируемость и (если ваш язык позволяет) большое количество безопасных типов.

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

Вы можете посмотреть презентацию на http://prezi.com/bqcr5nhcdhqu/, но на данный момент она не содержит примеров кода.

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

Книги Самека о диаграммах состояний во многом основаны на этой работе, хотя и в несколько иной области и, как сообщается, не столь ясны. « Практические диаграммы состояний UML в программировании на C/C++ для встраиваемых систем » также доступны в Safari .

Хоррокса цитируют довольно часто - на портале ACM двадцать статей, так что, если у вас есть доступ, вы можете найти что-нибудь полезное.

Есть книга и программное обеспечение FlashMX для интерактивного моделирования . У них есть примерная глава в формате PDF о диаграммах состояний.

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