交互逻辑分层模型:把复杂拆成三块

做座舱交互设计,最怕什么?

最怕一个按钮按下去,导航卡了、音乐停了、空调也失灵了。这种「牵一发而动全身」的噩梦,我经历过不止一次。

后来我学乖了。把交互逻辑拆成三层,各管各的,互不干扰。说白了,就是「分而治之」。

三层模型:感知层、决策层、执行层

我习惯把交互逻辑分成三层:

  • 感知层:负责接收输入。触摸、语音、手势、旋钮……所有用户操作都在这层被捕获。
  • 决策层:负责判断「用户想干嘛」。比如用户说「我冷了」,系统得知道这是要调空调温度。
  • 执行层:负责干活。调温度、切歌、导航到目的地……真正改变状态的动作都在这里。

你想想看,如果这三层混在一起,会怎样?

我在一个项目里见过这样的代码:触摸事件里直接调用了空调控制接口。结果后来要加语音控制,整个逻辑都得重写。嗯,那叫一个酸爽。

核心原则:每一层只做自己的事,不越界。

感知层不决策,决策层不执行,执行层不感知。

实际案例:语音调温度

举个例子。用户说「把温度调到26度」。

// 感知层:语音识别
class VoiceInput {
    onVoiceCommand(text) {
        // 只做识别,不做判断
        this.emit('command', { type: 'voice', raw: text });
    }
}

// 决策层:意图理解
class IntentProcessor {
    process(command) {
        if (command.raw.includes('温度')) {
            // 提取温度值
            const temp = parseInt(command.raw.match(/\d+/)[0]);
            return { action: 'setTemp', value: temp };
        }
    }
}

// 执行层:实际控制
class ClimateController {
    setTemperature(temp) {
        // 调用硬件接口
        hvac.setTargetTemp(temp);
    }
}

这样拆开之后,好处很明显:

  • 想换语音引擎?只改感知层。
  • 想加手势控制?再加一个感知模块就行。
  • 空调硬件换了?只改执行层。

我的经验:刚开始做分层时,别追求完美。先拆成三层,哪怕每层只有几个函数。后面再慢慢细化。

事件驱动机制:别轮询,等通知

座舱里同时发生的事情太多了。用户操作、传感器数据、网络请求……如果都用轮询,CPU得累死。

事件驱动,说白了就是「有事叫我,没事别烦我」。

事件的三要素

一个完整的事件机制,需要三样东西:

  1. 事件源:谁发出的?比如触摸屏、麦克风。
  2. 事件类型:什么事?比如 click、voiceCommand。
  3. 事件数据:带什么信息?比如坐标、语音文本。

我见过最糟糕的设计,是把所有事件都塞到一个全局变量里。结果调试的时候,根本不知道是谁改了这个变量。嗯,那叫一个崩溃。

事件总线:一个简单的实现

class EventBus {
    constructor() {
        this.listeners = {};
    }

    on(event, callback) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event].push(callback);
    }

    emit(event, data) {
        const callbacks = this.listeners[event] || [];
        callbacks.forEach(cb => cb(data));
    }

    off(event, callback) {
        const callbacks = this.listeners[event] || [];
        this.listeners[event] = callbacks.filter(cb => cb !== callback);
    }
}

用起来很简单:

const bus = new EventBus();

// 订阅事件
bus.on('temperatureChanged', (temp) => {
    updateDisplay(temp);
});

// 发布事件
bus.emit('temperatureChanged', 26);

避坑指南:我曾经在一个项目里忘了取消订阅,结果事件回调越积越多,内存泄漏了。记得在组件销毁时调用 off()。

事件优先级

座舱里有些事件需要优先处理。比如安全相关的告警,必须排在音乐切换前面。

优先级 事件类型 示例
安全告警 碰撞预警、车道偏离
用户操作 触摸、语音指令
后台更新 地图数据下载

实现优先级队列也不难,给每个事件加个 priority 字段就行。

状态管理基础:别让状态「跑偏」

座舱的状态,说白了就是「当前系统是什么样子」。空调温度、导航目的地、音乐播放列表……这些都是状态。

状态管理最怕什么?最怕多个地方同时改同一个状态,最后谁也不知道真实值是多少。

单一数据源

我坚持一个原则:所有状态只存一份

比如空调温度,只在状态管理里存一次。UI 显示、语音反馈、硬件控制,都从同一个地方读。

// 状态管理
class StateManager {
    constructor() {
        this.state = {
            climate: { temp: 24, fanSpeed: 2 },
            media: { track: 'Song A', volume: 50 },
            nav: { destination: null }
        };
        this.listeners = {};
    }

    get(key) {
        return this.state[key];
    }

    set(key, value) {
        this.state[key] = { ...this.state[key], ...value };
        this.notify(key, this.state[key]);
    }

    subscribe(key, callback) {
        if (!this.listeners[key]) {
            this.listeners[key] = [];
        }
        this.listeners[key].push(callback);
    }

    notify(key, data) {
        (this.listeners[key] || []).forEach(cb => cb(data));
    }
}

关键点:状态变更必须通过 set 方法,不能直接修改 state 对象。这样才能触发通知,让 UI 更新。

状态变更的「原子性」

有时候一个操作要改多个状态。比如「开启空调」这个动作,要同时改温度、风速、模式。

我建议把这些操作封装成一个 action:

function turnOnAC(stateManager) {
    stateManager.set('climate', {
        temp: 24,
        fanSpeed: 2,
        mode: 'cool',
        power: 'on'
    });
}

这样做的好处是:要么全部改成功,要么一个都不改。不会出现「温度改了但风速没改」的尴尬情况。

我的习惯:每个 action 只做一件事。如果「开启空调」和「设置温度」是两件事,就拆成两个 action。这样复用性更高。

状态持久化

用户熄火再启动,希望空调还是上次的温度。这就涉及到状态持久化。

我一般把状态分成两类:

  • 持久状态:需要保存的,比如空调偏好、座椅位置。存到本地存储或云端。
  • 临时状态:不需要保存的,比如当前播放进度、导航路线。熄火就丢。

分清楚这两类,能省不少存储空间和读写时间。

我曾经踩过的坑:把所有状态都持久化了。结果每次启动都要读一堆数据,启动速度慢得离谱。后来只持久化了用户偏好,启动快了 3 倍。

小结

交互逻辑分层、事件驱动、状态管理,这三块是座舱交互的基石。

分层让代码不乱,事件驱动让响应及时,状态管理让数据可靠。三者配合好了,座舱交互才能流畅又稳定。

下一章,我会聊聊「用户动线设计」——怎么让用户的操作路径最短、最自然。到时候见。