MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript实现复杂状态机类型建模

2022-03-111.5k 阅读

一、状态机基础概念

(一)什么是状态机

状态机(State Machine),简单来说,是一种数学模型,它可以处于有限个状态中的某一个。状态机根据当前所处的状态以及输入的事件,决定执行相应的动作,并转换到新的状态。例如,在一个简单的交通灯系统中,交通灯有红灯、绿灯、黄灯三种状态。当接收到“时间到”这个事件时,交通灯从绿灯状态转换到黄灯状态,并执行“闪烁”的动作。

(二)状态机的组成部分

  1. 状态(States):状态机当前所处的状况,如上述交通灯例子中的红灯、绿灯、黄灯。状态机在某一时刻只能处于一个状态。
  2. 事件(Events):导致状态机状态发生改变的触发条件。例如交通灯中的“时间到”事件。
  3. 动作(Actions):在状态转换时执行的操作,像交通灯从绿灯到黄灯转换时的“闪烁”动作。
  4. 状态转换(Transitions):从一个状态到另一个状态的转变,由事件触发,并伴随着相应动作的执行。

(三)状态机的应用场景

  1. 用户界面交互:例如在一个模态框组件中,模态框有打开、关闭、最小化等状态。用户点击“关闭”按钮(事件),模态框从打开状态转换到关闭状态,并执行隐藏模态框的动作。
  2. 游戏开发:游戏角色的状态管理,如角色的站立、行走、跳跃、攻击等状态。当玩家按下“跳跃”键(事件),角色从站立状态转换到跳跃状态,并执行跳跃动画等动作。
  3. 工作流系统:比如一个审批流程,文档有提交、审核中、通过、驳回等状态。当审核人员点击“通过”按钮(事件),文档从审核中状态转换到通过状态,并执行通知提交者等动作。

二、TypeScript 基础与类型系统

(一)TypeScript 简介

TypeScript 是 JavaScript 的超集,它扩展了 JavaScript 的语法,为其添加了静态类型系统。这意味着在 TypeScript 中,我们可以在代码编写阶段就指定变量、函数参数和返回值的类型,提前发现类型相关的错误,提高代码的可维护性和稳定性。例如:

let num: number = 10;
function add(a: number, b: number): number {
    return a + b;
}

(二)TypeScript 类型系统基础

  1. 基本类型:TypeScript 支持常见的基本类型,如 number(数字)、string(字符串)、boolean(布尔值)、nullundefinedvoid(表示没有任何类型,常用于函数返回值)等。
  2. 对象类型:我们可以使用接口(interface)或类型别名(type)来定义对象的形状。例如:
interface Person {
    name: string;
    age: number;
}
let person: Person = { name: 'John', age: 30 };

type Point = {
    x: number;
    y: number;
};
let point: Point = { x: 1, y: 2 };
  1. 联合类型与交叉类型
    • 联合类型:表示取值可以为多种类型中的一种。例如:let value: string | number; value = 'hello'; value = 10;
    • 交叉类型:表示同时满足多种类型的要求。例如:interface A { a: string; } interface B { b: number; } let ab: A & B = { a: 'a', b: 1 };
  2. 类型推断:TypeScript 编译器可以根据变量的赋值自动推断其类型。例如:let num = 10; // num 被推断为 number 类型

三、TypeScript 实现简单状态机类型建模

(一)定义状态类型

我们以一个简单的开关状态机为例,开关有“开”和“关”两种状态。首先,我们使用 TypeScript 的枚举(enum)来定义状态类型:

enum SwitchState {
    ON = 'on',
    OFF = 'off'
}

这里通过 enum 定义了 SwitchState 枚举类型,其中 ONOFF 分别表示开关的“开”和“关”状态,并且我们给它们赋予了字符串值。

(二)定义事件类型

对于开关状态机,可能的事件是“按下开关”。我们同样可以使用枚举来定义事件类型:

enum SwitchEvent {
    PRESS = 'press'
}

