我的思考:领域建模应当是开发人员/架构师需要加强的能力,通过领域建模对涉及的业务领域有更深入的了解,同时合理的建模,确保业务逻辑内聚,使企业应用更易于维护和迭代。
这方面的理论知识可以参考Eric Evans的《领域驱动干设计-软件核心复杂性应对之道》,实践相关的内容可以参考Vaughn Vernon的《实现领域驱动设计》,也可以参考我的系列博客【DDD】使用领域驱动设计思想实现业务系统。
初学者在实践DDD的时候,首先需要改变思维方式,业务领域的分析和建模是关键,通过不断的实践总结,形成自己的一套完整的建模战术。
另外,DDD对于复杂性较高的应用系统优势更加明显,我们团队在用户系统和社区系统都进行了DDD的实践,发现相比用户系统,DDD在社区系统的优势发挥的更充分。
最后,DDD需要不停地实践,不要追求一步到位,模型可以不断地迭代完善,DDD的实践也是如此。
项目实践:在用户体系和社区服务系统中均有实践,可以参考我的系列博客【DDD】使用领域驱动设计思想实现业务系统。
标识域模式概要:为了在内存对象和数据库之间维护标识而在对象内保存的一个数据库标识域。标识域满足两个特性:唯一性、不可变性。
我的思考:在数据库中通常存在两种类型的唯一且不可变键,一个是业务主键,一个物理主键,那么应当使用哪个主键来作为标识域呢?
可以根据DDD中介绍的实体和值对象来做区分,如果是实体,那么建议是用业务主键,比如“User”和“Order”,分别可以使用userNo和orderNo来标识。而对于值对象,可以直接使用其物理主键作为标识域,比如“用户点赞信息”,可以使用物理主键id作为标识域,当然也可以使用业务联合主键(userNo和postNo)作为标识域,但是会增加复杂度,不可取
另外,通常情况下,我们可以将物理主键命名为以Id结尾,将业务主键命名为No结尾;
很多服务场景,需要将实体的标识域暴露给调用方,就要考虑安全性问题,如果你的标识域是顺序递增的long型主键,那么很可能会被攻击者遍历,从而带来一些安全风险,这时候可以做如下两种考虑:标识域不再使用顺序递增的long型主键,而是使用不可遍历的uuid等;如果没法将标识域更改为uuid,那么考虑新建一个域,存储专门供外部使用的uuid值。比如:我们在用户系统中便为User创建了一个使用uuid值的UnionId字段。
不建议前后端将标识域明文传递,尤其是越权访问会带来数据泄露问题的场景,比如:查询用户信息,这时候实体的标识域应当考虑从会话中获取,避免越权访问带来的数据风险
项目实践:订单系统中,我们使用业务主键orderNo作为Order实体的标识域,且由于orderNo形式为:yyyyMMddHHmmssSSS+sequence,被遍历的成本非常高,因此直接暴露在外使用。
外键映射模式概要:把对象间的关联映射到表间的外键引用。
我的思考:外键映射适用于:1:1及1:N的关联关系,通常让非root实体持有root实体的标识域,比如唱片持有作者的标识域,曲目持有唱片的标识域。
项目实践:社区系统中的“帖子”实体持有“用户”实体的标识域,在数据库中则表现为Post表持有一个userNo字段。
关联表映射模式概要:把关联保存为一个表,带有指向表的外键。
我的思考:外键映射适用于:N:N的关联关系,关联表通常对应一个值对象。关联表通常存在两个方向的查询入口,这两个入口跟关联表外键对应的实体表有关,那个在DDD中,该关联表就可以同时属于两个“聚合”中。比如用户体系系统中“用户账户关系表”(UserAccount),作为值对象,持有userNo和accountNo;存在根据userNo查询accounts的场景,也存在accountNo查询UserAccount的场景;可以看出UserAccount属于User和Account这两个“聚合”中。
项目实践:用户体系系统中“用户账户关系表”,作为值对象,持有userNo和accountNo。
单表继承模式概要:将类的继承层次表示为一个单表,表中各列代表不同类中的所有域。
我的思考:书上列出了如下优点:只需关注一张表,对象继承层次更改时无需更改存储层;缺点也很明显:数据库空间浪费,维护成本增加,可扩展性差。实际的项目经验表明,最好少用这种模式,比如在社区系统中,多个渠道发布的评论属性不一样,开发人员将多个渠道的评论属性整合放入到一张表中,且文档注释不全,导致后续开发人员踩坑,在写程序时不清楚每个字段的实际适用渠道,维护起来非常麻烦。而且后续某些渠道下线后,该大表中多余字段仍然保留,造成了极大的空间浪费。
项目实践:社区系统中的评论表,存储来自多个渠道的评论内容,且缺乏注释,业务代码混乱,导致难于维护。
类表继承模式概要:将各个子类的公共属性放入一张父表中,子类的非公共属性放入各自的子表中。