英文:
How can I extract this variable's initialization out of my object's constructor?
问题
我正在编写我的第一个Go程序,一个SMTP服务器,并且我认为使用有限状态机(FSM)来表示网络协议的状态转换会很优雅。我非常喜欢这个Haskell SMTP FSM示例,所以我松散地模仿了它。
我创建了一个简单的FSM类型,它以转换表作为构造函数参数,并且有一个Run方法,它接受一个事件,并调用状态表中匹配的适当处理函数。然后我有一个“Session”类型,它在处理来自连接的SMTP命令后需要利用FSM。
这是一个FSM转换的样子:
type Transition struct {
from State
event Event
to State
handler func() string
}
然后,在我的Session对象中,我被迫在其构造函数中定义转换表,以便我可以访问其方法以进行转换操作:
func (s *SmtpSession) NewSession() {
transitions := []Transition{
{Initial, Rset, Initial, sayOk},
{HaveHelo, Rset, HaveHelo, sayOk},
{AnyState, Rset, HaveHelo, resetState},
...
{Initial, Data, Initial, needHeloFirst},
{HaveHelo, Data, HaveHelo, needMailFromFirst},
{HaveMailFrom, Data, HaveMailFrom, needRcptToFirst},
{HaveRcptTo, Data, HaveData, startData},
}
smtpFsm = StateMachine.NewMachine(transitions)
}
这样做似乎很浪费,因为每个会话都必须创建一个此FSM的实例,而所有会话实际上都将具有相同的FSM。我更希望只有一种“静态”FSM,它可以接收一个转换表,然后Run方法将接受当前状态和事件,并返回相应的“动作”函数。
然而,这就是我遇到的问题。因为所有的处理函数实际上都是Session对象的方法,我必须在Session内部定义它。我无法想到一种方法,可以为所有会话仅定义一次此转换表,并且仍然可以访问我需要的Session处理函数。
如果我以纯过程化的方式编写此程序,那么我就不会遇到这些问题。FSM将直接访问所有处理函数。
我唯一能想到的办法是修改我的FSM,不返回函数指针,而是返回一些任意的常量,然后Session将其映射到相应的函数:
var transitions = []Transition{
{Initial, Rset, Initial, "sayOk"},
{HaveHelo, Rset, HaveHelo, "sayOk"},
{AnyState, Rset, HaveHelo, "resetState"},
...
}
var smtpFsm = NewStateMachine(transitions)
func (s *Session) handleInput(cmd string) {
event := findEvent(cmd)
handler := findHandler(smtpFsm.Run(s.currentState, event))
handler(cmd)
}
func (s *Session) findHandler(handlerKey string) {
switch handlerKey {
case "sayOk":
return sayOk
case "resetState":
return resetState
}
}
这将解决为每个会话重新初始化新FSM的问题,但它也感觉有点不正规。有人有任何建议,我如何避免这个问题吗?这是一个链接到未完成的Session.go,演示了这个问题。
英文:
I'm writing my first Go program, an SMTP server, and I thought that using a FSM to represent the state transitions of the network protocol would be elegant. I really liked this haskell SMTP FSM example so I loosely modeled it after that.
I've created a simple FSM type which takes a transition table as its constructor argument and has a Run method which takes an event and will call the appropriate handler function it matches in the state table. I then have a "Session" type which is what needs to utilize the FSM after processing incoming SMTP commands from its connection.
Here is what a FSM Transition looks like:
type Transition struct {
from State
event Event
to State
handler func() string
}
Then, in my Session object, I'm forced to define the transition table in its constructor so I can have access to its methods for the transition actions:
func (s *SmtpSession) NewSession() {
transitions := []Transition{
{Initial, Rset, Initial, sayOk},
{HaveHelo, Rset, HaveHelo, sayOk},
{AnyState, Rset, HaveHelo, resetState},
...
{Initial, Data, Initial, needHeloFirst},
{HaveHelo, Data, HaveHelo, needMailFromFirst},
{HaveMailFrom, Data, HaveMailFrom, needRcptToFirst},
{HaveRcptTo, Data, HaveData, startData},
}
smtpFsm = StateMachine.NewMachine(transitions)
}
This seems wasteful to have to create an instance of this FSM as part of each session, when all sessions will have essentially the same FSM. I'd much rather just have some sort of "static" FSM which can be given a transition table, and then the Run method will take a current state, and an event and return the resulting "action" function.
However, that is where I run into trouble. Because all of the handler functions are actually methods of the Session object, I must define it within Session. I can't think of a way I can define this transition table only once for all sessions and still have the appropriate access to the Session handler functions I need.
If I wrote this program in a straight procedural style then I wouldn't have any of these problems. The FSM would have access to all the handler functions directly.
The only thing I can think of is altering my FSM to not return function pointers, but instead return some arbitrary constant that the Session will then map to the appropriate function:
var transitions = []Transition{
{Initial, Rset, Initial, "sayOk"},
{HaveHelo, Rset, HaveHelo, "sayOk"},
{AnyState, Rset, HaveHelo, "resetState"},
...
}
var smtpFsm = NewStateMachine(transitions)
func (s *Session) handleInput(cmd string) {
event := findEvent(cmd)
handler := findHandler(smtpFsm.Run(s.currentState, event))
handler(cmd)
}
func (s *Session) findHandler(handlerKey string) {
switch handlerKey {
case "sayOk":
return sayOk
case "resetState":
return resetState
}
}
This will fix the problem of having to re-intialize a new FSM for each session but it also feels a little hackish. Does anyone have any suggestions for how I might avoid this problem? Here is a link to the incomplete Session.go that demonstrates the problem.
答案1
得分: 3
这整个过程如果你的处理程序不是方法,而是以实例作为参数的函数,会变得更容易。这基本上是相同的事情,但你可以设置一个type State func(Session) State
类型的结构,并且在思考时会更加容易。
英文:
This whole thing gets way easier if you don't have your handlers be methods, and instead functions that take the instance as a parameter. This is roughly the same thing, but you could have a type State func(Session) State
type of setup and have a much easier time thinking about it.
答案2
得分: 2
我不确定这个一般想法有多么hackish。通过关注点的分离,你的状态机只是发出一个令牌,并且不知道以后将如何使用它。在你的handleInput
方法中,你将使用该令牌执行一个动作,这种情况下是在你的会话对象上找到适当的方法。
但整个事情对我来说似乎是矛盾的。你有一个想要了解Session
的转换表。你有一个名为fsm
的包,我猜它几乎什么都不做,但也想要了解Session
,因为它依赖于转换表,以便包能够有任何用途。
我会建议要么切断链接并发出一个const
,Session
可以使用它来找到适当的方法,走更加过程化的路线,或者将这些部分合并得更近一些(可能涉及放弃转换表)。
或者,如果你真的想要走一个hackishly低效的路线,让findHandler
通过名称反射出适当的方法,并且切掉那个switch语句,但只是为了好玩
你还可以考虑一些其他的实现方法。标准库中有很多很好的例子。查看text/template包的文件:
http://golang.org/src/pkg/text/template/parse/lex.go
http://golang.org/src/pkg/text/template/parse/parse.go
我想说的是,正如你已经注意到的,如果你想要在函数外部定义转换表,你需要引用一个令牌或一个函数,而不是一个你没有的实例的方法。但是为了好玩,使用类似下面的东西,然后你可以在你的转换表中说Action("sayOk")
,并将Session
传递给生成的函数。
package main
import (
"fmt"
"reflect"
)
type Foo struct{}
func (f *Foo) Bar() string {
return "hello"
}
func Action(name string) func(f *Foo) string {
return func(f *Foo) string {
s := reflect.ValueOf(f).MethodByName(name).Call([]reflect.Value{})
return s[0].String()
}
}
func main() {
f := &Foo{}
a := Action("Bar")
fmt.Println(a(f))
}
英文:
I'm not sure how hackish the general idea is. With the separation of concerns, then you're state machine is simply emitting a token and remains unaware of how that will be used later on. Within your handleInput
method, you'd be using that token to perform an action, which in this case is to find the appropriate method on your session object.
But the whole thing seems at ends to me. You have a transition table that wants to be aware of Session
. You have package fsm
that I'm guessing does very little but also wants to be aware of Session
due to its dependence on the transition table for the package to be of any use.
I would say either cut the link and emit a const
that Session
can use to find an appropriate method, go the more procedural route, or merge the bits closer (likely involving ditching the transition table).
Or if you really wanted to go the hackishly inefficient route, let findHandler
reflect the appropriate method by name and cut out that switch statement, but only for fun
You also might consider some alternatives for implementation. There are a lot of great examples in the standard library. Check out the package files for text/template:
http://golang.org/src/pkg/text/template/parse/lex.go
http://golang.org/src/pkg/text/template/parse/parse.go
The gist of what I'm saying is, and as you've already noted, if you want that transition table defined outside of a function as you have it, you'll need to reference a token or a function, not a method of an instance you don't have. But for fun, using something like below, then you could say Action("sayOk")
in your transition table, and pass in Session
to the resulting function delivered.
package main
import (
"fmt"
"reflect"
)
type Foo struct{}
func (f *Foo) Bar() string {
return "hello"
}
func Action(name string) func(f *Foo) string {
return func(f *Foo) string {
s := reflect.ValueOf(f).MethodByName(name).Call([]reflect.Value{})
return s[0].String()
}
}
func main() {
f := &Foo{}
a := Action("Bar")
fmt.Println(a(f))
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论