曾经作为一个“优秀”的Web开发工程师,这次博文似乎要聊我最熟悉的领域了。数据库系统作为信息系统的基石,同时也是系统的瓶颈所在。在游戏系统中,数据库地位同样重要,但不同于常规信息系统的是,编写游戏程序是把主要的人工编码工作放在游戏玩法逻辑的编写,优化重心也放在网络承载、战斗计算逻辑等。前文已经提到,数据库作为游戏数据的持久化存储,一方面是容灾,另一方面作物理媒介,PHP运营后台通过物理数据库与游戏大世界建立联系。
那说到数据访问层,J2EE程序员可能会说出一大堆专业词汇和技术:DAO、三层结构、ORM、Hibernate、Mybatis、工厂模式、数据库链接池(DBCP、C3P0)等等。那先看看J2EE世界里的数据访问层数如何设计的:
图1J2EE数据访问模型
Java世界里很多设计模式和框架无疑是相当优秀的,如面向接口编程就是杰出代表,也正是这种优秀的设计思维,大大减少了各层间的耦合,一层一层抽丝剥茧,每一层都可以按照需求选择各具特色的框架,同时在遵循一定接口规范的基础上,厂商可以开发性能各异的组件框架。一套应用系统的跨平台,包含多个方面的平台兼容性,而在数据访问层面上的跨平台,通常是指兼容多种关系型数据库,换而言之,就是更换数据库厂商不需要改动上层代码,或尽可能少改动上层代码。在这一方面,JAVA世界里的关系对象模型(ORM)无疑已经是相当优秀的了,ORM模型基本上屏蔽了不同数据库的差异,在ORM的基础上实现数据访问,减少设计复杂度,增强访问层兼容性。即便如此,在设计数据访问对象时,也还是遵循面向接口编程的编程思维,上层(服务层)依赖下层的接口而不是实现,这样做的目的同样也是为兼容ORM框架模型留有一定余地,一切为解耦而生啊。从J2EE的设计思维中,给了我们两大启示,1.一切面向接口;2.分层解耦。可以说这两点足以深深的印在我们的骨子里。
再回头看看C++生态系统里这方面设计惯例和资源。首先C++没有接口的概念,但这并不妨碍我们使用面向接口的编程思维,例如C++的抽象类,以及多重继承也可以达到JAVA里面类似的效果。此外从各个框架提供的情况来看,C++阵营显得有些可怜,这并不妨碍我们如何原滋原味的构建一套强健的数据访问模块。从整体上看,我们需要解决3个问题,第一链接池问题,第二设计出一套类似于JDBC的数据库访问机制,第三,从上层的解耦的角度上设计数据访问模型。
类JDBC数据库访问机制
JAVA的JDBC访问数据库主要通过Connection、Statement 、ResultSet这3个类实现对数据的操作,不仅如此,就算是ODBC、ADO、ADO.NET的数据库访问方式也基本上提供类似的功能的对象以完成对数据库的各种访问和操作。Connection表示一个数据连接,这是最最基本的对象,一旦获得一个连接即表示拥有了对数据的操控权;Statement可以理解成用来传递数据库操作命令的对象;而ResultSet用于存放返回结果的对象,可以理解成数据集里面的游标。为了屏蔽各个数据库厂商的差异就必须在这一层完成基本的包装以提供上层统一的接口。同样我们可以定义这三个抽象类,在此基础上针对不同的数据库系统实现这抽象类。比如mysql官方提供了C的访问驱动,就必须对基本的C函数进行封装实现三个基本的类,例如MysqlConnection类建立连接需调用mysql_real_connect()函数等。最终用一个静态类来管理这些物理连接,并将其放入连接池进行逻辑上的连接和关闭管理。连接池管理的这些连接的指针其静态类型是抽象类,而指针指向的动态类型要在程序运行时,根据配置或者宏定义进行实例化获得。这样上层在用到访问对象时只需要用到其静态类型,就是JAVA中的面向接口编程,上层只对这些抽象类的变动敏感,并不关心具体是怎么实现,如此很好实现了解耦。只要在JDBC访问接口不变的情况下,底层代码的改动,并不会影响上层代码结构。当然这一层只是屏蔽掉各种数据库访问方式的不同,除此之外,不同数据库可能会存在对SQL语言或者其内置函数的差异,对于这个问题我们只能放在更上一层去解决。
链接池问题
池的概念和缓存的概念类似,已经成为一种经典的设计模式深入人心。很多需要反复构造的实体(内存、对象、数据库连接、线程等)都可以用池的设计思想优化其设计。其大致的思想就是:在首次使用的时候构造多个数量的实体,其他实际需要构造改实体的时候直接去池子里取,池中实体不够则继续构造,当实体实际需要析构或者关闭时,放入池中即可,当然池只维持某个数量规模的实体,并不是无限增长。这样做的目的,主要还是为了减少实体反复构造析构或者连接反复打开和关闭所带来的开销,是一种典型用空间换时间的思想。基于这种原理设计数据库连接池就非常简单了,这里不帖代码了,文尾有整个实例的全部demo代码的托管地址。
藕合性小的数据访问模型
简单的系统可能只是增、查、删、改这些基本操作,牵涉到更加复杂的业务逻辑则是这些基本操作的组合。对于那些公共的、类似的、基础的操作可以独立出来一层,这里可以比照ORM的相关概念,ORM并没有提供五花八门的各种数据库方法,相反而是提供了尽量精简的基本操作,如get、update、remove、save等,通过这些方法的组合可以满足复杂的业务逻辑需求。同理,我们也可以把这些基本操作封装成一层暂且称之为实体数据基础访问层,这一层由于代码规则一样,可以通过类模板加上代码生成工具进行固化以提高生产效率,让程序员的主要工作放在跟业务贴得更近的上层。此外在第一个问题时提到,不同数据库厂商对于sql语言标准的支持程度以及差异化内置函数的区别并不能完全在那一层完成封装,那这里就是最后的机会了。以获取数据库系系统时间为例,mysql的时间函数是sysdate(),而ms sql server却是getdate(),在实体数据基础访问层进行屏蔽,让程序员安安心心的只关注与业务有关的代码。再往上一层,我们可以通过对数据基本操作方法进行组合形成满足业务需要的数据访问外观层,这里用到了设计模式中的外观模式,将细粒度的对象包装成粗粒度的对象。而这一层需要实现什么样的方法是需要根据逻辑业务层的需求来设计和实现的,其变动性比较大。以某一表为例,这种类图很好的表达了这种结构层次关系。
图2 数据访问类图
红色框里是两个抽象类,其中定义的一些纯虚方法,Astah软件进行了斜体以示区分但不是很明显,我用红线标示出来。这两个抽象类分别表示数据访问基础层、数据访问组合层的接口。显然IActiveDaoBase几乎固定的几个方法get、save、update、remove具有鲜明的特色,而IActiveDao中的load则是业务层实际需要用到的,需要程序猿进行定制编写的。CDaoBase有点类似于常常用到的DBHelper,它封装了对三大对象的基本操作,CActiveDaoImplBase通过对CDaoBase中JDBC对象组合实现IActiveDaoBase所定义的虚方法。而CActiveDaoImpl则是通过组合CActiveDaoImplBase里的基本方法实现业务层真正需要的数据访问接口。撇去接口和CDaoBase不看,剩下两个类CActiveDaoImplBase、CActiveDaoImpl分别表示数据基础访问层和数据访问外观层,数据基础访问层所用到的方法是固定的可以通过代码生成工具进行生产,而数据访问组合层是程序猿需要实际编写的,将不易变动的代码层次进行隔离也是这样分层的重要原因之一。
最终的所设计数据访问模型就成了这个样子
图三 访问层次图
总结:
不管是JAVA还是C++,设计模式、设计思想是相通的,虽然C++的语言优雅程度不及JAVA,可以使用的框架库类也远不及JAVA生态阵营丰富,但这是如此才使得每一个C++程序设计者返璞归真,享受最原汁原味的设计和编程体验。
代码托管:



