以概念为工具,用封装,抽象,解耦和模块化框架重新理解项目工程

21 阅读10分钟

从封装、抽象、解耦和模块化的角度理解静态逻辑组织

本文主要是为了梳理本人对工程项目中逻辑组织方式的理解。

Part1 —— 概念解释

首先需要对封装、解耦、抽象、模块化、静态逻辑组织这些概念做出解释说明。

静态逻辑组织

直观的理解就是一个工程项目中的逻辑框架(架构)。代码是如何分布的,各种功能之间怎么通过一定的沟通协同工作。

这里用一个简单的情景理解静态逻辑组织:

在某个项目中,开发者把所有的代码逻辑写在一个文件中,那么自然可以理解:这个开发者是把所有的逻辑都塞进这个文件中。这一般会导致文件过大,难以调试和维护,可读性差。当这个开发者意识到这个问题,他尝试着把这些逻辑分布到多个文件中,即多文件编程。这样,业务量均摊到了各文件,使得每个文件的业务量会显著减少。进一步的,开发者还可以根据一定的标准对这些文件分类,以加强每类文件之间的功能独立性。

项目中,不仅仅有处理业务逻辑的文件,一般还会用到各种资源文件,这些资源文件可以称为“数据”,这些数据供业务逻辑调用。而这些数据,也有两种方式:要么全塞到一个文件夹,要么分类存储,显然后者对后期的维护更友好。

当项目完成、发布的时候,项目中的所有数据和业务逻辑全部定死,包括各种需求、处理这些需求所用的逻辑和数据的布局。

这些需求所用的逻辑和数据的布局,就是项目中的静态逻辑组织。

封装

封装是项目中常用的一个概念,它容易理解:就是将实现一个或多个功能的逻辑组织形式打包(如函数、库等),并对外提供一个调用该包的接口。在实际工程中,别的开发人员无需再次组织逻辑,以用户的身份直接调用这个接口,通过输入-返回的方式获取处理结果。

可以用“黑箱”模型去理解“封装”。

抽象

这里的抽象是提取多个事物的共性,并提出一个新的概念去容纳这个共性。

比如在C++的面向对象编程中,多个类之间可能存在共通的逻辑,这时,可以用一个父类/抽象基类去容纳这个共通的逻辑。再比如,猫和狗,是具体的事物,我们可以用“动物”这个概念去容纳它们“会呼吸、会动、需要吃东西”等共性。以“动物”作为抽象基类为例,猫狗在满足抽象基类所具有的共同逻辑的情况下,可以存在差异,如叫声的差异。

不仅仅是面向对象,操作系统中也有例子:Linux中的“一切皆文件”思想,它将键盘等外设进程等都抽象为文件

解耦和模块化

简单说就是把混在一块的逻辑功能拆分为多个模块去处理,每个模块单独处理自己的逻辑,模块之间通过某种协议协作完成某个需求所要的逻辑。

例子如下:生物体的层次可以分为细胞、组织、器官、系统。比如要实现“呼吸”需求,是由各个系统之间通过协议协同完成。肺、肌肉、鼻等器官之间通过“血液循环”、“神经信号传递”等载体建立沟通协议,最终组成“呼吸系统”。

Part2 —— 具体应用:一个游戏项目的架构过程

这里先用我之前做的一个项目为例。

仓库链接

完整逻辑在 develop1.1.0 分支中

技术栈:C/C++,EasyX,Windows API

在写这篇文章之前(做该项目时),我并没有想过这些概念。当时是根据需求,通过直觉将逻辑分为 GameProcessUIGameElement 三个大模块。当然,这个直觉并非凭空产生,最初是在学习 EasyX 的时候浏览一个《丝之歌》复刻的项目架构得到启发。

现在在将概念显化之后,重新分析。

背景与构建过程

先简单回忆当时的情景:需求量爆炸,项目期限短,我们将这个项目分成了若干个小阶段,优先完成核心功能,再进而完成拓展功能和优化。

我们可以先抽象最顶端,然后以自顶向下的方式逐步构建这个项目,从架构开始:

首先,根据需求模拟一遍这个游戏(可以参考流程图)。在大致模拟了一遍流程图后,我们可以提炼出这个游戏的核心循环逻辑。不管功能有多复杂,实际上可以抽象为三个游戏大状态(主心跳):菜单、迷宫和战斗。这是游戏的业务逻辑 GameProcess

但是在需求中,不仅仅需要业务逻辑,还要前文提到的“数据”。这些数据,我们可以抽象为一个 GameElement 大目录,里面存放数据变量。

接下来,我们可以细分:GameProcess 中,我们不可能把菜单、迷宫、战斗都放在一个文件/函数中,因为从直观上看,三个最复杂的业务逻辑放在一个文件会非常臃肿。我们最好用多文件的方式将它们分成多个模块,这样在开发期间,还可以以文件为单位(多个函数)的方式分配任务。

因此,我们把 GameProcess 分成了 FightGameHeartGameMapHeart 三个大模块,以及其他 OtherProcess 模块,用来处理杂项逻辑,如商店、人物升级、音乐等模块(即对应主菜单界面除了开始游戏的其他模块按钮,每个模块匹配一个按钮)。

刚刚说到“臃肿”,这其实就是解耦前所有逻辑混在一块。通过解耦和模块化,把这三个逻辑分开实现,彼此之间通过协议协作。而这个项目中,Process 的协议其实就是简单的传参调用。

这样就把主要的架构分好了。GameProcessGameElement 是业务的主逻辑,而且我们把 GameProcess 里面的逻辑主架构也搭好了。

GameElement —— 游戏元素(数据部分)

根据需求文档和流程图,这里划分难度也比较简单:我们可以得知游戏中需要人物、怪物、物品、金币、迷宫中的刺、墙、空地……很多元素,一个个列下来非常复杂。

因此我们需要对数据元素进行抽象和封装:

我将 GameElement 中的各种元素抽象为:

  • BaseElement:游戏中的基础数据/核心数据/抽象数据
  • Items:各种具体物品数据
  • Monster:各种具体的怪物数据
  • Weapon:各种具体的武器数据

显然,核心数据是人物、所有物品抽象的抽象物品类、所有怪物抽象的抽象怪物类。因为物品和怪物的具体形式很多,我们需要一个抽象基类,这些都放到 BaseElement 中。其他目录下可以放具象化的类,如某个具体的怪物或物品。

Weapon 类在本次项目中没有开发,作为需求拓展项。

这样,GameElement 的数据也组织、划分好了。

UI —— 封装图形库接口

但是,在学过 EasyX 后,我们又遇到一个问题:组中一些人没有学过 EasyX,我们迫切需要封装一个接口供他们去使用。因此,我们还需要一个 UI 接口模块,让 EasyX、Windows API 这些组内学习情况不统一的技术单独搭建一个模块。

EasyX 的常见组件中,我们需要搭建按钮、窗口、文字、界面等。我给 UI 分成了如下的目录:

  • Animation:这里本用来展示人物的动画,但是时间过于紧张,最后没有用上,未测试
  • Button:这里用于封装窗口中的按钮模块
  • Music:封装音乐模块,用于在不同场景下的音乐播放
  • Interface:一份标准的交互界面模块模板,项目成品中在主菜单中使用

这样,最后的 UI 目录也给分好了。

小结

这里我先模拟流程图,抽象出项目的主心跳(游戏中标准且是最核心的东西),然后一步步往下拓展,最后分好整个文件结构。途中需要对抽象、封装、解耦和模块化灵活运用。

接下来就是每个文件夹中文件的创建,然后在最外层创建 main.cpp 作为程序的入口,就把这个主文件架构搭好了。随后就是以函数为单位的代码架构,每个函数需要指明负责人、函数参数、返回值、主要逻辑,这里内容过多,不展开了。

到此为止,我们就把整个项目的业务逻辑部分给架构好了。除了业务逻辑,我们还需要一份数据给业务逻辑调用,也就是 resource 目录,存放一些贴图、音乐、地图文件、存档文件等。这里的架构方式和前面类似。

这里也可以参考 UE5 游戏项目的默认格式,将游戏资源放在 asset 目录下。

最后,我们成功搭建这个项目的架构,在沟通后即可开发。


Part3 —— 对 Docker 和 Redis 中的简单分析

最近学习 Docker 和 Redis 后,我觉得可以尝试用这个框架去分析这两个技术工具。

Docker

需求:解决环境配置的复杂性。以前开发中可能会出现环境不一致的问题,这需要解决。

Docker 把运行环境抽象为镜像,每个镜像可以当成 mini 版的正常环境。镜像中封装了系统库、代码等,用户只用知道这个镜像可以跑什么服务。

解耦和模块化:Docker 将宿主机和开发环境之间的逻辑分成两个模块,中间的协议是将自己充当虚拟平台,建立宿主机和容器的沟通(中间件)。

Redis

需求:传统数据库磁盘 I/O 速度慢,处理高频访问比较困难。

Redis 内部封装复杂的数据结构,开发人员无需知道底层怎么实现,只需知道如何使用。

这里的解决方式是优化,较少涉及构建静态逻辑组织。Redis 把原本访问磁盘改成了访问内存,持久化单独解耦出来做一个模块,完成优化。

中间件抽象

再进一步,我们可以抽象出:Redis 和 Docker 本质上是中间件的角色,成为虚拟平台。

宿主机 ---- Docker ---- 容器---- Redis ---- 内存/磁盘
总结

本文用抽象、封装、解耦和模块化四个概念,梳理了开发项目中的静态逻辑组织,并用一个之前的游戏项目做了回顾和应用,最后用这套框架对 Docker 和 Redis 进行简单分析。

本文只梳理了抽象、封装、解耦和模块化在项目组织和常用工具中的体现。在计算机专业基础课中,这套框架同样也有影子——比如计算机网络用分层模型解耦各层协议,文件系统把磁盘块封装成用户看到的多级目录等

当然,在算法/性能优化,安全,可靠性这些方向上这些概念可能涉及不多