【软件开发】代码设计指南

本文最后更新于 2026年6月21日 凌晨

【软件开发】代码设计指南

写代码最费时间的是查 API,最难的则是设计代码结构。如今,查 API 已经可以被 AI 替代,但代码设计始终需要手工操作。这非常难,难到大部分人类都无法实现这一点。

检查代码设计是否有误

是否存在数量不定的条件判断

检查代码是否需要对实现进行抽象

如果你的代码实现里用到很多if、switch之类的判断,且你无法确定他们的最大数量,可以预见性的,他们未来还会继续增加。那你不应该使用条件分支实现(违反开闭原则),而是改用委托、抽象类等(依赖倒转原则),由外部调用者实现原本的条件分支代码。

1
2
3
4
5
6
7
8
//错误写法
if(是路线导览)
...
else if(是区域导览)
...
else if(是自由导览)
...
...

是否存在结果恒定的条件判断

检查代码是否存在多余的实现耦合

一段代码被分支,但部分场景下又完全不执行,说明该实现在某些功能中需要,但在其他的一些功能中则完全多余。这意味着代码没有实现单一职责、接口隔离等原则,存在功能耦合。你应该将其进一步拆分,确保功能中的每段代码都是必要代码。

1
2
3
4
5
6
7
8
//错误写法
bool 需要导游;
if(是路线导览)
需要导游 = true;
if(是区域导览)
需要导游 = true;
if(是自由导览)
需要导游 = false;

是否存在复制粘贴的编码方式

检查代码是否需要对功能封装复用

如果你写代码时出现了成段的复制粘贴行为,那说明你的代码需要优化。你应该将你复制的代码进行封装,考虑改用可以共用的函数实现,这样后期修改才不会出现“一次调整,到处要改”的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//错误写法
游戏开始:
...
控制数字人介绍
...
路线导览:
...
控制数字人导航到固定地点
...
区域导览:
...
控制数字人导航到固定地点
控制数字人介绍
...

是否无法轻易迁移单个功能

检查多个功能间的依赖关系是否正常

当有需求让你把项目中某一小功能快速迁移到其他项目,你却无法做到或得扯上一堆毫不相干的其他功能,那你的代码就存在依赖问题。最佳的功能依赖应该是单向树状的,且有明显的层级隔离(最少知道原则的体现),一个枝杈不可能影响到另一个枝杈。所以你应该确保你的项目内任意单一功能都能随时迁移。

1
2
3
4
5
6
上面的代码例子就无法实现模块化迁移。
例如如果要迁移:区域导览功能。
1. 首先不同模式是用分支实现,所有代码都放在一起,本身就不能单脚本迁出。
2. 由于对 `bool 需要导游` 的依赖,所以必须依赖主流程脚本环境。
3. 主流程中还存在对`路线导览`、`自由导览`的代码引用,所以也必须带上这些无关功能。
结果就是一个功能都迁不出来,所有代码全耦合成了一团。

正确的设计思路:

画板

学习良好的代码设计理念

基本原则:通过“高内聚低耦合”实现“模块化”

很多人把自己的功能称为模块,但事实上他们并没有实现模块化。“模块化”在《软件工程》中有专门的定义,仅逻辑上的功能独立并不能说明你的代码是模块化的,只有做到真正的随拆随删,才能叫模块化。

无论多复杂的编码技巧,本质都是为了实现“模块化”,实现“高内聚低耦合”。

指导思想:设计模式

市面上有很多类似的技巧和规范教我们如何高效管理项目,例如《设计模式》就是其中一种:

【软件开发】设计模式个人解读

开发框架:MVP与DDD

从整体关系上设计软件

MVP框架(局部视角)

如果你的代码实现了模块化,那必然就会自动使用上 MVP 框架,因为 MVP 就代表了三个不同的模块:

  • M(模型):核心功能,类似于 API 的存在。
  • V(视图):用户界面,与 M 分离,用于向用户呈现特定的数据交互形式。
  • P(主持人):驱动游戏逻辑,将各个模块(M、C)桥接,激活。

