跳转至

Assembly Definition 与依赖边界

前言

刚开始写项目时,所有脚本都放在 Assembly-CSharp 里。改一个 UI 类,Unity 会把整个工程重新编译一遍。后来拆了 .asmdef,编译确实快了一点,但 Common 程序集开始变大,战斗、UI、网络和编辑器工具都往里塞,最后又绕回去了。

所以拆程序集本身不难,难的是拆完以后,依赖方向能不能一直保持清楚。1

先定依赖方向

一个比较稳妥的分层如下:

Game.Domain          规则和数据结构,不引用 UnityEngine
Game.Application     用例编排,依赖 Domain
Game.Infrastructure  存档、网络、资源加载,依赖 Application
Game.Presentation    MonoBehaviour、UI、动画表现,依赖 Application
Game.Bootstrap       启动入口,依赖具体实现

依赖只能往下走。Domain 不应该知道 Presentation 里有没有按钮,Application 也不应该直接 FindObjectOfType。Bootstrap 是唯一允许同时看到接口和实现的地方。

在 Game.Domain.asmdef 里,引用列表保持为空:

{
  "name": "Game.Domain",
  "rootNamespace": "Game.Domain",
  "references": [],
  "autoReferenced": false
}

autoReferenced 设为 false 后,默认的 Assembly-CSharp 就看不见这个程序集,所有代码都必须显式放进对应的 asmdef。这样做一开始会多一些配置,但能避免项目里同时存在两套编译边界。

用组合根创建对象

业务类不负责查找依赖,创建过程集中在入口:

public sealed class GameBootstrap : MonoBehaviour
{
    private CombatService _combat;

    private void Awake()
    {
        var clock = new UnityClock();
        var rules = new DefaultCombatRules();
        _combat = new CombatService(clock, rules);
    }
}

CombatService 是普通的 C# 类,构造函数直接接收接口:

public sealed class CombatService
{
    private readonly IClock _clock;
    private readonly ICombatRules _rules;

    public CombatService(IClock clock, ICombatRules rules)
    {
        _clock = clock;
        _rules = rules;
    }

    public DamageResult Apply(Attack attack, Combatant target)
    {
        return _rules.Resolve(
            attack with { Timestamp = _clock.NowSeconds },
            target);
    }
}

这样测试时不用启动场景,换一个假的 IClock 就能验证时间相关逻辑。使用 VContainer、Zenject 这类容器也可以,但容器应该只在启动入口组装对象,不要让业务代码到处 Resolve。

编辑器代码单独放

编辑器工具很容易把 UnityEditor 引用带进运行时程序集。比较省事的做法是单独建 Game.Editor.asmdef:

{
  "name": "Game.Editor",
  "references": [
    "Game.Domain",
    "Game.Application"
  ],
  "includePlatforms": [
    "Editor"
  ]
}

这样打包时不会把编辑器脚本带进去,运行时程序集也无法反向引用编辑器代码。

几个容易踩的点

  • 不要因为两个程序集都需要一个类型,就马上新建 Common。先看这个类型属于谁的契约,能不能放到更下层。
  • 循环依赖不能靠“再拆一个程序集”解决。肯定是有一边的职责放错了,先找反向引用的原因。
  • 先拆稳定边界,不要把每个文件夹都做成 asmdef。程序集太多会拖慢编译,也会让引用配置变得难维护。
  • 修改 asmdef 后重新看一次依赖图。编译通过只说明类型能对上,不代表方向是对的。

拆分的时机不用太早。原型阶段可以先不拆;当项目出现多人并行开发、平台差异实现或独立发布需求中的两项,再开始整理边界比较划算。