技术分享-插件化架构

插件化架构:cocos组件化和生命周期的原理

叠甲

​ (护盾 + 999)本帖是技术分享,旨在为新手提供一些参考,以及和同好们交流。如果您是大佬,也欢迎斧正错误,感谢大家。

引言

​ 你是否会在完成复杂需求的时候一场酣畅淋漓之后发现代码行数已经爆炸,各种状态方法以及隐式依赖关系让人抓狂。

​ 你是否会在代码写着写着,发现后期要加一个新功能难如登天,不仅要先读懂之前为了业务妥协堆积的代码,还要大刀阔斧的改革,改完发现之前能跑的代码又不能跑了,不禁想起一句古人云:“代码能跑就不要动,你和代码有一个能跑就行。 ”

​ 你是否会在写完一次代码后,下次拿到同样需求时翻到以前的脚本发现有太多的耦合导致没法直接用,要改一通才能用,甚至出现改不如重新写一个简单。

​ 那么到底为什么我们的代码没能陪我们走到最后,甚至只是一次性的面条代码呢。

归因

​ 出现之前那些令人唏嘘的结果皆来自于一下原因:

​ 出现这些问题,根源主要有三点:

  1. 职责不清晰,一个类承担太多功能,耦合成庞大难以维护的块。
  2. 没有预留扩展点,新增功能必须修改原有代码。
  3. 模块无法独立拆分,要么全用,要么重写,不能灵活插拔。

​ 那么有没有什么代码结构可以在面对复杂需求,以及未来不确定的改动时都能够保持相对简洁的代码复杂度,和良好的扩展性,甚至轻易去掉现在不再需要的功能。

​ 为什么cocos可以凭借生命周期让开发者充分发挥自己的想象力,为什么组件可安装可卸载的设计让我们的代码可以征服各种需求,他们的背后原理到底是什么呢。

​ 是的我们说的是随插随用,可以安装功能,按需求组合多种功能,不需要时也可以卸载功能的插件化架构。

代码演示

import { _decorator, Component, Node } from 'cc';
import { Kernel } from './Kernel';
import { MovePlugin } from './plugins/MovePlugin';
const { ccclass, property } = _decorator;

@ccclass('Demo')
export class Demo extends Component {
    protected onEnable(): void {
        const kernel = new Kernel()//创造一个内核
        const movePlugin = new MovePlugin()//创造一个移动插件
        const attackPlugin = new AttackPlugin()//创造一个攻击插件
        
        kernel.use('move', movePlugin)//内核安装移动插件
        kernel.use('attack', attackPlugin)//安装攻击插件
        
        kernel.onEnable()//内核启用
        kernel.unuse('attack')//卸载攻击插件
    }
}

​ 上面的类定义相信大家都不陌生,是的,他们是我们天天写的组件还有生命周期,我创建了一个名为Demo的组件类,而onEnbale内部我们进行了一系列操作,包括创建内核,安装插件,启动内核,卸载插件等,就好像我们拼好了一个机器人,为他装上了双腿和双手,启动它。

​ 我在这些插件内打印日志,代替了他们可能出现的具体行为,以下是具体日志。

Cocos Creator v3.8.8
debug.ts:66 Using custom pipeline: Builtin
game.ts:905 Init Project: 196.264892578125 ms
MovePlugin.ts:5 MovePlugin 安装
AttackPlugin.ts:5 AttckPlugin 安装
MovePlugin.ts:11 MovePlugin 启用
AttackPlugin.ts:11 AttckPlugin 启用
AttackPlugin.ts:14 AttckPlugin 禁用
AttackPlugin.ts:8 AttckPlugin 卸载

​ 可以看到我们的插件全都正常安装启动,然后攻击插件因为指令被禁用卸载。没错,我们凭借简单的几行代码就可以组合不同的功能,他们会在生命周期中执行自己的行为,是不是就像我们在引擎上写的组件,挂上就有了神奇的效果,不想要了拿下来就可以。从表现上看,可以安装和卸载插件的能力就称作插件化,以下是完整的插件还有内核代码,以及demo仓库地址。

export interface IPlugin {
    enabled?: boolean
    onUse?(IKernel)
    onUnuse?(IKernel)
    onEnable?(IKernel)
    onDisable?(IKernel)
}

export interface IKernel {
    has(name: string)
    get(name: string): IPlugin | undefined
}