而且 Unity 天生对 MVP 有非常好的适配性,因为其自带编辑器界面(V)和游戏生命周期(P),使得可以轻松分离出功能核心部分(M)实现快速分开发。

当然,为了偷懒,有时会对 MVP 简化,例如改为 MV-P、M-PV。本质都是对 V 的退化实现(不过这种不利于自动化测试,因为V只能人工测试),但 M 和 P 一定是分离的,因为一个应用说到底就是由一堆功能模块和驱动模块构成的。

补充:

MVP 目前进一步发展到 MVVM,其中 VM (视图模型)是借助数据绑定技术,对原本 V 和 P 的进一步解耦,也是现在主流的前端框架。目前 Unity 的新 UI 系统 UIElements 已实现对绑定技术的支持,但该 UI 系统本身还无法替代 UGUI,UGUI 只能实现 V 到 P 的单向数据流,所以任需要 P 去控制 V。

DDD框架(宏观视角)

MVP 解决的是单个模块或少数模块间的设计方法,但从整个项目几十上百的模块视角来看,则需要利用 DDD 框架(领域驱动设计)。DDD 框架的思想简单来讲就是分层与隔离,相比传统分层它还进一步将业务实现分为“应用层”和“领域层”:

  • Interface(交互层):实现 UI 功能,负责实际功能的输入输出部分。
  • Application(应用层):根据软件最终的实际需要,对业务功能的组合使用。
  • Domain(领域层):基于业务需求,针对特定领域的一套通用功能实现。
  • Infrastructure(基础层):实现基础功能,例如后台 API、存储,等跨项目的通用功能服务。

所有功能模块被细分为多层,依赖上仅允许上层调用下层,且不允许跨层调用。这种分层思想在很多大型软件设施中都有体现,例如 OSI参考模型、来自GAMES104的现代游戏引擎架构分层

当一个软件庞大到一定程度,没有人能记住它的所有功能实现,再加上人员变动等,很快项目就会变成黑盒代码,而分层使得功能细节可以被隔离。从协作角度看,人员分配调度会变的更加方便;从个人角度看,上手门槛降低,实现时不用在意旧功能的细节,开发新功能压力小,旧代码也更安全。

实现细节:函数化

从具体实现上设计软件

我认为世界上的任何现象,都可以用函数进行归纳,这和人们试图用数学抽象现实的想法是一致的。同理,模块的实现也可以参考函数的实现。函数的组成非常简单,只有“输入”、“输出”、“实现”三个部分组成。

单模块实现(实现函数)

先不要关心其他模块,只关心当前模块本身的功能,其函数三核心在代码规范中的区域分布中就能体现出来。而且由于 Unity 本身的特性,一个 MonoBehaviour 自身就可以完成一套完整的 MVP 架构,因此它确实是一个独立且功能完整的存在,确实能被称为是一个模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static class AccountAPI
{
public static AccountInfo GetAccountInfo(string account,string passward);
}

// 提供账号信息服务
public class AccountSystem : MonoBehaviour
{
//输出服务(M 对外服务)
public AccountInfo AccountInfo {get; private set;}
public void SetAccountAndPassward(string account,string passward)
{
this.account = account;
this.passward = passward;
}

//输入依赖(V 用户界面)
[SerializeField] string account;
[serializeField] string passward;

//实现功能(P 驱动实现)
void Awake()
{
AccountInfo = AccountAPI.GetAccountInfo(account,passward);
}
}

多模块依赖(调用函数)

函数化的思想适用于每个模块,包括模块间的依赖调用和控制调用。整个模块使用就如同函数一样,填入参数,执行,取出参数,再将参数填入另一个函数,执行,直到输出到用户界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//实现对游戏流程的推动
[ExecutionDefaultOrder(-100)]// 确保在模块前运行,以实现完全控制
public class GameController : MonoBehaviour
{
//输出服务(控制器是顶层设施,不需要提供任何服务)

//输入依赖
[serializeField] AccountSystem accountSystem;
[serializeField] ConfigSystem configSystem;
[serializeField] AccountUI accountUI;

//具体实现
void Awake()
{
//取消自运行,完全由控制器管理模块生命周期
accountSystem.gameObject.SetActive(false);
configSystem.gameObject.SetActive(false);

//执行依赖模块,使用其提供的输出
configSystem.gameObject.SetActive(true);
string account = configSystem.GetAccount();
string password = configSystem.GetPassword();

//将输出作为另一个模块的输入并执行
accountSystem.SetAccountAndPassward(account, password);
accountSystem.gameObject.SetActive(true);

//继续传递参数,直到输出到用户界面
accountUI.AccountInfo = accountSystem.AccountInfo;
}
}

