lua开发:有限状态机模式设计

有限状态机,表示有限个状态以及在这些状态之间的转移和动作等行为的处理模型。在任意时刻有限状态机都处于某一特定的状态,并且可以根据当前状态和输入条件(触发事件),从当前状态转移到另一个状态。

核心概念:

实体(Entity):状态机的主体和作用对象,它的状态可以改变

状态(State):实体在某一特定时间点的情况,可分为现态(当前状态)、次态(跳转新状态)

事件(Event):导致状态转换的触发器和条件

动作(Action):状态转换时执行的操作

讨论抽象状态对象的实现模式:将对象的状态分离并封装到专用的状态类,使得对象状态可灵活变化,当状态改变时会相应改变对象的行为。

封装状态类

local EventDispatcher = {}

---指定事件输入当前状态对象触发回调动作
---这里回调需要的上下文从外部传入,确保状态对象设计无状态
function EventDispatcher:trigger(context, event, ...)
    local callback = self._event_handler[event]
    local nextState, cmd
    if callback then -- 输入事件符合当前状态对象的期望,触发回调动作
    		-- todo 这里可能需要处理并发
        nextState, cmd = table.unpack(callback)
        local ok = xpcall(context[cmd], debug.traceback, ...)
        return ok, nextState
    end
    return false
end

---创建状态对象
---@param:
---|state:当前状态对象对应的预定义状态
---|events:{ {event_type, next_state, cb_cmd_name}, ... } 当前状态下可接受的事件集
local function createState(state, events)
    local data = {
        _state = state, -- 所在状态
        _event_handler = {} -- 当前状态期望接收的事件及对应触发的动作
    }
    local object = setmetatable(data, { __index = EventDispatcher })
    for _, event in pairs(self._events) do
        self._event_handler[event[1]] = {event[2], event[3]}
    end
    return object
end

状态类维护所有期望的输入事件(条件)与对应触发的动作之间的映射关系,对外提供事件触发处理的API;

状态类设计之上,需要维护一个状态对象集用于描述指定业务的状态切换关系。创建这个集合需要依据业务层预定义的状态关系;

local _config_state_transition -- 业务层预定义的状态对应关系:{ [state] = {event, ...} }

local _state_object = {}, -- 状态to状态对象的映射
for state, events in pairs(_config_state_transition) do
  	_state_object[state] = createState(state, events)
end

封装状态机类:状态机对象与当前所在的状态解耦,将状态类以组合的方式引用到状态机内部,那么,状态机需要有以下属性:

local data = {
    _id = id, -- 关联的外部上下文ID,用来标识代理的业务层对象
    _context = context, -- 引用住外部上下文所在的服务环境,用于状态类回调触发
    _state = state, -- 当前状态机所处的状态
    _state_object = {}, -- 状态to状态对象的映射
  	_event_queue = queue.new(), -- 待处理事件队列
}
local FSM = {}

function FSM:init(...)
  	fork(self.event_dispatch, self) -- 开启事件队列的循环分发处理
end

---@根据当前所在状态获取到对应的状态对象,在对象内触发对应事件的行为
function FSM:trgEvent(event)
  	local stateobj = self._state_object[self._state]
    local ok, nextState = stateobj:trigger(self._context, table.unpack(event))
    if ok and nextState then
        self._state = nextState
    end
end

---@状态机内开启独立的执行序用于循环消费事件队列
function FSM.event_dispatch()
		local event
    while true do
        event = self._event_queue:pop()
        if not event then -- 当前没有待处理事件时挂起,等待队列有新增事件时被唤醒
            self._suspendco = coroutine.running()
            suspend()
            self._suspendco = nil
        else
            self:trgEvent(event)
        end
    end
end

-- 创建状态机实例
local function createEntity(id, context, init_state, ...)
    local data = {
        _id = id,
        _context = context,
        _state = init_state,
        _state_object = _state_object,
        _event_queue = queue.new(),
    		_suspendco = nil,
    }
    local entity = setmetatable(data, { __index = FSM })
    entity:init(...)
    return entity
end

上述设计是针对单状态机业务而言的。

对于一类业务中需要同时维护大量状态变化对象的场景,需要维护批量的状态机实例。上述状态机设计方案在单个实例内维护状态对象的映射以及事件队列,在批量实例的场景下存在消耗叠合的情况。

此时可以抽象出状态机实例管理器,用于统一协调管理批量状态机;

封装Entity管理器:

状态对象集是静态且无状态的,在批量的场景下应该与具体的状态机实例分离,组合在实例管理器中;

实例的事件消费由管理器统一驱动,所有实例的事件触发共用同一事件队列,减少队列重复消耗,使调度更集中;

维护外部上下文ID与状态机实例的映射关系,对业务层隐藏状态机实例设计,业务层通过上下文ID来索引到对应的状态机实例;

外部业务只持有管理器引用;

local data = {
    _modname = modname, -- 所在的上层业务模块名称
    _state_transition = transitionOp, -- 状态预定义表
    _state_object = {}, -- 状态to状态对象的映射
    _id_entity = {}, -- 外部上下文ID to 状态机实例的映射关系
    _event_queue = queue.new(), -- 待处理事件队列
    _suspendco = nil -- dispatcher 空闲挂起时记录coroutine
}
-- 创建当前业务对应的状态对象集
function Manager:init()
    for state, events in pairs(self._state_transition) do
        self._state_object[state] = createState(state, events)
    end

    skynet.fork(self.dispatch, self) -- 开启事件队列的循环分发处理
end

-- 为业务层上下文封装状态机实例
function Manager:createEntity(id, context, state)
    local entity = createEntity(self, id, context, state)
    self._id_entity[id] = entity
end

function Manager:getStateObject(state)
    return self._state_object[state]
end

-- 触发事件,由dispatcher内部使用
local function trgEvent(manager, event)
    local id = table.slice(event, 1, 1)
    local fsm = manager._id_entity[id[1]]
    return fsm:trgEvent(table.unpack(table.slice(event, 2, #event)))
end

function Manager:addEvent(id, ev, ...)
    local event = {id, ev, ...}
    self._event_queue:push(event)
    if self._suspendco then
        skynet.wakeup(self._suspendco)
    end
end

function Manager:dispatcher()
		-- ...
end

local function createManager(modname, transitionOp)
    local data = {
				-- ...
    }
    local manager = setmetatable(data, { __index = Manager })
    manager:init()
    return manager
end
posted @ 2024-07-20 22:44  linxx-  阅读(117)  评论(0编辑  收藏  举报