(三)状态转换函数

接下来,我们编写一个函数来处理状态转换。这个函数接收当前状态和事件作为参数,并返回新的状态:

function switchStateTransition(currentState: SwitchState, event: SwitchEvent): SwitchState {
    if (currentState === SwitchState.ON && event === SwitchEvent.PRESS) {
        return SwitchState.OFF;
    } else if (currentState === SwitchState.OFF && event === SwitchEvent.PRESS) {
        return SwitchState.ON;
    }
    return currentState;
}

(四)使用状态机

现在我们可以使用这个状态机了:

let currentSwitchState: SwitchState = SwitchState.OFF;
console.log('当前状态:', currentSwitchState);

currentSwitchState = switchStateTransition(currentSwitchState, SwitchEvent.PRESS);
console.log('按下开关后状态:', currentSwitchState);

currentSwitchState = switchStateTransition(currentSwitchState, SwitchEvent.PRESS);
console.log('再次按下开关后状态:', currentSwitchState);

在这个简单的示例中,我们实现了一个基本的开关状态机,通过 TypeScript 的类型系统明确了状态和事件的类型,使得代码更加健壮和可维护。

四、TypeScript 实现复杂状态机类型建模

(一)复杂状态机示例 - 自动售货机

  1. 状态定义:自动售货机有“空闲”、“投币中”、“选择商品”、“出货”、“找零”等状态。我们使用接口和类型别名来定义状态:
interface VendingMachineState {
    name: string;
}

type IdleState = {
    name: 'idle';
};

type InsertingCoinState = {
    name: 'insertingCoin';
    insertedAmount: number;
};

type SelectingProductState = {
    name:'selectingProduct';
    selectedProduct: string;
};

type DispensingState = {
    name: 'dispensing';
};

type ReturningChangeState = {
    name:'returningChange';
    changeAmount: number;
};

type VendingMachineStates = IdleState | InsertingCoinState | SelectingProductState | DispensingState | ReturningChangeState;

这里我们首先定义了一个 VendingMachineState 接口作为所有自动售货机状态的基础接口,它只有一个 name 属性用于标识状态名称。然后分别定义了各个具体状态的类型,最后使用联合类型 VendingMachineStates 来表示自动售货机可能处于的所有状态。

  1. 事件定义:自动售货机可能接收到的事件有“投币”、“选择商品”、“确认购买”、“取消购买”等。同样使用枚举来定义事件类型:
enum VendingMachineEvent {
    INSERT_COIN = 'insertCoin',
    SELECT_PRODUCT ='selectProduct',
    CONFIRM_PURCHASE = 'confirmPurchase',
    CANCEL_PURCHASE = 'cancelPurchase'
}
  1. 状态转换表与函数:为了实现状态转换,我们可以使用一个状态转换表来存储不同状态下对不同事件的响应。状态转换表可以用一个对象来表示,对象的键是当前状态的名称,值是另一个对象,这个内部对象的键是事件名称,值是一个函数,用于计算新的状态。
const vendingMachineTransitionTable: {
    [state in VendingMachineStates['name']]: {
        [event in VendingMachineEvent]?: (currentState: VendingMachineStates) => VendingMachineStates;
    };
} = {
    idle: {
        insertCoin: (currentState: IdleState): InsertingCoinState => ({
            name: 'insertingCoin',
            insertedAmount: 0
        }),
        selectProduct: (currentState: IdleState): SelectingProductState => ({
            name:'selectingProduct',
            selectedProduct: ''
        })
    },
    insertingCoin: {
        insertCoin: (currentState: InsertingCoinState): InsertingCoinState => ({
            name: 'insertingCoin',
            insertedAmount: currentState.insertedAmount + 1
        }),
        confirmPurchase: (currentState: InsertingCoinState): DispensingState => ({
            name: 'dispensing'
        }),
        cancelPurchase: (currentState: InsertingCoinState): IdleState => ({
            name: 'idle'
        })
    },
    selectingProduct: {
        selectProduct: (currentState: SelectingProductState): SelectingProductState => ({
            name:'selectingProduct',
            selectedProduct: 'newProduct'
        }),
        confirmPurchase: (currentState: SelectingProductState): DispensingState => ({
            name: 'dispensing'
        }),
        cancelPurchase: (currentState: SelectingProductState): IdleState => ({
            name: 'idle'
        })
    },
    dispensing: {
        // 出货后进入找零状态,假设商品价格为 5,这里简单处理找零金额
        dispensing: (currentState: DispensingState): ReturningChangeState => ({
            name:'returningChange',
            changeAmount: 5
        })
    },
    returningChange: {
        returningChange: (currentState: ReturningChangeState): IdleState => ({
            name: 'idle'
        })
    }
};

