我正在寻找解决我的前端小部件代码的方法.有人建议使用有限状态机来思考我正在做的事情.我知道一个状态机模式可以适用于几乎任何问题.我想知道是否有一些经验丰富的UI程序员实际上养成了这个习惯.
所以,问题是 - 你们中的任何一个UI程序员都会在你的工作中考虑状态机吗?如果是这样,怎么样?
谢谢,-Morgan
我目前正在使用(专有)框架,它非常适合UI作为状态机的范例,它肯定可以减少(但不能消除)UI元素之间复杂和无法预见的交互问题.
主要的好处是它允许您以更高的粒度在更高的抽象级别进行思考.而不是想"如果按下按钮A然后组合框B被锁定,文本字段C被清除并且按钮D被解锁",您认为"按下按钮A将应用程序置于CHECKED状态" - 并且进入该状态意味着某些事情发生.
但是,我不认为将整个UI建模为单个状态机是有用的(甚至是可能的).相反,通常有许多较小的状态机,每个状态机处理UI的一部分(由几个在概念上相互作用并在一起的控件组成),以及一个(可能不止一个)处理更多基本问题的"全局"状态机.
状态机通常太低级别,无法帮助您考虑用户界面.它们为UI工具包提供了一个很好的实现选择,但是在正常的应用程序中有太多的状态和转换需要您手动描述它们.
我喜欢考虑具有延续的UI.(谷歌它 - 这个术语足够具体,你可以得到很多高质量的点击.)
而不是我的应用程序处于由状态标志和模式表示的各种状态,我使用continuation来控制应用程序下一步做什么.用一个例子来解释是最容易的.假设您要在发送电子邮件之前弹出确认对话框.第1步构建一封电子邮件.第2步得到确认.第3步发送电子邮件.大多数UI工具包都要求您在每个步骤之后将控制权传递回事件循环,如果您尝试使用状态机来表示它,则会使其变得非常难看.通过延续,您不会考虑工具包强加给您的步骤 - 这是构建和发送电子邮件的所有过程.但是,当进程需要确认时,您将继续捕获应用程序的状态,并将该继续执行到确认对话框上的"确定"按钮.按下确定后,
在编程语言中,Continuations相对较少,但幸运的是,你可以使用闭包获得一个穷人的版本.回到电子邮件发送示例,在您需要获得确认时,您将剩余的进程写为闭包,然后将该闭包交给OK按钮.闭包类似于匿名嵌套子例程,它们在下次调用时记住所有局部变量的值.
希望这能给你一些新的思考方向.我将尝试稍后使用真实代码回来向您展示它是如何工作的.
更新:以下是Ruby中Qt的完整示例.有趣的部分在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()
不需要将UI建模为状态机; 它是显示的对象,它可以有助于建模为状态机.然后,您的UI变为(过度简化)一组事件处理程序,用于各种对象中的状态更改.
这是一个变化:
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(); }
考虑到您正在显示的数据中的哪些更改应该导致重新绘制可以澄清,无论是从客户端UI端还是服务器Foo端.
如果你发现,当Foo的状态发生变化时,可能需要重新绘制100个UI内容,当调色板发生变化时,所有这些都需要重新绘制,但是当宽度发生变化时,只需要重新绘制10个,它可能会暗示某些事件/状态改变Foo应该发出信号.如果您发现有一个大型事件处理程序OnFooStateChanged()通过检查许多Foo的属性来查看已更改的内容,为了最大限度地减少UI更新,它会提示有关Foo事件模型的粒度的信息.如果您发现想要编写一个小的独立UI小部件,您可以在UI中的多个位置使用它,但它需要知道Foo何时更改并且您不希望包含Foo实现带来的所有代码,它建议一些与您的UI相关的数据组织,是你的表示层,比"我的表单类中的所有代码"更严肃.
-PC
有一本关于这个主题的书.可悲的是,它的绝版和罕见的二手产品非常昂贵.
Constructing the User Interface with Statecharts by Ian Horrocks, Addison-Wesley, 1998