Skip to content

系统设计题回答框架

先明确需求边界

先明确需求边界

这句话在面试里可以这样讲:我不会一上来就写代码,而是先确认需求边界,明确这个功能要解决什么问题、不解决什么问题、有哪些性能和平台约束、最后怎么验收。

project-requirement-boundary-clarification

标准回答

做功能前我会先明确需求边界,主要确认四件事:目标范围、非目标范围、约束条件、验收标准。

目标范围是这次必须解决什么问题,比如技能系统要支持主动技能、被动技能、CD、消耗、目标筛选。非目标范围是这次明确不做什么,比如先不做连招编辑器、不做复杂公式编辑器,避免需求无限扩张。

约束条件也要提前问清楚,比如目标平台、低端机性能、资源大小、加载时间、网络环境、是否需要热更新。最后还要有验收标准,比如多少个技能配置能正常运行、是否支持异常配置提示、战斗中是否无 GC、Profiler 指标是否达标。

可直接背的面试话术

NOTE

我做系统前会先确认需求边界,而不是马上写代码。我会问清楚这个功能解决什么问题、哪些场景必须支持、哪些暂时不做、性能和平台有什么限制,以及最终怎么验收。这样后面拆模块、定接口、排期和测试都有依据,也能避免做到一半发现方向错了或者范围不断膨胀。

拆模块

拆模块

拆模块不是把代码拆成很多文件,而是把系统按职责、数据归属、接口边界、生命周期和依赖方向切清楚。好的模块应该高内聚、低耦合,改一个需求时影响范围可控。

project-module-decomposition

标准回答

我拆模块时会先看需求边界,然后找出核心对象和数据归属。比如背包系统里,ItemConfig 是配置,ItemInstance 是运行时数据,InventoryService 负责增删查改,UI 只负责显示和交互,不直接改背包数据。

然后我会按职责拆模块:业务模块负责规则,表现模块负责 UI、动画、特效,基础模块负责事件、对象池、计时器、资源加载。模块之间尽量通过接口、事件或命令通信,不直接互相访问内部字段。

面试里可以这样说

TIP

我拆模块的原则是高内聚、低耦合。先确定这个模块负责什么、不负责什么,再确定它拥有的数据和对外接口。比如任务系统不直接操作 UI,只发任务状态变化事件;UI 监听事件后刷新显示。这样业务规则、表现层、资源加载就不会混在一起。

常见坑

拆得太粗会变成一个 Manager 什么都管,后期很难维护。拆得太细会导致调用链太长,联调成本变高。所以我通常用“改一个需求时影响范围是否可控”来判断模块拆分是否合适。

定义核心数据结构

定义核心数据结构

定义核心数据结构,就是先把系统里最重要的数据对象设计清楚:哪些是配置、哪些是运行时状态、哪些需要索引、哪些需要存档。数据结构定稳了,后面的逻辑才不会乱。

project-core-data-structure-definition

标准回答

我设计系统时,会先定义核心数据结构。比如技能系统里,我会把数据拆成 SkillConfigSkillRuntimeSkillSaveData

SkillConfig 是静态配置,来自配置表或 ScriptableObject,运行时一般只读。 SkillRuntime 是运行时状态,比如当前 CD、是否解锁、等级。 SkillSaveData 是需要持久化的数据,只保存必要状态,并带版本号方便以后兼容。 如果有高频查询,我还会额外建 Dictionary<int, SkillRuntime> 这种索引,避免每次都遍历 List。

示例代码

c
using System.Collections.Generic; // 引入 List<T> 和 Dictionary<TKey, TValue> 所在命名空间

public sealed class SkillConfig // 定义技能静态配置数据
{ // SkillConfig 类体开始
    public int Id; // 技能配置 ID,用来唯一标识一个技能
    public string Name; // 技能名称,通常来自配置表
    public float Cooldown; // 技能冷却时间,单位一般是秒
    public int Cost; // 技能消耗,比如蓝量、怒气或体力
} // SkillConfig 类体结束

public sealed class SkillRuntime // 定义技能运行时状态数据
{ // SkillRuntime 类体开始
    public int ConfigId; // 关联 SkillConfig.Id,表示这个实例属于哪个配置
    public float CooldownLeft; // 当前剩余冷却时间,会在运行时变化
    public bool IsUnlocked; // 当前技能是否已经解锁
} // SkillRuntime 类体结束

public sealed class SkillSaveData // 定义技能存档数据
{ // SkillSaveData 类体开始
    public int Version; // 存档版本号,用于以后做数据兼容
    public List<SkillRuntime> Skills = new List<SkillRuntime>(); // 保存玩家拥有的技能运行时状态
} // SkillSaveData 类体结束

