软件中的对象
About DOMAIN-DRIVEN DESIGN
领域驱动设计是一种思维方式,目的在于处理具有复杂问题的软件项目。在传统的瀑布软件开发模型中,经历需求分析、设计、开发、测试、交付等阶段,但是问题在于需求从业务方传递到开发团队的时候并不是很顺畅。尽管需求阶段整理了复杂详细的需求文档,设计阶段也产出了详细设计文档,但是开发者由于很少参与了问题域的分析和建模,他们对设计文档的理解往往是片面的,有时甚至会推翻设计文档的模型创作一些临时解决方案,而且往往这时都会有冠冕堂皇的理由---性能。许多设计文档自书写之日起就被束之高阁,我的一个同事L说我们的文档修改日期永远只是创建日期,因为设计文档的更新远远跟不上代码的更新,有时候并不是说逻辑变动多么多么大,只是更新设计文档有时居然比更新代码成本还要高!这种现象在我的项目中屡见不鲜,我越来越意识到设计和开发脱节的危险。
开发者自身也有一些问题,人们很容易将经历和技能集中在技术细节上,软件的网络、数据库等技术层面是技术人最爱讨论的内容。这也跟环境有很大的关系,企业中、社区论坛中充斥着对选择哪种编辑器、那种语言的争论,我看到很多人以精通某种框架为荣,好想有了框架所有的问题都迎刃而解了。我花了三周的时间将LUA引擎做了封装,可以无缝的在C++中调用LUA代码,同时将C++类可以很轻松的注册到LUA中,使用了C++的模板实现了tolua++、luabind的功能。但是这一切完成的时候,我自信的交付的时候,F同事安排我使用LUA实现任务系统,我想了又想、想了又想,突然发现我毫无头绪,因为我对任务系统的逻辑没有认知,纵然LUA性能多么好、多么小巧、语法多么简洁,但是在不理解任务系统的情景下我能保证开发出高新能的服务?纵然实现其基本功能已经是不小的挑战了,还要支持策划人员动态的添加、修改、删除任务定义等额外功能。
我渐渐意识到,许多软件的最主要的复杂性并不在技术上,而是在领域上、用户的活动或业务。如果问题域的负责性没有解决,再好的技术(LUA?LAMADA?ASYNC?MULTI_THREAD?)都是浮云。我们每个人精力都是有限的,故此则失彼,如果对IDE的学习花费太多的时间那么花在学习模式、建模上的时间必然会减少。认知超载,认知负荷理论中术语,问题解决和学习过程中各种认知活动均需消耗认知资源,若所有活动所需资源总量超过个体拥有的资源总量,就会引起资源的分配不足,从而影响个体学习或问题解决的效率,这种情况被称为认知超载!
最近一直关注DOMAIN-DRIVEN DESIGN的社区,受益匪浅。对软件以及对象技术有了新的思考,这些思考还不太成熟,但是还是用文字记录一下。
关于关联
对象之间最基本的关系就是关联,现实中对象往往是多对多的关联,但是在代码层面多对多关系是比较难维护、难理解的。如果对象A和对象B是一对一关系,那么意味着A对象包含一个B对象的引用,B对象也包含一个A对象的引用,若A对应多个B对象,那么A中就会包含一个B对象的集合(vector?set?map?hash_map?),A对象还会附加一些遍历B的方法、查找、添加的方法等。针对多队夺得关系的指导原则是添加约束尽量使其变成一对一的关系。比如公司-员工的关系可能是多对多的关系,但是由于在某一时间段某人只能在一个公司就职,这样添加period的约束,变成一对一的关系(一个公司在某个时间只有一个叫XX的员工)。DOMAIN-DERIVEN DESIGN中这样规定:
l 规定对象的遍历只有一个遍历方向
l 添加限定条件,减少多重关联
l 消除不必要的关联
Entity
一些对象是由只有他们的属性定义的,他们属性在时间跨度上往往会发生变化,但是总有些特性是不变的、是可以唯一标识这个对象的。这样的对象称之为Entity,即实体对象。例如人这个对象是实体,他的名字可以唯一标识他吗?答案是不能,他可能叫小明,同学可能给他起外号叫超级明,工作中可能有英文名Kevin,QQ账号可能叫大灰狼,名字只是人的属性,属性在时间、空间跨度上是可以变化的,但是他的身份证号码是唯一注册的,可以唯一标识这个小明这个人。当处理Entity时标识的选择至关重要,因为Entity往往涉及到序列化存储等情况,唯一标识往往影响其在序列化时的方案。
Value Object
Value Object即值对象。其只关心对象的属性,在值对象生命周期内,一般属性是不允许变化的,如果要变化,也是完全的更换value object整体而不是修改value object 部分属性。比如地址address对象包含省、市、区、街道等属性。Customer对象拥有一个address对象,该对象大部分情况是不需要修改的,即使Customer搬家了,address更换了,只需要重新创建一个address对象将老的替换掉即可。使用value object 可以对系统带来非常大的优化。比如在任务系统中,每个任务都一段描述文字description,该对象完全属于value object,每个task对象拥有一个description属性,这里完全可以使用引用而不是拷贝,由于我们限定了description不允许修改,他甚至是线程安全的。但系统中有成百万的task对象时,内存优化就彰显无遗了。实际上这种建模完全符合现实中的关系,从建模层面做到了优化,设计和开发衔接紧密,完全没有脱节。
Service
有时候对象不是一个事物,而是一系列的特殊动作。它用来协调各个对象之间的关系,一般以一个活动命名,一般它的名字会是个动词。Service应该是无状态的。在我们的任务系统中,有一个service叫做task_generator,他的职责是为user生成正确的新任务。他根据参数user的context为其生成任务,举个例子,若user首次进入系统,那么需要给其初始化一些基本任务比如说生成3个系列的强制任务。若此user是刚刚完成了一个任务,task_generator只需为其生成这个任务的后续任务即可。Task_generator只是包含一系列的动作而已,他操纵任务定义仓库、task对象、user context数据等。Task_generator是无状态的,所以多线程放完它,多user并发放完都不是问题,唯一的竞争在于任务仓库是全局的,实现时使用了读写锁。Task_generator,说白了,我们只是把一系列操纵封装成了对象。
Module
我们经常提到module,使用module的优点是什么。从第一天我们接触编程老师就告诉我们软件编程要分而治之。Module根本思想仍然是这个。Module的原则老生常谈了,高内聚,低耦合。DOMAIN_DERIVEN DESIGN 中提到Module提供了两种模型的认知方法:
l 在Module内部可以查看内部细节,而不需关系外部因素,因为Module是高内聚的
l 从module外部可以查看各个module之间的关系,而不需要考虑module的实现细节
DOMAIN_DERIVEN DESIGN也强调,module的重构比对象的重构影响大的多,所以对module的重构要谨慎,而且module应该是中度粒度的,细粒度的module往往不合适,最好保证操作和数据的维护对象在同一个module中。
在我们的任务系统中,集成了成就系统,二者是两个独立的module,唯一的联系只是用户的行为基数(打怪等)会累加到相应的任务和成就上。DB的分库分表也是一个独立的module,逻辑层维护一个task_service将逻辑层的对象和Mysql中的数据实现映射。
Question
DOMAIN-DERIVEN DESIGN中有很多指导的建模模式,由于并不是很多都在项目中经历过,很多并不是参的很透彻,目前的最大疑问是在建模阶段,对于对象的抽象有什么知道原则,尤其是当逻辑纷繁芜杂毫无头绪之时,那里是比较好的Begin?
一直以来,我都认为软件和建筑像极了,但是软件比建筑还要负责,因为软件是无形的。我的一个一直纠结的问题是为什么软件这么复杂!!
DOMAIN-DERIVEN DESIGN应该是个好的方向。引用老子的一句话:吾生也有涯,而知也无涯!
后半句更有哲理:以有涯随无涯,殆已。