我正在寻找对前端小部件代码进行去spaghttify 的方法。有人建议有限状态机是思考我在做什么的正确方法。我知道状态机范式几乎可以应用于任何问题。我想知道是否有一些经验丰富的 UI 程序员真正养成了这种习惯。
所以,问题是——你们中的任何一个 UI 程序员在你的工作中是否考虑到状态机?如果是这样,怎么做?
谢谢,-摩根
我正在寻找对前端小部件代码进行去spaghttify 的方法。有人建议有限状态机是思考我在做什么的正确方法。我知道状态机范式几乎可以应用于任何问题。我想知道是否有一些经验丰富的 UI 程序员真正养成了这种习惯。
所以,问题是——你们中的任何一个 UI 程序员在你的工作中是否考虑到状态机?如果是这样,怎么做?
谢谢,-摩根
我目前正在使用一个(专有)框架,该框架非常适合 UI-as-state-machine 范例,它肯定可以减少(但不能消除)UI 元素之间复杂和不可预见的交互问题。
主要好处是它允许您以更高的抽象层次、更高的粒度进行思考。与其认为“如果按下按钮 A 则组合框 B 被锁定,文本字段 C 被清除并且按钮 D 被解锁”,而是认为“按下按钮 A 将应用程序置于 CHECKED 状态” - 进入该状态意味着某些事情发生。
不过,我认为将整个 UI 建模为单个状态机是没有用的(甚至是不可能的)。相反,通常有许多较小的状态机,每个状态机处理 UI 的一部分(由几个在概念上交互并属于一起的控件组成),以及一个(可能不止一个)处理更基本问题的“全局”状态机。
状态机通常太低级,无法帮助您考虑用户界面。它们为 UI 工具包提供了一个不错的实现选择,但是在普通应用程序中要描述的状态和转换太多,您无法手动描述它们。
我喜欢考虑具有延续性的 UI。(谷歌一下——这个词足够具体,你会得到很多高质量的点击。)
我的应用程序不是处于由状态标志和模式表示的各种状态,而是使用延续来控制应用程序接下来要做什么。用一个例子来解释是最容易的。假设您想在发送电子邮件之前弹出一个确认对话框。第 1 步构建电子邮件。第 2 步得到确认。步骤 3 发送电子邮件。大多数 UI 工具包都要求您在每个步骤之后将控制权传递回事件循环,如果您尝试使用状态机来表示它,这会变得非常丑陋。使用 continuation,您不必考虑工具包强加给您的步骤——它只是构建和发送电子邮件的一个过程。但是,当流程需要确认时,您可以继续捕获应用程序的状态,并将该继续传递给确认对话框上的 OK 按钮。按下确定时,
延续在编程语言中相对较少,但幸运的是,您可以使用闭包获得某种穷人的版本。回到电子邮件发送示例,此时您需要确认您将流程的其余部分编写为闭包,然后将该闭包交给 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 的组织,是您的表示层,比“我的表单类中的所有代码”更严重。
-个人电脑
有一本关于这个主题的书。可悲的是,它绝版了,稀有的二手货非常昂贵。
Constructing the User Interface with Statecharts
by Ian Horrocks, Addison-Wesley, 1998
我们只是在谈论 Horrocks 的使用状态图构建用户界面,二手价格从 250 美元到近 700 美元不等。我们的软件开发经理将其评为他所拥有的最重要的书籍之一(遗憾的是,他住在世界的另一端)。
Samek 的状态图书籍从这项工作中汲取了很多灵感,尽管领域略有不同,据报道并不那么清晰。“嵌入式系统的 C/C++ 事件驱动编程中的实用 UML 状态图”也可在Safari上找到。
Horrocks 被大量引用 - ACM 门户上有20 篇论文,所以如果您可以访问那里,您可能会发现一些有用的东西。
有一本书和软件FlashMX for Interactive Simulation。他们有一个关于状态图的 PDF示例章节。
带有 UML 的对象、组件和框架:催化(SM)方法有一章是关于行为模型的,其中包括大约十页使用状态图的有用示例(我注意到它可以非常便宜地获得二手货)。这是相当正式和沉重的,但该部分很容易阅读。
老实说,这并不是真正的 UI 问题。
我会做以下事情:
我得到了一个关于我称之为“状态优先”的模式的预演。
它是 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 中的所有转换,您可以在视图/模型之间获得超低耦合、高可测试性以及(如果您的语言允许)安全的大量类型。
使用 FSM 的一个重要点是,您不应该只是在状态之间跳转 - 您还应该在跳转时携带所有有状态的数据(作为参数,见loggedIn
上文)。这将帮助您避免通常会乱扔 gui 代码的全局状态。
您可以在http://prezi.com/bqcr5nhcdhqu/上观看演示文稿,但目前不包含代码示例。
呈现给用户的每个界面项都可以从当前状态进入另一种状态。您基本上需要创建一个地图,说明哪些按钮可以导致哪些其他状态。
此映射将允许您查看未使用的状态或多个按钮或路径可以导致相同状态而没有其他状态(可以组合的状态)的状态。
嘿 Morgan,我们正在 Radical 在 AS3 中构建自定义框架,并使用状态机范例来支持任何前端 UI 活动。
我们为所有按钮事件、所有显示事件等设置了状态机。
AS3 作为一种事件驱动的语言,使它成为一个非常有吸引力的选择。
当某些事件被捕获时,按钮/显示对象的状态会自动改变。
拥有一组通用的状态绝对可以帮助你的代码去spaghttify!
状态机是允许代码与其他状态机一起工作的东西。状态机只是具有过去事件记忆的逻辑。
因此,人类是状态机,他们通常希望他们的软件能够记住他们过去所做的事情,以便他们能够继续。
例如,您可以将整个调查放在一个页面上,但人们更愿意使用多个较小页面的问题。与用户注册相同。
所以状态机对用户界面有很多适用性。
但是,在部署它们之前应该理解它们,并且在编写代码之前必须完成整个设计 - 状态机可以,现在并且将会被滥用,如果你不知道为什么要使用它一,目标是什么,你最终可能会比其他技术更糟糕。
-亚当