??xml version="1.0" encoding="utf-8" standalone="yes"?> 因ؓ(f)之前一直处在游戏开发行业,׃U种原因一直对软g工程中的目理、项目开发方法缺乏体验。虽焉目中也曾倡导~写更多的文档,无论是模块说明文还是设计文档,但效果一直不好。不甚理想的地方主要体现在文的规范性欠~、不l一、Q于表面没有实质内宏V文的~写~Z详尽的方法指|那么所谓的设计文要么是用来敷衍上U要么就是随着开发h员的水^不一而千差万别?/p> 当我开始目前这个非游戏目Ӟ我也曾想Q前期做好结构设计,制定好关键问题的解决Ҏ(gu)Q那么要完成q个目׃在话下了(jin)。但是我很快面临了(jin)一个问题:(x)需求不定。回惌n处游戏公司的那些日子Q程序员L抱怨策划需求变更过快过多,在每一ơ策划提Z个需求变更时Q}慎的E序员都?x)再三让{划保证Q放?j),不?x)变(sh)(jin)。而我面(f)的问题则更ؓ(f)严峻。我意识刎ͼ目的需求,p用户也无法一一|列出来。我们需要的是需求调研。但q你将客户的所有需求全部挖掘出来后Q这几乎不可能,因ؓ(f)他们自己也不太清楚自己想要什么)(j)Q当你交付(sh)(jin)W一个Y件版本,几乎可以肯定客户?x)提Z大堆的需求变_(d)(x)我要的不是这个,我要的那个怎么没有Q哦Q我当初以ؓ(f)你说的是另一个意思?/p> 当然Q需求调研这U工作不是让E序员去做的Q那?x)更?zhn)剧Q无论是对客戯是对E序员而言Q他们都是在对牛弹琴Q。但需求的不确定性也L存在的?/p> 事实上,需求变化本w就是一个很正常的现象。我一向愿意更(zhn)观地处理Y件开发方面的问题Q所谓小?j)得万q船。基于此Q我军_摆好?j)态学学Y件开发的Ҏ(gu)学?/p> 本文要描q、ȝ?jin)RUP开发方法学的主要内容,l合我自q感受阐述?jin)一些RUP的核?j)原则。我怿我所理解的内Ҏ(gu)肤浅的,对于非代码的表达我更怿其是存在歧义的。所以本文仅当是一U经验参考,不必当真?/p> RUP据传是用于指导大型甚臌大型目开发的Q我们做的不是这栯模的目。但是我们需要记录下整个目的开发过E,通过q个q程中出的工gM一个h可以看出q个目是如何实现出来的Q其目的在于规避唯有从v量代码中才能熟?zhn)目实现q种问题。这里出C(jin)一个概念:(x)工gQ其指的是Y仉目开发过E中M留下记录的事物,例如文、图、代码等?strong>RUP的一个重要思想Q在于其整个软g开发过E都是可推导?/strong>。例如我们通常说的软g架构Q或一点的模块l构Q都是通过开发过E中前面阶段产出的工件推导得出,?strong>不是凭借程序员的经验拍脑袋惛_来的Q经验不太可靠,q且千差万别Q?strong>推导意味着每个环节变得可靠)(j)。我们借助RUP的这个特性,创徏q些工gQ用以徏立v软g实现的可靠知识库?/p> 以下均摘?lt;Thinking in UML>中对RUP的描qͼ(x) l一q程归纳和集成了(jin)软g开发活动中的最?jng)_践,它定义了(jin)软g开发过E中最重要的阶D和工作Q四个阶D和?ji)个核?j)工作)(j)Q定义了(jin)参与软g开发过E的各种角色和他们的职责Q还定义?jin)Y件生产过E中产生的工Ӟq提供了(jin)模板。最后,采用演进式Y件生命周期(q代Q将工作、角色和工g串在一P形成?jin)统一q程?/p> l一q程是一U追求稳定的软gҎ(gu)Q它q求开发稳定架构,控制变更 l一q程集成?jin)面向对象方法、UML语言、核?j)工作流、工件模板和q程指导{知?/p> 单来_(d)RUP作ؓ(f)一UY仉目开发方法学Q?strong>它定义了(jin)软g开发的每一个过E,最重要的是它指g(jin)在每一个过E需要Z么,q些产出又是怎样得到。它试图规范化整个流E,以规遉K求变_(d)目参与者水q不一{带来的目不可控等问题Q以期一个Y件品稳定地开发出?/strong>。在一个项目开发过E中Q最核心(j)的资源是人,最不可控的亦是人?/p> 我觉得要快速学?fn)一U知识,需要首先获得这门知识的M框架。另一斚wQ在我们获得更多信息后,我们需要挖掘出q门知识的核?j)思想。学?fn)RUP我觉得从q两斚w入手是相Ҏ(gu)较快速和有效的手Dc(din)?/p> RUP定义?jin)Y件开发过E的四个阶段Q以?个工作流E?/strong>。一张极为经典的RUP开发过E框架图如下Q?/p>
RUP整个Y件开发过E分为四个阶D:(x) 每一个阶D늚工作分ؓ(f)9个流E:(x) 其中Q前6个流E被l称?#8221;engineering disciplines”Q后3个流E被UCؓ(f)”supporting disciplines”。当?dng)我们主要x(chng)?个流E。那么,q些工作程和开发阶D又有什么关pdQ上图中其实已经阐明?jin)这些关pR?/p> RUP指导q代开发。在软g开发的q?个阶D中Q每一个阶D会(x)被分q次q代。而每一ơP代则늛?jin)?个工作流E。随着开发阶D向产品化靠q,自然而然圎ͼ需求的变更、增加自然会(x)减少Q所以从图中可以看出Q开发过E越到后期,其工作流E中关于需求的工作则越。同P在先启阶D,光求相关的工作则占据了(jin)该阶D늚主要工作内容?/p>
RUP中的q代要求在每一ơP代中Q都?x)完整地实施一遍整个工作流E。在软g实施阶段Q甚至会(x)在每一个P代过E完后输Z个可q行的Y件版本。这个版本可能会(x)被交付给客户Q以期进一步地在功能需求上取得与客户一致的意见。这倒是同敏捷开发有点类伹{?/p> 既然制定?jin)工作流E,那每一个工作流E该如何d施呢Q?strong>RUP制定?jin)每个工作流E需要参与的角色Q这些角色需要从事的zdQ以?qing)这些活动生的工g?/strong> q句话实际上反映?jin)RUP的一个重要信息,摘自wikiQ?/p> RUP is based on a set of building blocks, or content elements, describing what is to be produced, the necessary skills required and the step-by-step explanation describing how specific development goals are to be achieved. The main building blocks, or content elements, are the following: 在我看来QRUP每个工作程所完成的工作,是一个徏模的q程。所谓徏模,单来说就是将需要描q的事物通过更系l的形式表达出来Q以期获得对该事物更深入的理解?lt;Thinking in UML>中定义徏模概念ؓ(f)Q?/p>
建模(modeling)Q是指通过对客观事物徏立一U抽象的Ҏ(gu)用以表征事物q获得对事物本n的理解,同时把这U理解概念化Q将q些逻辑概念l织hQ构成一U对所观察的对象的内部l构和工作原理的便于理解的表达?/p> 在这里,建模的过E需要用一些工兗在RUP中徏模用UML来完成。在<Thinking in UML>中讲qC(jin)UML的核?j)模型,包括Q?/p> 可能在大家的普遍认识中,UML无非是几种图,q且_看一眼理解v来也不困难,甚至q能用来ȝcd做下代码l构设计。但UML的作用不仅仅如此?/p> 以上所描述的UML核心(j)模型中,每个模型q不单指的的是一UUML图。每个模型实际上都会(x)包含几种UML图,?x)包含若q张UML图。这些模型基本上渗透于RUP?个工作流E中Q只不过不同的工作流E用的模型比重不一而已?/p>
例如?#8220;分析设计”工作程中,可能?x)用到pȝ用例模型、分析模型、Y件架构和框架、设计模型等Q而业务用例模型可能在q个程中根本不?x)用刎ͼ相反Q业务用例模型则可能?#8220;业务建模”程中被q泛使用?/p> 前已q及(qing)在RUP的每个工作流E中QRUP定义?jin)该程需要参与的角色Q以?qing)这些角色需要进行的zdQ例如这里可以看?#8220;分析设计”程中的角色和活动集Q摘?lt;Thinking in UML>Q:(x) 相应的,在该工作程中需要出的工g集ؓ(f)Q摘?lt;Thinking in UML>Q:(x) 既然使用?jin)UML作ؓ(f)建模工具Q所以可以简单地说这些工件主要就是UML图,当然也会(x)有其他文档性质的事物,例如|络协议l构、数据库表等UML无法描述的东西则通过普通文字性文描q?/p>
到目前ؓ(f)止我们已l了(jin)解到RUP定义?jin)开发过E?phase)Q定义了(jin)每个q程包含的若q工作流E,q定义了(jin)每个工作程需要哪些角色从事哪些活动来完成哪些工g。除此之外,RUPq提供了(jin)6条最?jng)_는以指DY件开发:(x) q些实践在我看来仅仅是一些项目开发的指导原则Q它们渗透到每一个过E,每一个工作流E。在目q程中实践这些原则,用以保?rn)目的成功。例如我们用UML建模Q以辑ֈ“可视建模”。我们通过建立需求用例,?#8220;理需?#8221;?/p> g没有文来专门阐qRUP的核?j)思想Q但我觉得掌握其核心(j)思想才是学习(fn)的要Ҏ(gu)在。要理解一UY件开发方法学的核?j)思想Q其实最好是其与别的方法学做比较。这里先我的一些感惛_阐述?/p> 用例驱动指的是整个Y仉目的推进q程Q是依靠“用例”来完成?lt;Thinking in UML>Q?/p> 在实际的软g目中,一个Y件要实现的功能通过用例来捕P接下来的所有分析、设计、实现、测试都q例来取得Q即以实现用例ؓ(f)目标。在l一q程中用例能够驱动的不仅仅是分析设计?/p> 用例单来说就是描qC(jin)一个系l功能。但必须注意的是Q这仅仅是它定义的一部分。用例主要分布在“业务建模”?#8220;需?#8221;?#8220;分析设计”q些工作q程中。在不同的过E中用例的粒度和性质都不一栗例如对于一个借书pȝ而言Q在业务建模阶段Q我们可以获取出一?#8220;借书”用例Q其pȝ边界甚至不是pȝ而可能仅x(chng)q个业务本nQ因个阶D还没有考虑到计机如何实现q个借书业务Q;在系l分析阶D,我们可以将“借书”q个用例l化为用户和计算Y件系l的交互Q进一步地Q我们可能会(x)q一步精化这个用例,例如用户通过|页l端“借书”。(q里描述?jin)很多徏模的l节Q可不必qQ本文只l出一个概要性的介绍Q?/p> 我们说用例驱动Y件开发,但它如何驱动的呢Q我在实际的建模q程中,最明显的感受就是用例驱动了(jin)整个建模q程?/p> q入设计阶段后,虽然可以q一步徏模,得到?x)直接映到代码的类图、序列图{,但这L(fng)囑֜面(f)需求变更时Q基本上?x)面临修改,q意味着l护q些文需要耗费_֊。所以,<Thinking in UML>中主张将_֊攑֜l护分析cL型中Q而通过其他U定实现从分析类到实际代码的转换。我觉得q个也在理?/p> 我个得RUP的一大特点在于规范化?jin)整个Y件开发过E,每一个步骤需要哪些角色参与,该干什么,怎么d都有指导。加之这些活动的”可推导?#8220;Q这意味着不论参与角色属于什么水qI都可以稳固地推进目q程?/p>
此外Q这U规范化也会(x)l项目留下详l的演化q程。你可以明确地看到整个Y件是如何演化出最l的产品代码Q可以深入地理解目代码中的设计?/p> 我只是一个RUP新手Q即便如此,我依然不觉得RUP是Y件开发的万能药。我怿M软g开发方法都是有局限性的。我们在实际使用的时候也只是吸取其精华。不同的开发方法其适用范围也是不一L(fng)。正如有人将RUP和XP做比较时_(d)如果你用RUPd一个杂货铺Q在没开张之前你已l破产了(jin)Q同样如果你用XPd飞机Q飞机毁?jin)十来次也许才能做出来(from <Thinking in UML> againQ?/p> 一般的MMORPG中,游戏对象主要包括怪物和玩家。这两类对象在经q游戏性方面的不断“q化”后,其属性数量及(qing)与之相关的逻辑往(xin)往(xin)?x)变得很巨大。如何将q一块做得既不损失效率,又能保证l构的灵zR清晰、可l护Q本文将提供一U简单的l构?/p>
最原始的结构,极有可能? 也就是,一个对象ؓ(f)一个C++c,然后里面直接塞满?jin)各U属性名Q然后是针对q个属性的逻辑操作Q函敎ͼ(j)。其l果是Player成ؓ(f)巨类。针对这个情况,一直以来我觉得可以使用一U简单的Ҏ(gu)来拆分这个类。冠以官腔,UC为Entity-Component-based Desgin。生这U想法和我的个h技术积累有一定关p,见下文?/p>
Policy-based DesignQ基于决{的设计。这个概忉|源于<Modern C++ Design>。虽然这本书讲述的是针对C++模板的用及(qing)设计技巧。但q种思想依然被我潜意识般地用在其他地斏VPolicy大致来说是一个小的组?Component)。它努力不依赖于其他东西Q它可能是个简单的c,它拥有极的数据l构Q及(qing)针对q些数据的极操作接口。D例而言Q玩家MP的自动回复功能,可装Z个Policy。将许多Policyl合hQ就可完成一个复杂的功能?/p>
q种思想q可指导很多E序l构斚w的设计。例如在做功能的接口拆分Ӟ将每个函数设计得够小Q小到单U地完成一个功能。一个功能的入口函数Q就之前实现的函数全部组合v来,然后共同完成功能炏V?/p>
当然Q?lt;Modern C++ Design>里的Policy在表现Ş式上有所不同。但其核?j)思想相同Q主要体现在 l合 特点上?/p>
Entity-Component-based Design按照google到的文章Q严格来说算是与OOP完全不同的Y件设计方法。不q在q里它将按照我的意思重新被解释?/p>
如果说Policy-based Design极大可能地媄(jing)响着我们qx(chng)的细节编码,那么Entity-Component则是直接Ҏ(gu)戏对象的l构设计做直接的说明?一个游戏对象就是一个Entity?/strong> Entity拥有很少的属性,也许仅包含一个全局标示的ID?一个Component则是Entity的某个行为、或者说某个l成部分?/strong> 其实说白?jin),以玩家?f)例,一个玩家对象就是一个EntityQ而一个MP的自动回复功能就可被包装Z个Component。这个Component可能包含若干与该功能相关的数据,例如回复旉间隔Q每ơ的回复量等。我们往(xin)玩家对象q个Entityd各种ComponentQ也是l玩家添加各U逻辑功能?/p>
但是QComponent之间可能?x)涉及(qing)到交互Q玩家对象之外的模块可能也会(x)与玩家内的某个Component交互。子功能点的拆分Q不得不涉及(qing)到更多的胶水代码Q这也算一U代仗?/p>
q䆾属性结构设计,基本是参考了(jin)上面提到的设计思想。整个系l有如下lg: 意即Q所有Entity都包含一个属性表和组件表。这里的属性表q编码的属性数据成员集合,而是一个key-value形式的表。Property包含一个观察者列表,其实是一pd回调函数Q但是这些观察者本质上也是lgQ后面会(x)提到。Component正如上文描述Q仅包含Component本n实现的功能所需要的数据和函数。整个结构大致的代码如下: 属性本w是抽象的,q完全是因ؓ(f)我们属性统一地放在了(jin)一个表里。从而又D属性的g需?a >l箋(hu)阅读 模块化的程序是怎样的程序?我们可以说一个具有明显物理结构的软件是模块化的,例如带
插件的软件,一个完整的软件由若干运行时库共同构建;也可以说一个高度面向对象的库是
模块化的,例如图形引擎OGRE;也可以说一些具有明显层次结构的代码是模块化的。 模块化的软件具有很多显而易见的好处。在开发期,一个模块化的设计有利于程序员实现,
使其在实现过程中一直保持清晰的思路,减少潜伏的BUG;而在维护期,则有利于其他程序
员的理解。 在我看来,具有良好模块设计的代码,至少分为两种形式: 上述两种形式并非完全分离,在分层设计中,某一层软件层也可能由若干个独立的模块构成
。另一方面,这里也不会绝对说低层模块就完全不依赖于高层模块。这种双向依赖绝对不是
好的设计,但事实上我们本来就无法做出完美的设计。 本文将代码分层分为两大类:一是狭义上的分层,这种分层一般伴有文件形式上的表现;一
是广义上的分层,完全着眼于我们平时写的代码。 软件分层一般我们可以在很多大型软件/库的结构图中看到。这些分层每一层本身就包含大
量代码。每个模块,每一个软件层都可能被实现为一个运行时库,或者其他以文件形式为
表现的东西。 Android是Google推出的智能手机操作系统,在其官方文档中有Android的系统架构图: 这幅图中很好地反映了上文中提到的软件层次。整个系统从底层到高层分为Linux kernel,
Libraries/Runtime,Application Framework,Applications。最底层的Kernel可以说与应
用完全不相关,直到最上层的Applications,才提供手机诸如联系人、打电话等应用功能。 每一层中,又可能分为若干相互独立(Again,没有绝对)的模块,例如Libraries那一层
中,就包含诸如Surface manager/SGL等模块。它们可能都依赖于Kernel,并且提供接口给
上层,但彼此独立。 在编译器实现中,也有非常明显的层次之分。这些层次可以完全按照编译原理理论来划分。
包括: 软件分层的好处之一就是对任务(task)的抽象,封装某个任务的实现细节,提供给其他
依赖模块更友好的使用接口。隔离带来的好处之一就是可轻易替换某个实现。 例如很
多UI库隔离了渲染器的实现,在实际使用过程中,既可以使用Direct X的渲染方式,也可
以使用OpenGL的实现方式。 但正如之前所强调,凡事没有绝对,凡事也不可过度。很多时候无法保证软件层之间就是单
向依赖。而另一些时候过度的分层也导致我们的程序过于松散,效率在粘合层之间绕来绕去
而消失殆尽。 如果说软件分层是从大的方面讨论,那么本节说的代码分层,则是从小处入手。而这也更是
贴近我们日常工作的地方。本节讨论的代码分层,不像软件分层那样大。每一层可能就是
百来行代码,几个接口。 很多C代码写得少的C++程序员甚至对一个大型C程序中的模块组织毫无概念。这是对其他技
术接触少带来的视野狭窄的可怕结果。 在C语言的世界里,并不像某些C++教材中指出的那样,布满全局变量。当然全局变量的使
用也并不是糟糕设计的标志(goto不是魔鬼)。一个良好设计的C语言程序懂得如何去抽象、
封装模块/软件层。我们以Lua的源代码为例。 lua.h文件是暴露给Lua应用(Lua使用者)的直接信息源。接触过Lua的人都知道有个结构体
叫lua_State。但是lua.h中并没有暴露这个结构体的实现。因为一旦暴露了实现,使用者就
可能会随意使用其结构体成员,而这并不是库设计者所希望的。 封装数据的实现,也算
是构建模块化程序的一种方法。 大家都知道暴露在头文件中的信息,则可能被当作该头文件所描述模块的接口描述。所以,
在C语言中任何置于头文件中的信息都需要慎重考虑。 相对的,我们可以在很多.c文件中看到很多static函数。例如lstate.c中的stack_init。
static用于限定其修饰对象的作用域,用它去修饰某个函数,旨在告诉:这个函数仅被当前文件(
模块)使用,它仅用于本模块实现所依赖,它不是提供给模块外的接口! 封装内部实现
,暴露够用的接口,也是保持模块清晰的方式之一。 良好的语言更懂得对程序员做一种良好设计的导向。但相对而言,C语言较缺乏这方面的语
言机制。在C语言中,良好的设计更依赖于程序员自己的功底。 相较而言,Java语言则提供了模块化设计的语法机制。在Java中,如同大部分语言一样,一
般一个代码文件对应于一个代码模块。而在Java中,每个文件内只能有一个public class。
public class作为该模块的对外接口。而在模块内部,则可能有很多其他辅助实现的class
,但它们无法被外部模块访问。这是语言提供的封装机制,一种对程序员的导向。 无论在C++中,还是在Java中,一个类中的接口,都大致有各种访问权限。例如public、
private、protected。访问权限的加入旨在更精确地暴露模块接口,隐藏细节。 在C中较为缺乏类似的机制,但依然可以这样做。例如将结构体定义于.c文件中,将非
接口函数以static的方式实现于.c文件中。 OO语言中的这些访问权限关键字的应用尤为重要。C++新手们往往不知道哪些成员该public
,哪些该private。C++熟手们在不刨根挖底的情况下,甚至会对每个数据成员写出get/set
接口(那还不如直接public)。在public/private之间,我们需要做的唯一决策就是,哪些
数据/操作并非外部模块所需。如果外部模块不需要,甚至目前不需要,那么此刻,都不要
将其public。一个public信息少的class,往往是一个被使用者更喜欢的class。 (至于protected,则是用于继承体系之间,类之间的信息隐藏。) 又得提提Lisp。 基于上文,我们发现了各种划分模块、划分代码层的方式,无论是语言提供,还是程序员自
己的应用。但是如何逐个地构建这些层次呢? Lisp中倡导了一种更能体现这种将代码分层的方式:自底而上地构建代码。这个自底而上,
自然是按照软件层的高低之分而言。这个过程就像上文举的编译原理例子一样。我们先编写
词法分析模块,该模块可能仅暴露一个接口:get-token。然后可以立马对该模块进行功能
测试。然后再编写语法分析模块,该模块也可能只暴露一个接口:parse。语法分析模块建
立于词法分析模块之上。因为我们之前已经对词法分析模块进行过测试,所以对语法分析的
测试也可以立即进行。如此下去,直至构建出整个程序。 每一个代码层都会提供若干接口给上层模块。越上层的模块中,就更贴近于最终目标。每一
层都感觉是建立在新的“语言“之上。按照这种思想,最终我们就可以构建出DSL,即Domain
Specific Language。 基于以上,我们可以总结很多代码分层的好处,它们包括(但不限于): 有时候,我们的软件层很难做到单向依赖。这可能是由于前期设计的失误导致,也可能确实
是情况所迫。在很多库代码中,也有现成的例子。一种解决方法就是通过回调。回调的实现
方式可以是回调函数、多态。多态的表现又可能是Listener等模式。 所有这些,主要是让底层模块不用知道高层模块。在代码层次上,它仅仅保存的是一个回调
信息,而这个信息具体是什么,则发生在运行期(话说以前给同事讲过这个)。这样就简单
避免了底层模块依赖高层模块的问题。 精确地定义一个软件中有哪些模块,哪些软件层。然后再精确地定义每个模块,每个头文件
,每个类中哪些信息是提供给外部模块的,哪些信息是私有的。这些过程是设计模块化程
序的重要方式。 但需要重新强调的是,过了某个度,那又是另一种形式的糟糕设计。但其中拿捏技巧,则只
能靠实践获取。前言
概要
RUP概览
RUPq程与实?/h2>
RUP框架
RUP建模
RUP最?jng)_?/h3>
RUP核心(j)思想
用例驱动
规范化整个过E?/h3>
ȝ
参考文?/h2>
MMO游戏对象属性设?/h1>
Author:
Kevin Lynx Date:
5.2.2011 原始l构
Player: +---------------+
| property-1 |
+---------------+
| property-2 |
+---------------+
| ... |
+---------------+
| operator-1 |
+---------------+
| operator-2 |
+---------------+
| ... |
+---------------+
Policy-based Design
Entity-Component-based Design
游戏对象属性设?/h1>
Entity: +-------------------+
| property-table |
+-------------------+
| component-table |
+-------------------+
Property: +-------------------+
| observer-list |
+-------------------+
Component: +--------------------+
| logic-related data |
+--------------------+
| logic-related func |
+--------------------+
class Entity {
private:
GUID id;
std::map<std::string, IComponent*> components;
std::map<std::string, Property*> properties;
};
class Property {
private:
std::string name;
Value val;
std::vector<IComponent*> observers;
};
class IComponent {
public:
virtual bool Operate (const Args &args) { return false; }
virtual void OnNotify (const Property &property, const Args &args) {}
protected:
std::string name;
Entity *entity;
};
浅谈代码分层:构建模块化程序
Author: Kevin Lynx Date: 4.4.2011 Contact: kevinlynx at gmail dot com 软件分层
Example Android
Example Compiler
代码分层
Example C中的模块组织
Example Java中的模块组织
Example OO语言中类接口设计
Example Lisp中的模块设计
分层的好处
一个问题的解决
END
在一个UI与逻辑模块交互比较多的E序中,因ؓ(f)q不惌两个模块发生太大的耦合Q基本目标是
可以完全不改代码地换一个UI。逻辑模块需要在产生一些事件后通知到UI模块Qƈ且在q个通知
里携带够多的信息(数据Q给接收通知的模块,例如UI模块。逻辑模块q可能被攄于与UI?br>块不同的U程里?/p>
最初的l构
最开始我直接采用最单的Ҏ(gu)Q逻辑模块保存?sh)个UI模块传过来的listener。当有事件发生时Q?br>回调相应的接口此通知传出厅R大致结构如下:(x)
但是Q在代码写多之后Q逻辑模块需要通知的事件越来越多之后,EventNotifyq个cd?br>膨胀Q接口变多了(jin)、不同接口定义的参数看v来也来恶?j)?jin)?/p>
改进
于是我决定将各种事g通知l一化:(x)
q样Q逻辑模块只需要创Z件结构,两个模块间的通信只需要一个接口即可:(x)
void OnNotify( const Event &event );
但是问题又来?jin),不同的事件类型携带的附属参数Q数据)(j)不一栗也许,可以使用一个序列化
的组Ӟ各U数据先序列化,然后在事件处理模块对应地取数据出来。这样做L觉有点大?br>q戈?jin)。当?dng)也可以用C语言里的不定参数去解冻I如:(x)
void OnNotify( long event_type, ... )
其实Q我需要的是一个可以表面上cd一P但其内部保存的数据却多样的东ѝ这样一惻I
模块p让事情简单化Q?/p>
在上面这个例子中Q虽焉过Param的包装,逻辑模块可以在事仉知里放|Q意类型的数据Q但
毕竟只支?个参数。实际上Z(jin)实现支持多个参数Qv码得?5个)(j)Q还是免不了(jin)自己实现多个
参数的Param?/p>
q怺我以前写q宏递归产生代码的东西,可以自动地生成这U情况下诸如Param1、Param2的代码?br>如:(x)
卛_生成Param1和Param2的版本。其实这样定义了(jin)Param1、Param2的东西之后,又得OnNotify
的参C是特定的?jin)。虽然可以把Param也泛化,但是在逻辑层写q多的模板代码,L觉不好?/p>
于是又想C前写的一个东西,可以把各U类型包装成一U类?--对于外界而言Qany。any?br>boost中有提到Q我只是实现?jin)个单的版本。any的大致实现手法就是在内部通过多态机制将?br>U类型在某种E度上隐藏,如:(x)
q样Qanyc通过一个base_typec,利用C++多态机制即可将cd隐藏于var_holder里。那么,
最l的事g通知接口成ؓ(f)下面的样子:(x)
void OnNotify( long type, any data );
OnNotify( ET_ENTER_RGN, any( create_param( player, rgn_id ) ) );其中Qcreate_param
是一个辅助函敎ͼ用于创徏各种Param对象?/p>
事实上,实现各种ParamN版本Q让其名字不一样其实有点不妥。还有一U方法可以让Param的名?br>只有一个,那就是模板偏特化。例如:(x)
q种Ҏ(gu)主要是通过l合ZU函数类型,来实现偏特化。因为我觉得构造一个函数类型给L版,
q不是一U合情理的事情。但是,即使用偏特化来让Param名字看v来只有一个,但对于不同的
实例化版本,q是不同的类型,所以还是需要any来包装?/p>
实际使用
实际使用h让我觉得非常赏心(j)(zhn)目。上面做的这些事情,实际上是做了(jin)一个不同模块间零耦合
通信的通道Q零耦合g有点q激Q。现在逻辑模块通知UI模块Q只需要定义新的事件类型,?br>两边分别写通知和处理通知的代码即可?/p>
PS:
针对一些评论,我再解释下。其实any只是用于包装Param列表而已Q这里也可以用void*Q再转成
Param*。在q里q多地关注是用any*q是用void*其实偏离?jin)本文的重点。本文的重点其实是ParamQ?/p>
其实Q我们都知道Qauto_ptrq些东西始终是无法避免野指针和空指针带来的灾难?br>SAFE_DELETE也不能阻止别Z用这个空指针?
在我看过的一些开源项目的代码中,q些代码lh的感觉就是别人总能详细地掌控各U资?br>Q包括指针及(qing)其他变量Q的使用情况。相比之下,公司隔壁l的老大则显得保守很多。他?br>求我们几乎要Ҏ(gu)有指针的使用q行I值判断(野指针也判断不了(jin)Q,当然Q各U成员变?br>也要q行即现在看上L多大用的初始化?
也许Q这样做后程序是不会(x)挂掉?jin)。但是,我们的观点来看Q这样反而会(x)隐藏一些BUG?br>Z么我们不能详地ȝ理一个指针?一个指针变?sh)空了(jin),L因ؓ(f)在这之前发生?jin)错?br>。当?dng)野指针本w就是愚蠢代码生的东西Q这里没必要讨论。空指针之所以ؓ(f)I,也是
因ؓ(f)在很多时候我们把IZ为失?错误/无效的标志?
恰好上周我的一些代码就真的在空指针上出C(jin)问题。外|的服务器随时会(x)因ؓ(f)玩家的一?br>临界操作行ؓ(f)而崩溃掉。虽然我通过修改脚本来屏蔽这个问题(因ؓ(f)不能说停机维护就停机
l护Q,但是L觉程序是不安全的。h不吃Ҏ(gu)训绝对不学乖?
后来我对q个问题d思考了(jin)一下。很多程序员都自认聪明。在写C(j)++E序Ӟ我从来不?br>供没用的public接口Q尤其是set/get。我也从来不Ҏ(gu)必要的成员变量进行初始化。我l?br>的理由是对于q些东西我都有很清晰的把握,我ؓ(f)什么要做stupid的事情?
但是Q我几乎从来没有界定Q指针在哪些情况下需要去判断为空Q函数的参数l对不需要?br>假如函数的参数就是个I指针,那是clientE序员的责Q。仅供模块内使用的指针(包含?br>他资源)(j)在内部用时也不需要去判断。如果去判断?jin),那说明你对你自己写的模块都缺?br>_的把握,证明你的设计思维不够清晰?
什么时候需要判断?当指针依赖于外部环境Ӟ例如读配|文件、蝲入资源,因ؓ(f)外部因素
不确定不在自己控制范围内Q那么进行判断。同P当用了(jin)其他模块q回的指针值时Q也
需要判断。这个其实和“外部环境”属于同一U情c(din)因为我们对其他模块也不清楚Q更?br>隐蔽的是Q随着其他模块的改变,来?x)在你的模块里爆发崩溃错误?j)Q其他模块由别hl?br>护,其变化更不受自己控制。之前我对这一点界定不是很清楚Q这也是我犯错的原因?
现在x(chng)Q像游戏服务器这U程序,里面塞着各种各样的游戏功能。无论是哪一个模块出?br>个空指针讉K出错的问题,都会(x)直接让服务器崩掉。关键是q个l果l常伴随着玩家的损?br>。所以理想状态下Q把每一个模块都攄在单独的q程里,实是很有好处的?
struct MyLess
{
bool operator() ( long left, long right )
{
//...
}
};
DEBUG模式下编译没问题QRELEASE模式下却出现C3848的错误。这有点奇怪了(jin)Q如果确实存在语法错误,
那么DEBUG和RELASE应该一h寏V查?jin)下MSDNQC3848的错误是因ؓ(f)const限定W造成的,如:(x)
const MyLess pr;
pr(); // C3848
于是Q在operator()后加上constQ就O(jin)K?jin)。看?jin)下VC std::map相关代码Q以为是DEBUG和RELEASE使用?jin)?br>同的代码造成。但是我始终没找C同点。另一斚wQ就STL内部的风格来看,应该不会(x)把predicator处理
成const &之类的东西,全部是value形式的。奇怪了(jin)?/p>
W二个问题,涉及(qing)到静(rn)态变量。这个东西给我的印象特别深刻Q因Z前去一家外企应聘时被问刎ͼ当时
觉得那个LEADER特别厉害。回来后让我反思,是不是过多地x(chng)?jin)C++里的花哨Q而漏掉了(jin)C里的朴素Q导?br>我至今对UC存在偏好?/p>
说正题,我现在有如下的文件关p:(x)
// def.h
struct Obj
{
Obj()
{
ObjMgr::AddObj( id, this );
}
int id;
};
struct ObjMgr
{
static void AddObj( int id, Obj *t )
{
ObjTable[id] = t;
}
static std::map<int, Obj*> ObjTable;
};
static Obj _t;
// ObjMgr.cpp
#include "def.h"
static std::map<int, Obj*>::ObjMgr ObjTable;
// main.cpp
#include "def.h"
q里丄例子可能有点不恰当,我在一台没有编译器的机器上写的q篇文章。忽略掉q些旁支末节。我的意思,
是惌Obj自己自动向ObjMgr里添加自己。我们都知道?rn)态变量将在程序启动时被初始化Q先于main执行之前?/p>
上面代码有两个问题:(x)
一?br>代码没有按照我预期地执行Q如果你按照我的意思做个测试,你的E序甚至在进main之前crash?jin)。我假定?br>用的是VCQ因为我没在其他~译器上试验q。问题就在于QObj的构造依赖于ObjTableq个map对象。在调试q程
中我发现Q虽然ObjTable拥有?jin)内存空_(d)其this指针有效Q但是,map对象q没有得到构造。我的意思是QObj
的构造先于ObjTable构造(下几个断点即可轻易发玎ͼ(j)Q那么在执行map::operator[]Ӟ出错了(jin)Q因个时?br>map里相x(chng)据还没准备好?/p>
那是否存在某U机制可以手动静(rn)态变量的初始化顺序呢Q不知道。我最后怎样解决q个问题的?
二?br>在还没有惛_解决办法之前Q不改变我的设计Q,我发C(jin)q段代码的另一个问题:(x)我在头文仉定义?jin)?rn)?br>变量Qstatic Obj _t; q有什么问题?x(chng)预编译这个过E即可知道,头文件在预编译阶D被文本展开到CPP
文g里,然后QObjMgr.cpp和main.cpp文g里都出现static Obj _t;代码。也是_(d)ObjMgr.obj和main.obj
里都有一个文仉(rn)态变量_t?/p>
看来Q在头文仉放这个静(rn)态变量是肯定不对的。于是,我将_tUd到ObjMgr.cpp里:(x)
// ObjMgr.cpp
#include "def.h"
static std::map<int, Obj*>::ObjMgr ObjTable;
static Obj _t;
按照q样的顺序定义后Q_t的构造居然晚于ObjTable?jin)。也是_(d)攄于前面的变量定义Q就意味着它将?br>首先构造初始化。这样两个问题都解决?jin)?/p>
但是Q谁能保证这一点特性?C标准文档里?q是VC~译器自己?
Kevin Lynx
Proactor?a target=_blank>Reactor都是q发~程中的设计模式。在我看来,他们都是用于z֏/分离IO操作事g的。这里所谓的
IO事g也就是诸如read/write的IO操作?z֏/分离"是单独的IO事g通知C层模块。两个模式不同的地方
在于QProactor用于异步IOQ而Reactor用于同步IO?
摘抄一些关键的东西Q?
"
Two patterns that involve event demultiplexors are called Reactor and Proactor [1]. The Reactor patterns
involve synchronous I/O, whereas the Proactor pattern involves asynchronous I/O.
"
关于两个模式的大致模型,从以下文字基本可以明白:(x)
"
An example will help you understand the difference between Reactor and Proactor. We will focus on the read
operation here, as the write implementation is similar. Here's a read in Reactor:
* An event handler declares interest in I/O events that indicate readiness for read on a particular socket ;
* The event demultiplexor waits for events ;
* An event comes in and wakes-up the demultiplexor, and the demultiplexor calls the appropriate handler;
* The event handler performs the actual read operation, handles the data read, declares renewed interest in
I/O events, and returns control to the dispatcher .
By comparison, here is a read operation in Proactor (true async):
* A handler initiates an asynchronous read operation (note: the OS must support asynchronous I/O). In this
case, the handler does not care about I/O readiness events, but is instead registers interest in receiving
completion events;
* The event demultiplexor waits until the operation is completed ;
* While the event demultiplexor waits, the OS executes the read operation in a parallel kernel thread, puts
data into a user-defined buffer, and notifies the event demultiplexor that the read is complete ;
* The event demultiplexor calls the appropriate handler;
* The event handler handles the data from user defined buffer, starts a new asynchronous operation, and returns
control to the event demultiplexor.
"
可以看出Q两个模式的相同点,都是Ҏ(gu)个IO事g的事仉知(卛_诉某个模块,q个IO操作可以q行或已l完?。在l构
上,两者也有相同点Qdemultiplexor负责提交IO操作(异步)、查询设备是否可操作(同步)Q然后当条g满Ӟ回调handler?br>不同点在于,异步情况?Proactor)Q当回调handlerӞ表示IO操作已经完成Q同步情况下(Reactor)Q回调handlerӞ表示
IO讑֤可以q行某个操作(can read or can write)Qhandlerq个时候开始提交操作?
用select模型写个单的reactorQ大致ؓ(f)Q?
在网上找?jin)䆾Proactor模式比较正式?a href="http://www.shnenglu.com/Files/kevinlynx/proactor2.rar" target=_blank>文档Q其l出?jin)一个M的UMLcdQ比较全面:(x)
Ҏ(gu)q䆾图我随便写了(jin)个例子代码:(x)
Reactor通过某种变ŞQ可以将其改装ؓ(f)ProactorQ在某些不支持异步IO的系l上Q也可以隐藏底层的实玎ͼ利于~写跨^?br>代码。我们只需要在dispatch(也就是demultiplexor)中封装同步IO操作的代码,在上层,用户提交自己的缓冲区到这一层,
q一层检查到讑֤可操作时Q不像原来立卛_调handlerQ而是开始IO操作Q然后将操作l果攑ֈ用户~冲??Q然后再
回调handler。这P对于上层handler而言Q就像是proactor一栗详l技法参?a target=_blank>q篇文章?
其实p计模式而言Q我个h觉得某个模式其实是没有完全固定的l构的。不能说某个模式里就肯定?x)有某个c,cM间的
关系p定是q样。在实际写程序过E中也很去特别地实现某个模式,只能说模式会(x)l你更多更好的架构方案?
最q在看spserver的代码,看到别h提各Uƈ发系l中的模式,有点眼红Q于是才来扫扫盲。知道什么是leader follower模式Q?br>reactor, proactorQmultiplexingQ对于心(j)中的那个|络库也来清晰?
最q还q了(jin)些离q事,写了(jin)传说中的字节编码,用模板的方式实现Q不但保持了(jin)扩展性,q少写很多代码;处于效率考虑Q?br>写了(jin)个static array容器(其实是template <typename _Tp, std::size_t size> class static_array { _Tp _con[size])Q?br>加了(jin)iteratorQ遵循STL标准Q可以结合进STL的各个generic algorithm用,自我感觉不错。基模块搭徏完毕Q解析了(jin)公司
服务器网l模块的消息Q我是不是真的打用自己的网l模块重写我的验证服务器Q在另一个给公司写的工具里,因ؓ(f)实在厌恶
来多的重复代码,索性写?jin)几个宏Q还真的做到?jin)代码的自动生?D?
对优雅代码的q求真的成了(jin)U癖? = =|
1.半同?半异步(half-sync/half-asyncQ:(x)
在网上一份资?/a>中引用了(jin)一本貌似很l典的书里的比喻Q?br>?
许多厅使用 半同?半异?模式的变?sh)。例如,厅常常雇䄦一个领班负责迎接顾客,q在厅J忙时留意给֮安排桌位Q?br>为等待就的֮按序排队是必要的。领班由所有顾客“共享”,不能被Q何特定顾客占用太多时间。当֮在一张桌子入坐后Q?br>有一个侍应生专门张桌子服务?
?
按照另一份似乎比较权威的文档的描qͼ要实现半同步/半异步模式,需要实C层:(x)异步层、同步层、队列层。因为很多操?br>采用异步方式?x)比较有效?例如高效率的|络模型g都采用异步IO)Q但是异步操作的复杂度比较高Q不利于~程。而同?br>操作相对之下~程要简单点。ؓ(f)?jin)结合两者的优点Q就提出?jin)这个模式。而ؓ(f)?jin)让异步层和同步层互盔R信(模块间的通信)Q系
l需要加入一个通信队列。异步层操作结果放入队列,同步层从队列里获取操作结果?
回过头来看看我之前写的那个select|络模型代码Q个为基本上是一个半同步半异步模式的单例子:(x)Buffer相当于通信
队列Q网l底层将数据写入BufferQ上层再同步C该队列里获取出数据。这L(fng)来似乎也没什么难度?= =
关于例子代码Q直接引用iunknownl的Q?
//q就是一个典型的循环队列的定义,iget 是队列头Qiput 是队列尾</STRONG>
int clifd[MAXNCLI], iget, iput;
int main( int argc, char * argv[] )
{
......
int listenfd = Tcp_listen( NULL, argv[ 1 ], &addrlen );
......
iget = iput = 0;
for( int i = 0; i < nthreads; i++ ) {
pthread_create( &tptr[i].thread_tid, NULL, &thread_main, (void*)i );
for( ; ; ) {
connfd = accept( listenfd, cliaddr,, &clilen );
clifd[ iput ] = connfd; // 接受到的q接句柄攑օ队列</STRONG>
if( ++iput == MAXNCLI ) iput = 0;
}
}
void * thread_main( void * arg )
{
for( ; ; ) {
while( iget == iput ) pthread_cond_wait( ...... );
connfd = clifd[ iget ]; // 从队列中获得q接句柄</STRONG>
if( ++iget == MAXNCLI ) iget = 0;
......
web_child( connfd );
close( connfd );
}
}
2.领导?q随者(Leader/FollowersQ:(x)
同样Q给出别人引用的比喻Q?br>?br>在日常生zMQ领D?q随者模式用于管理许多飞机场出租车候R台。在该用例中Q出UR扮演“线E”角Ԍ排在W一辆的?br>UR成ؓ(f)领导者,剩下的出UR成ؓ(f)q随者。同P到达出租车候R台的乘客构成?jin)必被多\分解l出UR的事Ӟ一般以先进
先出排序。一般来_(d)如果M出租车可以ؓ(f)M֮服务Q该场景׃要相当于非绑定句?U程兌。然而,如果仅仅是某?br>出租车可以ؓ(f)某些乘客服务Q该场景q当于l定句柄/U程兌?
?
其实q个更简单,我记?lt;unix|络~程>中似乎提到过q个。M有一U网l模?connection-per-thread?)里,一个线E用?br>acceptq接。当接收C个新的连接时Q这个线E就转ؓ(f)connection threadQ而这个线E后面的U程则上升ؓ(f)acceptU程。这里,
acceptU程q当于领导者线E,而其他线E则属于q随者线E?
iunknown 的例子代码:(x)
int listenfd;
int main( int argc, char * argv[] )
{
......
listenfd = Tcp_listen( NULL, argv[ 1 ], &addrlen );
......
for( int i = 0; i < nthreads; i++ ){
pthread_create( &tptr[i].thread_tid, NULL, &thread_main, (void*)i );
}
......
}
void * thread_main( void * arg )
{
for( ; ; ){
......
// 多个U程同时d在这?accept 调用上,依靠操作pȝ的队?lt;/STRONG>
connfd = accept( listenfd, cliaddr, &clilen );
......
web_child( connfd );
close( connfd );
......
}
}