首页 > 技术文章 > 由单元测试发现的设计问题

saaav 2014-06-25 13:44 原文

  问题背景:采用DDD设计开发的公司内部业务审批系统,架构分层大概上就是经典DDD的架构,其中应用层使用AutoMapper对聚合和DTO做映射,聚合的大致模型:

  

  问题定位:问题发生在应用层,通过映射产生审批单聚合时,审批单聚合中最下层(如上图中业务关系明细)用于与其上层对象(如上图中业务信息)关系的唯一标识与预期不符,依旧是命名因为我的偏执,会有一点问题,知道就成了。

        public AllographRequisitionData Save(AllographRequisitionData requisitionData)
        {
            DataObjectConvert convert = new DataObjectConvert();
            AllographRequisition requisition = convert.DTOToEntity(requisitionData);
            RequisitionService requisitionService = new RequisitionService();
            requisitionService.Create(requisition);
            return requisitionData;
        }

  上面代码就是单元测试未通过的代码,方法本返回值本应该是执行是否成功,不过为了检查问题,暂时放了DTO还没改回来,未通过检查的是Requisition聚合实例requisition,其中DTOToEntity()方法是适用AutoMapper进行映射的方法。

   问题原因: 聚合内部关系通过ID实现,由于最初考虑聚合模型比较简单,完全可以由聚合内部维护组装逻辑---即聚合内部实体的关系。但是应用层将聚合实体与DTO做映射时,聚合自身创建的ID被测试用例中DTO携带的ID覆盖了,这样额外产生了聚合内部一致性被应用层侵入的问题。

  问题解决有两种办法。一种是打补丁似的哪疼补哪,这种方法很简单,通常大多数程序员因为受限于经验可能发现不了或懒不愿意分析问题更深层次的原因,会选择这样的方法,只要在映射时忽略了ID就可以了,只要在映射时加上一句代码就可以解决了,这种方法隐式的使业务和映射产生了耦合。

.ForMember(target => target.RequisitionContentID, opt => opt.Ignore())

  好了,以上只是引子,我要说的其实是这个问题本就不应该存在。因为在查找这个问题的过程中,我调试了上面贴的Save方法单元测试中的两个部分:一个是聚合根构造函数产生聚合内部各对象间关系时,也就是产生ID,并分发给聚合内各个实体实例的部分;另外一个是自然是DTO到聚合映射的部分。

  但是很明显,我明明是一个聚合内维护内部关系的功能产生了问题,却同时必须关心另外一个本应毫不相干的地方,这已经说明了问题;而且映射方法的职责只是映射,不应该有额外对聚合逻辑关系的维护的职责。

  显然,由聚合跟来负责组装聚合,维护连接聚合内各部分的关系在这里不太合适,那就只好不由聚合来维护了。DDD中创建聚合有两种方式,另外一种是工厂,其实DTO映射出来的结果从概念上来所仅仅是聚合应当持有的数据,而并不是真正的完整的聚合,所以虽然Requisition实例已经产生,但聚合并没有完全形成。工厂一般有两个存在位置,一个是聚合根,一个是领域服务。这里为了工厂之后可以被仓储的重建过程调用,将工厂放在领域服务中。

  下面代码的return就当没看见吧,暂时没起作用。

        public bool Create<TRequisition, TRequisitionContent, TBusinessContent>(RequisitionBase requisition)
            where TRequisition : RequisitionBase
            where TRequisitionContent : RequisitionContentBase
            where TBusinessContent : BusinessContentBase
        {
            bool isNotNull;
            ValidateHelper.IsNotNull(requisition, out isNotNull);
            RequisitionFactory build = new RequisitionFactory();
            build.BuildRequisitionID(requisition);
            RequistionRepository repository = new RequistionRepository();
            repository.Add<TRequisition, TRequisitionContent, TBusinessContent>(requisition as TRequisition);

            return true;
        }

  如是,就可以避免出现上面的问题,虽然说做设计总不可能方方面面、上上下下(不知道为什么想到了日日夜夜)、从头到尾全部一次性考虑完善,修改、重构、迭代一定是会存在的,但我很少写项目的代码,不知道以往的设计中这种程序员难以注意但有很大隐患的设计问题会存在多少,所以以此为警醒,一定要对设计做回归测试,有条件的情况下可以配合程序员进行开发,然后就是,认真、仔细、对所有设计部分尽量考虑清楚、对扩展和可能的使用方式进行估计,有条件可以做些测试。

推荐阅读