DYNAMIC EXPLAINER / ARTICLE FLOW

把章节变成可单步观察的过程

01 / 07
STEP 01 / CODE / LOGIC

先说结论

门面把底层 ECS、存储或网络细节收在稳定 API 后面;泛型 helper 抽取多个入口中完全相同的流程,只把类型和少量差异作为参数。前者控制依赖方向,后者减少重复实现。

SetPlayerTags:验证 ID -> 取组件 -> 清旧值 -> 写新值 -> 发事件
SetMonsterTags:验证 ID -> 取组件 -> 清旧值 -> 写新值 -> 发事件

分类:工程设计

先说结论

门面把底层 ECS、存储或网络细节收在稳定 API 后面;泛型 helper 抽取多个入口中完全相同的流程,只把类型和少量差异作为参数。前者控制依赖方向,后者减少重复实现。

两个重复入口怎样合并

SetPlayerTags:验证 ID -> 取组件 -> 清旧值 -> 写新值 -> 发事件
SetMonsterTags:验证 ID -> 取组件 -> 清旧值 -> 写新值 -> 发事件

若五步流程完全相同,只是组件类型不同,可以收敛到 SetTagsInternal<TComponent>。公开层仍保留 SetPlayerTagsSetMonsterTags,调用者不需要知道底层查询和组件存储。

核心问题

GAS / ECS 这类系统内部通常很复杂:

Entity
EntityManager
IComponentData
NativeArray
AddComponent
RemoveComponent
SetComponentData
Dispose

如果业务代码到处直接操作这些东西,会带来很多问题:

UI、技能、配置、战斗逻辑都要懂 ECS
NativeArray 释放容易漏
组件增删时机容易错
调用方写法不统一
底层一改,上层到处跟着改

因此可以在复杂子系统外增加一层门面包装:

外部看到普通 OOP API;
内部怎么操作 ECS,都藏在类里面。

这就是门面模式。

门面模式

门面的设计准则是:

门面无 ECS,内部随便用。

也就是说,public/protected API 不暴露:

Entity
EntityManager
NativeArray
DynamicBuffer
IComponentData

外部只需要调用:

effect.SetDuration(30);
effect.SetAssetTags(new[] { 1001, 1002 });
effect.ApplyTo(target, source);
effect.Remove();

而不需要知道底层是哪个 Entity、哪个 Component、哪个 NativeArray。

为什么 public API 不暴露 ECS 很重要

如果接口返回 NativeArray<int>

public NativeArray<int> GetAssetTags()

调用方就必须理解:

这个 NativeArray 谁释放?
它能不能跨帧保存?
它是临时分配还是 Persistent?

这会把底层复杂性泄露给业务代码。

现在它返回普通数组:

public int[] GetAssetTags()

调用方更安全,理解成本也更低。

所以门面模式的核心价值是:

复杂性可以存在,但要被限制在模块内部。

泛型 helper:固定流程,传入变化点

例如 AssetTagsGrantedTags 的 public API 很相似:

public int[] GetAssetTags() => GetTagsInternal<CEffectAssetTags>(c => c.tags);
public int[] GetGrantedTags() => GetTagsInternal<CEffectGrantedTags>(c => c.tags);

它们流程相同,变化点只有:

组件类型不同
字段访问方式不同
组件创建方式不同

所以底层抽成了泛型 helper:

private int[] GetTagsInternal<T>(Func<T, NativeArray<int>> getter)

private void SetTagsInternal<T>(
    int[] tags,
    Func<T, NativeArray<int>> getter,
    Func<NativeArray<int>, T> factory)

private void RemoveTagComponentInternal<T>(Func<T, NativeArray<int>> getter)

这里的设计思维是:

重复的流程抽成泛型;
变化的部分用函数参数传进来。

SetTagsInternal 的价值

设置 Tag 时,底层要做几件危险操作:

检查 Entity 是否有效
检查组件是否存在
释放旧 NativeArray
添加组件
创建新 NativeArray
写回 ECS

这些操作如果散落在每个 public 方法里,很容易某个方法忘记释放旧数组,导致内存泄露。

集中到 SetTagsInternal 之后:

所有 Tag 设置逻辑都走同一套规则。
危险操作集中管理。
重复代码变少。
行为更一致。

最重要的收获

门面模式负责隔离复杂性。

泛型 helper 负责消灭重复风险。

一句话总结:

好的封装不是为了“看起来高级”,而是为了控制复杂性和风险边界。

尤其当底层涉及 ECS、NativeArray、异步、资源句柄、网络状态这类容易出错的东西时,不应该让业务代码到处直接碰。