《模块化开发》由会员分享,可在线阅读,更多相关《模块化开发(7页珍藏版)》请在金锄头文库上搜索。
1、模块化开发模块化开发 前言本文我们将讨论一下模块化开发。这里提到了两个关键词:模块化和开发。模块化是本文将要讨论的核心。而对于模块化的对象,在这里我们主要针对的是开发,而不单单只是针对编程、设计或者管理等来阐述的,这样描述的主要目的是希望可以通过本文让处于不同岗位的开发人员在工作中的各个方面都能去进行模块化思考。由于笔者一直从业于游戏软件开发领域,所以本文中对模块化所举的例子、说明等,大多都是针对游戏软件开发的。何为模块化开发不知道您之前有没有在网上看过这个视频,视频的内容是一栋 15 层的宾馆大楼如何在 7 天的时间里拔地而起的。这段视频引起了国内外网友们的热议。在这段视频中,我们完整的看到
2、这栋大楼如何一层一层的将架构搭起来,又一步一步的把内外填充好,最终完成整栋建筑。我对建筑行业完全外行,但如果跟我以前看到过的盖楼的方式上比较,从表面上我也看到了一点区别:过去我看到的盖楼过程是一点、一点建起来的,比如一块砖、一块砖的砌,一小块楼板、一小块楼板的灌注。在这个视频中我看到这栋大楼则是一块、一块的建起来的,一大块墙、一大块墙的拼,一大块楼板、一大块楼板的拼。也许这个视频就可以作为一个让人们对模块化产生直接认知的很好的说明示例。(图(图 1. 7 天建起了天建起了 15 层的大楼)层的大楼) 总的来说,模块化并不只是软件开发领域独有的概念,这个概念在很多传统行业中有着更深远的历史,比如
3、建筑业、汽车业、计算机硬件业等等。并且,无论我们有没有仔细想过模块化这个概念,人类自古以来做事的方式一直都是按照这种思路来进行的,只是在不同阶段、不同条件下执行的方式、程度等有所不同。这里是对模块化这个名词的一个基本解释:构建标准化的灵活多用的单元。我想给出这样的解释来说明何为模块化:1. 将大的个体分解成多个小的个体;2. 独立的、闭合的去分析处理这个个体和其与外界的关系;3. 将这些个体组织、集成为一个新的大的个体;4. 持续的、迭代的进行这个过程直至解决问题。什么可以被视为模块模块化的对象和处理单元都是模块,那都有什么可以被视为模块呢?也许这是一个既简单又复杂的问题,说它简单是因为它的概
4、念很好理解,但应该去做到什么程度却又是一件有挑战的事情,如果处理的不是恰当好处就会让事情变得十分复杂而低效。但为了能有个直观的感受,我还是想举几个例子。在软件开发中,一个 C+函数、一个 C+类或者一个功能库都可以被视作一个模块;一款产品、应用或者工具也可以被视作一个模块;再放大并抽象一点说,一个游戏功能、项目管理中的一个用例也可以被视作一个模块;甚至项目中的一个小组、公司中一个部门也可以被视作一个模块。总之,理论上来说任何事情、事物都可以模块化的来看待,但我的忠告是不要做的太过、太抽象。软件开发中的几个示例下面给出几个更具体的示例来帮助我们更好的来体会模块化。从下面这几个示例中,我们可以感受
5、到一个模块化的系统可能具有的一些特性。 基于组件的游戏对象系统基于组件的游戏对象系统我们先来看看“基于组建的游戏对象系统” 一种组织、管理游戏中各种类型对象的技术或者设计。这个设计的大致结构如图图 2 所示。它有什么样的特性呢?每个对象内部都具有比较完整的、通用的功能和属性用于它对自身的表述。对象大部分的行为都是在其内部进行的。对象之间的关系主要是通过消息这个简单的接口建立起来的。即便是直接的相互调用,它所提供的也是标准的、通用的接口,也就是说一个对象可以很容易的跟另一个对象解开关联、再和其它对象关联起来。(图(图 2. 基于组件的游戏对象)基于组件的游戏对象) 并行计算并行计算再让我们看看并
6、行计算。为了提高处理能力,目前的处理器(CPU 或 GPU)都将多核作为其主要的解决方案之一。相应的,在(游戏)软件开发中,程序开发人员也都在努力的应用着这个特性来提高系统的处理速度。但同时多核也意味着要系统尽量去做并行的处理,这将大大提高程序逻辑处理和调试的复杂度。因此,对于并行计算来说,它天然的就对模块化有着很高的需求。例如:将一大批运算拆解为一个个能独立处理的单位,这样它们就可以被交给不同的核心去处理。尽量的减小各个单位之间的依赖或者对资源的共享,使得并行处理的单位之间的执行顺序不影响结果的正确性。尽量的减少或简化各个单位之间通信和交互,来降低逻辑上的复杂度或相互等待。 下面两个图示简要
7、的展示了游戏系统在单核处理和多核处理上的不同。(图(图 3. 在单核上,资源是被顺序的使用的,逻辑也是顺序的执行的。)在单核上,资源是被顺序的使用的,逻辑也是顺序的执行的。)(图(图 4. 多核时,同一个资源则有可能被不同核上的处理任务同时请求,不同处理任务之间也可能有逻辑多核时,同一个资源则有可能被不同核上的处理任务同时请求,不同处理任务之间也可能有逻辑上的依赖。)上的依赖。)Visual Studio 工程工程最后我们看看 Visual Studio 工程。绝大多数项目中的程序开发人员都会通过 IDE 去创建工程来管理项目的代码、属性和构建等,或者至少也会去创建 make 文件来描述项目的
8、构建方式,比如 C+游戏开发人员几乎都会使用 Visual Studio C+工程来管理游戏的 C+代码和库。对于一个大型项目,我们通常会将不同的系统用不同的工程来管理当然,你也可以通过用不同的文件目录来标识和组织不同的模块,但这样做的话那些 IDE 所提供的优秀的辅助管理功能就不能为你服务了。现在我们看看这些由 Visual Studio 工程构成的一个个模块又有哪些特性:每个模块对自己有清楚地描述与定义。各个模块间的关系被明确的建立起来:谁依赖于谁、相关联的配置、包含路径是什么等等。操作(如编译、发布)一个模块的时候,会根据依赖关系先去处理那些被依赖的模块。可以独立的处理每个模块或者所有模
9、块。(图(图 5. Visual Studio Solution 中的项目关系:包含和依赖)中的项目关系:包含和依赖)模块化的原则 现在我们对模块化应该已经有了更清晰地感受了,后面我们就尝试着把这个思想融入到我们的开发工作当中去。在此之前,我们先归纳一下模块化的几个原则。因为模块化是人类固有的一个非常基本的思维方式,所以它的内容描述起来比较简要。先从过程上来说先从过程上来说分解与综合分解与综合。如果一件事情太大、太复杂,那我们就:把它分解成几个小的部分,如果还是太复杂,那就继续分解。一个一个的去解决掉这些相对小的部分。再把这些已经解决的小的部分组织并集成为大的整体。再从特性上来说再从特性上来说
10、低耦合、高内聚低耦合、高内聚。把问题拆开后,对于每个部分:清楚地给出或者限定模块的功能与职责。跟别的模块之间只去建立那些必需的关联。只把那些必要的接口暴露出来。(图(图 6. 一个模块化的系统)一个模块化的系统) 模块化开发的初步实践前面,我们主要是从概念上在讨论模块化开发,下面我们来把这个概念具体化,看看它可以怎样跟我们的实际工作结合起来。在此之前我想再强调一下,模块化绝不仅仅只是针对编程工作的,它也可以在开发流程、项目管理、团队组织等方面进行有效地实践。但因为大多数的读者对程序开发都比较熟悉,所以这里我们就只借用程序开发工作来初步的讨论一下模块化的实践。下面我们分别从内聚与耦合两个方面来说
11、一下软件开发、设计工作中的模块化。内聚内聚一方面,每个模块都应该是尽量的内聚的。那么,一个内聚的模块要有哪些特性?我们如何能生产出这样的一个模块?代码设计代码设计首先从代码本身来说,一个内聚的 C+模块的代码设计要尽量考虑到这些方面:接口、内存、性能、线程、网络、IO 等,尽可能在模块的内部处理好它们。下面我们概要的来说一下每个方面。接口接口在在设计和实现过程中,尽量做到只开放或者说只暴露出来那些真正需要开放的接口,如 C+头文件、类或函数等等,不要将所有内部实现和接口都暴露出来让用户随便使用。并且,尽量的保证接口的稳定。内存内存在在设计时,或者至少在完成系统后,清楚这个系统大概需要使用多少基
12、本内存或者说最小内存,并最好能够在系统的介绍说明中提出来;对外提供一个接口,让这个系统的用户可以通过这个接口指定这个系统最多要用多少内存,最好也可以让用户能把一整块分配好的内存空间提供给这个系统来使用;尽量避免动态的分配、释放内存,这里的动态是指在程序(比如游戏程序)的主要状态的循环中,每帧或者高频率的发生的;考虑一下这个系统需要什么样的数据结构来管理其内存使用;弄清楚这个系统的内存分布情况是什么样的。性能性能在设计这个模块的时候,尽量给出在性能上的分析与预算。例如,在每一次循环(帧)中 ,这个模块大概会用掉多少 CPU 或 GPU 时间(毫秒)?这个模块在不同情况下是如何影响性能的?线程线程
13、这个模块本身是否可以或者需要执行多线程的任务?如果需要就要把这个模块的线程使用规划出来,比如是简单的使用几个就创建出来几个,还是用线程池来管理它们。这个模块是否会在多个线程中被使用?如果会,这个模块的内存、文件等资源就要被设计和实现成线程安全的。网络网络如果这个模块有网络访问,估计好它自身可能需要使用多少网络负载。如果可能接受多个网络连接,考虑一下是否需要使用连接池来管理连接。IO如果这个模块有对文件的读写,设计时就要考虑一下这些方面:如果有数据读取或者载入,还需要其有较高的读取性能,那么这个模块就应该支持序列化的数据载入。如果这个模块要读取流数据,那它最好能对其自身能同时处理的流数据的最大数
14、量有个限定。测试测试现在我们已经让一个模块在尽量独立的处理自己能处理的事情了,之后我们就需要让它能有效地对内、对外保障其自身的正确性与稳定性。如何保障测试。对于程序开发人员,其中一个不错的实践方法就是使用单元测试。当各个模块有了单元测试后,任意一个开发人员修改了某一个模块,他可以马上知道这个模块和与其相关模块是不是仍在很好的工作。当然也可以选择人工的进行这个过程,但如果能将单元测试集成到一个模块的内部作为其一部分,将会给这个模块带来更高、更稳定、更有效的保障。 打包打包现在假设你已经拥有了一个内聚性很好的模块,它也能很好的进行自我保障,然后呢?然后你的这个系统就应该被模块化的提供出去,我们也可
15、以说将它打个包交给使用者。打成包的产品也可以被看作一个独立模块。提供打包的形式可能有:你的这个包是一个底层库,把源代码,文档和编译出来的静态、动态库文件打成一个压缩包,发布出来,让使用者可以从某个地方下载或者拷贝;这个包也可能是一个 C+工程,这样你所提供的就是源代码和工程文件,发布方式就是提交到版本控制系统上,使用你的包的用户会直接把这些代码和工程集成到他的项目中去。至于这个包具体要提供哪些内容、以什么样的形式发布或交付给其他用户,就取决于你的组织、团队、项目、产品、构建方式等各方面因素了。耦合耦合另一方面,各个系统之间的耦合应被尽量的降低。那么,这些耦合是怎么形成的?我们可以怎样降低模块间
16、的耦合呢? 模块间关系模块间关系说到耦合首先要说的就是模块间的关系,正是模块间的各种各样的关系造成了模块之间的耦合。那么模块之间都有可能存在哪些关系呢?依赖关系依赖关系一个模块需要使用另外的一些模块,没有它们这个模块就没办法工作,这就是依赖。对于依赖,在设计代码时就应该确定好你的系统到底要依赖于哪些库,并搞清楚你的这个系统是不是真的需要或者适合依赖于某一个库,这个分析过程会有助于降低你的模块对其它模块的耦合程度。继承(父子)关系继承(父子)关系继承关系主要是面向复用的,子模块具备部分或者全部父模块的特性,在此基础上再进行扩展,这就是继承。例如,一个新的项目开始了,我们从一个老的项目里拿出了整套生产线,在这套老的生产线上进行改进,来生产新产品,这就是一种继承关系。包含关系包含关系包含关系主要是面向组织和行为的,当一个模块做了什么事情,对应的就需要有些子模块的操作被触发或执行,或者要让一些信息从当前模块传递到包含它的模块,这就构成了包含关系。例如,在一个项目中除了主产