function vendingMachineStateTransition(currentState: VendingMachineStates, event: VendingMachineEvent): VendingMachineStates {
    const transitionFunction = vendingMachineTransitionTable[currentState.name][event];
    if (transitionFunction) {
        return transitionFunction(currentState);
    }
    return currentState;
}

在上述代码中,vendingMachineTransitionTable 定义了状态转换表,根据不同的当前状态和事件计算新的状态。vendingMachineStateTransition 函数根据当前状态和事件从转换表中获取相应的转换函数,并执行它以得到新的状态。

  1. 使用复杂状态机
let currentVendingMachineState: VendingMachineStates = { name: 'idle' };
console.log('当前自动售货机状态:', currentVendingMachineState.name);

currentVendingMachineState = vendingMachineStateTransition(currentVendingMachineState, VendingMachineEvent.INSERT_COIN);
console.log('投币后状态:', currentVendingMachineState.name);

currentVendingMachineState = vendingMachineStateTransition(currentVendingMachineState, VendingMachineEvent.INSERT_COIN);
console.log('再次投币后状态:', currentVendingMachineState.name);

currentVendingMachineState = vendingMachineStateTransition(currentVendingMachineState, VendingMachineEvent.CONFIRM_PURCHASE);
console.log('确认购买后状态:', currentVendingMachineState.name);

currentVendingMachineState = vendingMachineStateTransition(currentVendingMachineState, VendingMachineEvent.DISPENSING);
console.log('出货后状态:', currentVendingMachineState.name);

currentVendingMachineState = vendingMachineStateTransition(currentVendingMachineState, VendingMachineEvent.RETURNING_CHANGE);
console.log('找零后状态:', currentVendingMachineState.name);

通过这个复杂的自动售货机状态机示例,我们展示了如何使用 TypeScript 对复杂状态机进行类型建模。从状态和事件的详细定义,到状态转换表和转换函数的实现,TypeScript 的类型系统确保了代码的类型安全性和可维护性。

(二)使用类和接口实现状态模式的复杂状态机

  1. 定义状态接口
interface VendingMachineStateInterface {
    handleEvent(event: VendingMachineEvent): VendingMachineStateInterface;
}

这里定义了一个 VendingMachineStateInterface 接口,它有一个 handleEvent 方法,用于处理事件并返回新的状态。

  1. 实现具体状态类
class IdleStateClass implements VendingMachineStateInterface {
    name = 'idle';
    handleEvent(event: VendingMachineEvent): VendingMachineStateInterface {
        if (event === VendingMachineEvent.INSERT_COIN) {
            return new InsertingCoinStateClass();
        } else if (event === VendingMachineEvent.SELECT_PRODUCT) {
            return new SelectingProductStateClass();
        }
        return this;
    }
}

class InsertingCoinStateClass implements VendingMachineStateInterface {
    name = 'insertingCoin';
    insertedAmount = 0;
    handleEvent(event: VendingMachineEvent): VendingMachineStateInterface {
        if (event === VendingMachineEvent.INSERT_COIN) {
            this.insertedAmount++;
            return this;
        } else if (event === VendingMachineEvent.CONFIRM_PURCHASE) {
            return new DispensingStateClass();
        } else if (event === VendingMachineEvent.CANCEL_PURCHASE) {
            return new IdleStateClass();
        }
        return this;
    }
}