面试里可以这样说

IMPORTANT

我不会把所有字段都塞进一个类里,而是会区分静态配置、运行时状态、查询索引和存档数据。配置数据只读,运行时数据可变,索引可以重建,存档只保存必要状态。这样系统更清晰,也方便热更新、存档兼容和性能优化。

定义流程

定义流程

定义流程,就是把一次操作从“谁触发”到“怎么结束”完整说清楚。它要包含主流程、分支流程、异常流程和结束条件。

project-process-flow-definition

标准回答

我定义流程时,会先画主流程,再补分支和异常。比如释放技能,主流程是:输入触发、条件校验、扣资源、进入 CD、执行技能逻辑、发事件、播放动画和特效、流程结束。

然后我会补失败分支,比如 CD 没好、蓝量不足、目标不合法、角色被控制,这些情况要直接返回并给出提示。最后还要定义结束条件,比如技能释放成功、被打断、取消、超时,都要能进入明确状态,不能卡在半完成状态。

面试里可以这样说

我不会只写成功流程,而是会把入口、校验、执行、通知、表现、收尾都定义清楚。比如技能系统里,输入层只负责触发请求,技能服务负责校验和修改数据,表现层通过事件播放动画和特效。如果校验失败,就返回失败原因;如果技能被打断,就清理锁定状态、停止表现并进入对应状态。这样流程可控,也方便调试和扩展。

常见坑

最常见的问题是只考虑成功路径,没有考虑失败、取消、超时和重复触发。结果项目里就容易出现按钮连点、状态卡死、动画播了但数据没改、数据改了但 UI 没刷新的问题。

处理异常和边界

处理异常和边界

处理异常和边界,就是不要只写“成功流程”,还要提前设计非法输入、状态冲突、资源失败、网络超时、重复操作、极限值这些情况怎么处理。

project-exception-boundary-handling

标准回答

我做系统时会把异常和边界单独列出来处理。首先是输入边界,比如参数为空、ID 不存在、数量为 0、背包已满、重复点击。其次是状态边界,比如角色死亡时不能释放技能、任务已完成不能重复领奖、资源加载过程中不能再次发起同一请求。

然后是异步和网络边界,比如加载超时、请求失败、回调回来时对象已经销毁、弱网下重复返回。对这些情况我会设计明确的处理方式:入口校验、失败提示、状态回滚、按钮解锁、重试机制、降级方案和日志记录。

面试里可以这样说

WARNING

我不会只关注主流程能不能跑通,而是会把异常情况也当成流程的一部分来设计。比如领取奖励时,我会先校验任务状态、背包空间、奖励配置和请求幂等性。如果失败,就返回具体错误码并提示;如果中途异常,就恢复按钮和临时状态;如果是网络请求,还要防止重复领奖,并记录日志方便排查。

常见坑

CAUTION

最常见的问题是只写成功路径:成功时数据改了、UI 刷了、动画播了,但失败时按钮没解锁、状态没回滚、重复请求发了两次奖励。面试里补上这些细节,会比只说“加 try-catch”成熟很多。

讨论性能和扩展

project-performance-extensibility-discussion

讨论性能和扩展

讨论性能和扩展,就是在系统设计最后主动补一句:这个方案现在能不能撑住规模,以后需求变了能不能少改代码。

标准回答

我做系统设计时,会单独讨论性能和扩展。性能方面,我会先看这个系统的高频路径在哪里,比如每帧 Update、ScrollView 刷新、大量怪物 AI、技能命中检测、资源加载等。然后考虑时间复杂度、GC Alloc、内存占用、加载耗时、DrawCall 或网络消息量。

扩展方面,我会看未来最可能变化的地方,比如技能效果类型会变、任务条件会变、奖励类型会变、UI 表现会变。对这些变化点,我会用接口、配置表、事件、策略模式或工厂模式留扩展口,而不是把所有逻辑写死在一个类里。

面试里可以这样说

TIP

我会先保证主流程简单稳定,再看性能热点。比如高频执行的逻辑尽量少分配、少反射、少 LINQ、少全表遍历;大量对象用对象池;大量 UI 用虚拟列表;资源加载做异步和引用计数。扩展上,我会把容易变化的规则配置化,把表现层和业务层解耦,通过事件或接口连接模块。这样新增功能时尽量新增配置或新增策略,而不是大改老代码。

常见坑

CAUTION

不要一上来就过度设计,也不要没测数据就乱优化。低频逻辑可以更重扩展性,高频逻辑要更重性能和简单直接。面试官喜欢听到你能讲取舍:哪些地方值得抽象,哪些地方保持直接实现更好。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0