优势分析

  1. 函数化的思考方式非常简单,对外只用考虑本身功能的输入输出,对外细节完全封装,自动形成层级隔离。
  2. 实现完全基于 MonoBehaviour 的原生设计,没有任何学习成本,理解运用非常轻松。
  3. 主动暴露输入条件于面板,使得用户可以借助编辑器自由调节参数,并且前置依赖一眼便知。
  4. 支持基于 Unity 事件的自动激活,在前期快速开发和单元测试场景中非常方便。
  5. 模块内代码分区明了,输出放顶,细节放尾,实现了代码文档化,查阅起来非常方便。

依赖注入

https://github.com/modesttree/Zenject

https://docs.unity3d.com/Packages/com.unity.dt.app-ui@2.0/manual/mvvm-di.html

没有框架是最好的框架

就在刚刚的实现细节中,其实我们已经实现了一系列 MVC、模块化、封装分层的思想。这是很难的操作吗?在我看来,用 Unity 面板编辑参数,用 Unity 事件执行功能,这明明就是 Unity 最基本的用法。

实际上,在写该文章前,我不知道什么是 MVP(我一直以为我用的是MVC),也不知道什么是 DDD,设计方法我也是懒得看的那种。直到现在,我才了解到,原来这些对我来说很自然的代码实现,原来是有名称的。

这种后知后觉的现象很正常,因为不是框架让你的代码变好,而是你为了让代码变好,自然的写出了框架。实际上写代码只要遵循以下原则,那写出来的代码一定是最优代码:

  • 不要没事找事:写代码永远只把注意力放在当下的事,不要妄加揣测尚未提及的需求,也不要试图定制一个完美的框架。(如果你经验不足,你的小巧思,通常都会成为废物甚至拖慢项目的枷锁,不如只把当前的事做好)
  • 不要瞻前顾后:写一个模块就只关注这个模块要做的事,不要同时考虑多个模块,那样写出来的全是耦合代码。模块在之后可以扩展,可以按需提供更多服务,但它本身一定是独立的,绝不是为了特定于为某物而写的。
  • 遵循设计原则:检查你的代码是否符合设计模式中的原则要求,以及上文的代码问题检查,如果违反,那一定是代码存在问题,必须要重新设计你的实现。如果你不知道如何修改,那么你就可以进一步参考设计模式中的设计方法。
  • 及时修改代码:没有人是先知,无论是开发者还是项目经理,需求总是会变的,代码结构总是会改的,所以要以不变应万变。代码调整很正常,但只要做好了隔离,再大的火都只会烧在一个模块内,然后根据需求随时调整你的代码,确保每次迭代都始终遵守原则,不要偷懒,否则它将成为屎山的开始。

很多时候开发和项目经理会存在需求上的矛盾,但对我来说项目经理的需求我一向都会接受,我甚至会感觉他们思考太多。对他们来说可能是为了开发好,但当他们主动试图去理解开发的工作内容,来决定成品的效果时,我只感觉到了本末倒置,感觉到了单一职责原则的违背。

我从不怕提需求,也从来没有过大改过代码结构,在我看来适用需求本身就是软件开发的一项职责。而这一强大适应性的背后,我却从来不写什么 Demo 版,也从来不设计什么全局框架,我唯一做的事,就是“接到需求,实现功能、调整复用”,以此往复。

“没有框架是最好的框架”:我的项目没有框架,又或者说到处都是框架,每天都在变化。以不变应万变,这才是一个真正优秀的软件框架。


【软件开发】代码设计指南
https://bdffzi-blog.pages.dev/posts/3849233909.html
作者
BDFFZI
发布于
2026年4月13日
许可协议