class SelectingProductStateClass implements VendingMachineStateInterface {
    name ='selectingProduct';
    selectedProduct = '';
    handleEvent(event: VendingMachineEvent): VendingMachineStateInterface {
        if (event === VendingMachineEvent.SELECT_PRODUCT) {
            this.selectedProduct = 'newProduct';
            return this;
        } else if (event === VendingMachineEvent.CONFIRM_PURCHASE) {
            return new DispensingStateClass();
        } else if (event === VendingMachineEvent.CANCEL_PURCHASE) {
            return new IdleStateClass();
        }
        return this;
    }
}

class DispensingStateClass implements VendingMachineStateInterface {
    name = 'dispensing';
    handleEvent(event: VendingMachineEvent): VendingMachineStateInterface {
        if (event === VendingMachineEvent.DISPENSING) {
            return new ReturningChangeStateClass();
        }
        return this;
    }
}

class ReturningChangeStateClass implements VendingMachineStateInterface {
    name ='returningChange';
    changeAmount = 5;
    handleEvent(event: VendingMachineEvent): VendingMachineStateInterface {
        if (event === VendingMachineEvent.RETURNING_CHANGE) {
            return new IdleStateClass();
        }
        return this;
    }
}

这里分别实现了 IdleStateClassInsertingCoinStateClass 等具体状态类,每个类都实现了 VendingMachineStateInterface 接口的 handleEvent 方法,根据接收到的事件返回新的状态实例。

  1. 使用状态模式的状态机
let currentVendingMachineStateClass: VendingMachineStateInterface = new IdleStateClass();
console.log('当前自动售货机状态(状态模式):', currentVendingMachineStateClass.name);

currentVendingMachineStateClass = currentVendingMachineStateClass.handleEvent(VendingMachineEvent.INSERT_COIN);
console.log('投币后状态(状态模式):', currentVendingMachineStateClass.name);

currentVendingMachineStateClass = currentVendingMachineStateClass.handleEvent(VendingMachineEvent.INSERT_COIN);
console.log('再次投币后状态(状态模式):', currentVendingMachineStateClass.name);

currentVendingMachineStateClass = currentVendingMachineStateClass.handleEvent(VendingMachineEvent.CONFIRM_PURCHASE);
console.log('确认购买后状态(状态模式):', currentVendingMachineStateClass.name);

currentVendingMachineStateClass = currentVendingMachineStateClass.handleEvent(VendingMachineEvent.DISPENSING);
console.log('出货后状态(状态模式):', currentVendingMachineStateClass.name);

currentVendingMachineStateClass = currentVendingMachineStateClass.handleEvent(VendingMachineEvent.RETURNING_CHANGE);
console.log('找零后状态(状态模式):', currentVendingMachineStateClass.name);

通过使用类和接口实现状态模式的复杂状态机,我们进一步展示了 TypeScript 在面向对象编程风格下对状态机建模的能力。这种方式使得状态和状态转换的逻辑更加清晰和模块化,更易于理解和维护复杂状态机的代码。

五、复杂状态机类型建模中的高级技巧与注意事项

(一)类型保护与类型守卫

在处理状态机的状态转换时,经常需要根据当前状态的类型进行不同的操作。TypeScript 的类型保护和类型守卫可以帮助我们在代码中准确地判断状态类型。例如,在前面自动售货机的状态转换函数中,我们可以使用类型守卫来确保在特定状态下执行正确的逻辑:

function vendingMachineStateTransition(currentState: VendingMachineStates, event: VendingMachineEvent): VendingMachineStates {
    if ('insertedAmount' in currentState && event === VendingMachineEvent.INSERT_COIN) {
        return {
           ...currentState,
            insertedAmount: currentState.insertedAmount + 1
        } as InsertingCoinState;
    }
    // 其他状态的类型守卫和逻辑处理
    const transitionFunction = vendingMachineTransitionTable[currentState.name][event];
    if (transitionFunction) {
        return transitionFunction(currentState);
    }
    return currentState;
}