export class Kernel {
    private enabled: boolean = false; //表示内核处于启用还是禁用状态
    private plugins: Map<string, IPlugin> = new Map()//¥核心结构¥ 用map配合字符串存取插件
    private valids: IPlugin[] = []//有效状态插件的缓存列表
    // 只有安装了且启用的插件才会存在这里,方便遍历

    /**
     * 更新有效插件的缓存--对应上面的valids数组
     */
    public updateValidPlugins() {
        //把插件Map转成数组,再过滤掉被禁用的插件
        this.valids = Array.from(this.plugins.values()).filter(plug => plug.enabled)
    }

    /**安装插件 */
    public use(name: string, plugin: IPlugin) {
        if (this.has(name)) {
            console.warn(`Kernel 重复注册 name=${name} 已存在`);
            return
        }
        //把插件按名字做键存进map,然后调用插件的安装,启用生命周期
        //如果内核未启用就等启用时一起调用
        this.plugins.set(name, plugin)
        plugin.onUse?.(this)
        if (this.enabled) {
            plugin.enabled = true
            plugin.onEnable?.(this)
        }
        //新增插件 更新有效插件的缓存
        this.updateValidPlugins()
    }

    /**卸载插件 */
    public unuse(name: string) {
        if (!this.has(name)) {
            console.warn(`Kernel name=${name} 不存在`);
            return
        }
        //从Map中取出名字对应插件,设置插件内部属性为禁用,
        // 然后调用他的禁用卸载生命周期,并从map移除
        const plugin = this.plugins.get(name)
        plugin.enabled = false
        plugin.onDisable?.(this)
        plugin.onUnuse?.(this)
        this.plugins.delete(name)
        this.updateValidPlugins()//卸载插件 更新有效插件的缓存
    }

    /**检查键名对应的插件是否存在 */
    public has(name: string) {
        return this.plugins.has(name)
    }

    /**获取键名对应的插件 *///是的 Component组件可以获取他的兄弟组件们,我们也可以
    public get(name: string): IPlugin | undefined {
        return this.plugins.get(name)
    }

    /**启用内核 */
    public onEnable() {
        //更新内核状态为启用
        this.enabled = true
        //把Map转成数组,然后插件逐个按安装先后顺序启用
        Array.from(this.plugins.values()).forEach(plugin => {
            plugin.enabled = true
            plugin.onEnable?.(this)
        })
        //所有插件启用了,这个时候应该更新有效插件的缓存
        this.updateValidPlugins()
    }

    /**禁用内核 */
    public onDisable() {
        this.enabled = false
        this.valids.forEach(plugin => {
            plugin.enabled = false
            plugin.onDisable?.(this)
        })
        this.updateValidPlugins()//所有插件禁用了 更新有效插件的缓存
    }


}
import { IPlugin } from "../Kernel";
/**移动插件*/
export class MovePlugin implements IPlugin {
    onUse() {
        console.log(`MovePlugin 安装`)
    }
    onUnuse() {
        console.log(`MovePlugin 卸载`)
    }
    onEnable() {
        console.log(`MovePlugin 启用`)
    }
    onDisable() {
        console.log(`MovePlugin 禁用`)
    }
}
import { IPlugin } from "../Kernel";
/**攻击插件*/
export class AttackPlugin implements IPlugin {
    onUse() {
        console.log(`AttckPlugin 安装`)
    }
    onUnuse() {
        console.log(`AttckPlugin 卸载`)
    }
    onEnable() {
        console.log(`AttckPlugin 启用`)
    }
    onDisable() {
        console.log(`AttckPlugin 禁用`)
    }
}

插件化Demo: 插件化架构的演示demo

总结

​ 插件化架构可以兼具职责分离,同时随插随用的特点是因为这个设计把每个插件负责什么,同时只把整个业务流程中几乎最不可能变的一些流程,俗称生命周期。写死,作为约定,插件负责实现生命周期里的内容,这样就完成了一个流畅清晰的业务流程代码。插件化架构并非神药,而是他最遵守关注点分离,按维度设计功能,除了插件调度和生命周期,不做过多设计,把功能设计的选择权交给未来。事实上插件化架构对于不复杂的小功能来说可能反而是累赘,而只要对业务进行合理的模块化划分,无论用什么方式,都能实现不错的效果。

发帖重复了

插件化架构:cocos组件化和生命周期的原理 原帖

有点像 ECS 的感觉。

是的,本质上都是关注点分离,不过我这个没有数据和逻辑分离