把章节变成可单步观察的过程
分类:工程设计
先说结论
门面把底层 ECS、存储或网络细节收在稳定 API 后面;泛型 helper 抽取多个入口中完全相同的流程,只把类型和少量差异作为参数。前者控制依赖方向,后者减少重复实现。
两个重复入口怎样合并
SetPlayerTags:验证 ID -> 取组件 -> 清旧值 -> 写新值 -> 发事件
SetMonsterTags:验证 ID -> 取组件 -> 清旧值 -> 写新值 -> 发事件
若五步流程完全相同,只是组件类型不同,可以收敛到 SetTagsInternal<TComponent>。公开层仍保留 SetPlayerTags 和 SetMonsterTags,调用者不需要知道底层查询和组件存储。
核心问题
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:固定流程,传入变化点
例如 AssetTags 和 GrantedTags 的 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、异步、资源句柄、网络状态这类容易出错的东西时,不应该让业务代码到处直接碰。