这里通过 'insertedAmount' in currentState 这种类型守卫来判断当前状态是否是 InsertingCoinState,如果是,则执行相应的状态转换逻辑。

(二)泛型在状态机中的应用

泛型可以使我们的状态机代码更加通用和灵活。例如,我们可以定义一个泛型状态机框架,使得不同类型的状态机都可以复用这个框架。

type StateType = string;
type EventType = string;

interface StateMachine<S extends StateType, E extends EventType> {
    currentState: S;
    transitionTable: {
        [state in S]: {
            [event in E]?: (currentState: S) => S;
        };
    };
    transition(event: E): S;
}

function createStateMachine<S extends StateType, E extends EventType>(initialState: S, transitionTable: {
    [state in S]: {
        [event in E]?: (currentState: S) => S;
    };
}): StateMachine<S, E> {
    return {
        currentState: initialState,
        transitionTable,
        transition(event) {
            const transitionFunction = this.transitionTable[this.currentState][event];
            if (transitionFunction) {
                this.currentState = transitionFunction(this.currentState);
            }
            return this.currentState;
        }
    };
}

// 使用泛型状态机框架创建一个简单的状态机示例
enum SimpleState {
    STATE1 ='state1',
    STATE2 ='state2'
}

enum SimpleEvent {
    EVENT1 = 'event1',
    EVENT2 = 'event2'
}

const simpleTransitionTable: {
    [state in SimpleState]: {
        [event in SimpleEvent]?: (currentState: SimpleState) => SimpleState;
    };
} = {
    state1: {
        event1: () => SimpleState.STATE2
    },
    state2: {
        event2: () => SimpleState.STATE1
    }
};

const simpleStateMachine = createStateMachine(SimpleState.STATE1, simpleTransitionTable);
console.log('简单状态机初始状态:', simpleStateMachine.currentState);
simpleStateMachine.transition(SimpleEvent.EVENT1);
console.log('简单状态机触发 EVENT1 后状态:', simpleStateMachine.currentState);

在上述代码中,我们定义了一个泛型状态机框架 StateMachine,通过 SE 泛型参数分别表示状态类型和事件类型。createStateMachine 函数用于创建具体的状态机实例,这样我们可以方便地为不同的状态机需求复用这个框架。

(三)注意事项

  1. 状态转换的完整性:在定义状态转换表或状态转换逻辑时,要确保覆盖所有可能的状态和事件组合。否则,在运行时可能会出现未预期的状态转换或错误。例如,在自动售货机状态机中,如果遗漏了某个状态下对某个事件的处理,可能导致状态机进入无效状态。
  2. 类型兼容性:在使用联合类型、交叉类型等复杂类型时,要注意类型之间的兼容性。确保状态转换函数返回的新状态类型与预期的状态类型一致。例如,在定义状态转换表时,函数返回的状态类型必须是 VendingMachineStates 联合类型中的一种。
  3. 可维护性与可读性:随着状态机的复杂度增加,代码的可维护性和可读性变得尤为重要。合理地使用接口、类、类型别名等 TypeScript 特性来组织代码,使状态、事件和状态转换逻辑清晰明了。例如,将状态相关的逻辑封装在各自的状态类中,通过接口统一对外提供 handleEvent 方法,这样可以提高代码的可维护性。

通过掌握上述高级技巧并注意相关事项,我们能够更加高效、准确地使用 TypeScript 实现复杂状态机的类型建模,为各种复杂系统的开发提供坚实的基础。无论是在大型应用程序的状态管理,还是在游戏、工业控制等领域的状态机设计中,TypeScript 的类型系统都能发挥重要作用,帮助我们编写可靠、可维护的代码。