“领域驱动开发”实例之旅(1)--不一样的开发模式

    听说DDD-“领域驱动开发”已经很久了,园子里面已经有不少大牛写过博文介绍,但我一直没有尝试过,直到今年公司的一个项目出现数据库移植,原来的业务逻辑都写在SqlServer的存储过程中,现在要移植到PostgreSQL中,才真切的体会到,再继续走“表驱动开发”的模式,没有好前途了。于是,花了几个星期,来实践一下领域驱动开发这种开发模式。

     征得《领域对象驱动开发:来吧,让我们从对象开始吧》原文作者的同意,我选择文中的“超市收银”业务场景,开发了一个“超市管理系统”--PDF.NET SuperMarket MIS,这里是下载地址。本系列将会讲解这个开发过程的有关技术细节,但作为这个系列的开篇,还是先说说领域驱动开发这个开发模式给我的不一样感觉的地方。

 

领域驱动开发模式

     一、分析业务需求。

    超市管理系统包括收银管理,商品管理,设备管理,雇员管理,客户管理等几部分,其中收银管理包括收银员管理,收银机管理,收银台管理;商品管理包括商品基本信息管理,商品存货信息管理;设备管理、雇员管理和客户管理都是辅助的,比较简单,系统的核心还是“收银过程”,注意是“过程”而不是“管理”,说到管理很容易落入“管理系统”的思路,说“过程”更容易跟业务场景,业务用例,业务流程等结合起来。

    收银业务场景:

    顾客选购商品之后,来到收银台,收银员检查扫描商品,收银机显示商品的价格清单,收银员通知客户货物总价格,客户确认,付款,完成收银。

   收银业务用例:

   这里不严格区分“场景”和“用例”,我觉得“用例”是更加技术化的词汇。在该用例中,有一些角色,如顾客,收银员,收银机,还有一些对象,如商品,购物车,价格单,有一些活动,如扫描商品(读条码),计算价格,付款。

   收银业务流程:

   大家都去过超市,流程简单来说很简单,但专业来说还是很复杂,这里我不班门弄斧了。

 

    二、设计领域对象模型

    在《领域对象驱动开发:来吧,让我们从对象开始吧》一文中,作者已经给出了领域对象模型,这里也不在重复,不过我设计的模型与原作者有点细微差别,这个以后再说。

 

    有了DomainModel,在系统进入全面开发之前,就可以测试DomainModel,从而验证系统的核心逻辑设计是否合理。

    三、测试领域对象模型

    为什么要这一步?因为我们经过前面的业务分析之后,得到了我们的领域对象模型,但我们的理解是否正确呢?为了验证我们的理解是否正确,需要对第二步中的模型进行测试,看它是否正确,是否合理。

 

 static void TestModel()
        {
            
//http://www.cnblogs.com/assion/archive/2011/05/13/2045253.html

            
//我们创建几样商品
            GoodsStock RedWine = new GoodsStock() { GoodsName = "红酒", GoodsPrice = 1800, GoodsNumber = 10 };
            GoodsStock Condoms 
= new GoodsStock() { GoodsName = "安全套", GoodsPrice = 35, GoodsNumber = 10 };

            
//我们创建几位顾客
            Customer Chunge = new Customer() { CustomerName = "春哥" };
            Customer Beianqi 
= new Customer() { CustomerName = "贝安琪" };
            Customer Noname 
= new Customer();

            
//有一台收银机
            CashierRegisterMachines crManchines = new CashierRegisterMachines() { CashRegisterNo = "CR00011" };
            
//当然,我们需要收银员啊
            Cashier CashierMM = new Cashier(crManchines) { CashierName = "收银员MM", WorkNumber = "SYY10011" };

           

            
//顾客开始排队结帐了
            Queue<Customer> customerQueue = new Queue<Customer>();
            customerQueue.Enqueue(Chunge);
            customerQueue.Enqueue(Beianqi);
            customerQueue.Enqueue(Noname);

            
//队伍过来,按先后顺序挨个收银喽
            foreach (var customer in customerQueue)
            {
                
//收银
                CashierMM.CashRegister(customer);
            }

        }

 

    四、设计业务处理类

    我们在第三步编写领域对象模型的测试案例的时候,实际上就是针对业务场景的测试,这个处理业务场景的代码差不多就是“业务处理类”--BIZ class,我们把它提取出来,在完善下,就得到真正的业务处理类了。这个业务处理类后续还会一直完善的,但这里已经基本成型了。

 

    五、设计Entity和ViewModel

    在完善业务处理类的时候,我们需要分析哪些领域对象的属性需要持久化,注意不要单个的去分析领域对象,而要根据整个领域对象模型去分析,比如可能有两个领域对象会使用一个持久化属性的,这个时候我们应该考虑将这个属性放到一个实体对象中,这样我们就得到了系统需要的实体类(Entity);分析哪些领域对象的属性可能是需要给用户界面(View)使用的,同样的原因,可能会组合多个领域对象的属性给一个用户界面,这样我们就得到了ViewModel。

    说简单点,Entity 和ViewModel 都依赖于 BIZ class ,BIZ class调度DomainModel,使用或产生Entity和ViewModel。

    BIZ就像业务用例,它组合MomainModel的调用,我这里这个Biz也许更像Service。系统只有Entity会和数据库打交到。

 

Entity

 

ViewModel

      六、测试业务处理类

    我们已经在第三步中测试了领域对象模型,当时的数据都是模拟的,没有使用数据库,现在我们编写一些测试案例来进行真正的测试了。测试案例可以使用VS自带的单元测试来做,也可以编写专门的测试项目,或者直接编写简单的测试页面。由于“领域对象模型”已经测试过,所以这一步的测试我们的业务操作类是否能够正确的管理领域对象,能够生成ViewModel等。

 

static void TestBIZ()
        {
            
//我们创建几样商品
            GoodsStock RedWine = new GoodsStock() { GoodsName = "红酒", GoodsPrice = 1800, GoodsNumber = 10, SerialNumber ="J000111" };
            GoodsStock Condoms 
= new GoodsStock() { GoodsName = "安全套", GoodsPrice = 35, GoodsNumber = 10, SerialNumber ="T213000" };

            
//我们创建几位顾客
            Customer Chunge = new Customer() { CustomerName = "春哥" };
            Customer Beianqi 
= new Customer() { CustomerName = "贝安琪" };
            Customer Noname 
= new Customer();

            
//有一台收银机
            CashierRegisterMachines crManchines = new CashierRegisterMachines() { CashRegisterNo = "CR00011" };
            
//当然,我们需要收银员啊
            Cashier CashierMM = new Cashier(crManchines) { CashierName = "收银员MM", WorkNumber = "SYY10011" };

            
//顾客逛了一圈,选了自己想要的商品
            Chunge.LikeBuy(RedWine.TakeOut(1));
            Beianqi.LikeBuy(RedWine.TakeOut(
1));
            Beianqi.LikeBuy(Condoms.TakeOut(
1));
            Noname.LikeBuy(Condoms.TakeOut(
2));

            
//调用收银业务类
            CashierRegisterBIZ biz = new CashierRegisterBIZ(CashierMM ,crManchines);
            biz.AddQueue(Chunge);
            biz.AddQueue(Beianqi);
            biz.AddQueue(Noname);

            biz.CashierRegister();


        }

 

    七、设计表架构 

    有了Entity对象,很自然的就可以得到特定数据库系统的创建表的脚本了。超市管理系统使用了PDF.NET框架的实体类,实体类的属性和表的字段映射关系非常清楚,因而可以直接从实体类得到创建表的脚本。运行系统的建表脚本,这样我们的数据库就建好了,系统已经可以运行了。

 

 

 八、开发用户界面

    终于等到这一步了,好多时候我都想直接跨过前面7个步骤,先做这一步的,但为了实践“领域驱动开发”模式,还是坚持了下来。系统使用ASPX页面作为用户界面,在这一步中,根据要展现的功能,设计对应的页面,调用BIZ class得到ViewModel,将它绑定到页面上。如果说M(BIZ class),V(ASPX页面),VM(ViewModel),这种模式是不是很像传说中的MVVM呢?

 

下面是系统的有关用户界面:

会员登录页

 

顾客购物首页

 

表驱动开发模式

    这是我以前以及我们公司现有项目,还有很多公司做项目的开发模式,详细说说我们公司最近一个项目的开发过程吧:

    一、分析需求,制作静态页面作为Demo

    需求人员守着美工制作人员,一个个模块,一个个功能的制作好所有的静态页面,作为系统的Demo。这个过程很长,需求人员常常会反复的修改需求,然后让美工修改界面,指示这些界面反应的系统功能。以下简称这些静态页面为Demo页面。

    二、从Demo页面熟悉系统功能

    开发人员、需求人员、美工制作人员一起来看这些Demo页面,开发人员提问,需求人员解答,如果开发人员提出质疑和不合理之处,再由美工去修改。当然,功能上开发人员是无权否定和修改的,只是第功能的布局和展现方式提出意见,方便程序开发实现。这个过程耗费的时间也很长,通常一个个页面的过,而且开发人员事先已经有了分工,每个人负责一个模块,听到自己负责的模块的时候,就打起精神来听,遇到跟自己不相关的模块,也就是滥竽充数而已,在会议室耗时间。

    三、进行表设计

    这个过程有DBA主导,每个模块的负责人和DBA一道,根据Demo页面上面展现的功能、表单、表单域,来设计这个模块相关的表和表的字段。这个过程也要耗费较长的时间,主要纠结于该用什么英文名称来对应表字段,和太多的表以及表字段的含义、类型。

    四、开发页面,设计存储过程

    开发人员按照前期的Demo页面,用ASP.NET来实现一遍,主要的工作就是写好多JS代码,来动态调用后台数据。而DBA就将数据工作全包了,为开发人员编写一个个的SQL函数,输出每个页面用到数据,为了对接方便,输出的数据字段名称用的是中文;程序的功能,也就是所谓的业务逻辑,就由DBA全部写在存储过程中了。DBA专心写存储过程,使用SqlServer 2008上的那些最酷的特性;开发人员专心做ASPX页面,BLL层只是一个传声筒,DAL层已经由PDF.NET的代码生成器自动生成了,不用开发人员操心,只是问问DBA这个功能的SQL该怎么写而已。DBA乐得专心,开发人员乐得简单(虽然是体力活),这样大家都HAPPY 。

    五、测试

    等开发人员开发万所有的模块,DBA写完了所有的存储过程,测试人员终于上阵了。一阵测试下来,Bug不少,主要是数据不对,功能实现跟需求有差异,大家又手忙脚乱的开始改Bug了。我们的那个项目开发用了2个月,而测试改Bug花了4个多月啊,有些同事受不了测试人员和需求人员的“轮番攻击”只好走人了,大家都盼望着快点结束这个项目,太累了。

 

    项目开发完成了,有人问我们的系统有没有业务模型,大部分都不置可否,说到“去看数据库吧”;问开发技能有没有提高,答案是对JS更熟悉了,其它的就没有了;再问你们的开发文档怎么样,回道“开发文档早就没用了”,有问题直接去看代码吧。

    一个项目一个项目就是这么开发的,年复一年,日复一日,大家的工作就是写代码,改Bug,没有什么改变。

 

 两种开发模式的区别

    下面,回过头来看看“领域驱动开发”模式,有什么不一样的地方:

  1.     领域驱动注重“领域对象模型”的设计,可以先设计,再测试,最后才开发;
  2.     领域驱动能够产生系统的核心价值--“领域对象模型”;
  3.     领域驱动使得整个开发过程更容易关注系统的重点功能,使得“有的放矢”;
  4.     领域驱动无需重点关注数据问题,使得系统跨数据库移植非常容易;
  5.     领域驱动更关注“业务”,而不是“数据本身”,适合业务非常复杂的场景;
  6.     领域驱动更关注“业务对象”,从而能够使用各种设计模式,架构模式,使得系统更容易扩展和优化。

 关于这点,在我们现有系统中深有体会,由于所有业务逻辑的写到了存储过程中,而现在系统运行效率比较低下,在不改变硬件的前提下,想优化的空间都没有。

 

    当然,表驱动开发模式并发一无是处,它比较适的情况是:

  1.     开发团队的整体设计能力欠缺;
  2.     项目的业务不是很复杂,不经常变更业务功能;
  3.     以数据为中心,数据在项目中具有核心价值;
  4.     有很强的DBA团队

 

   通常很多项目业务也比较复杂,也不是以数据为中心,也没有很强的DBA团队,但仍然选择“表驱动开发模式”,我想主要原因应该是“ 开发团队的整体设计能力欠缺”,而项目或产品“设计的好坏”是直接影响项目或产品的“成本”,甚至是“成败”的。“设计应对变化”,看你是否认同了。

 

    原来的表驱动开发模式,只会傻傻的根据页面的DEMO,得知应该有哪些表和字段,很难分析出中间的复杂业务对象和相关联的业务流程,做出来的程序每个部分都是严重“割裂”的!
    领域驱动开发模式,是先分析需求,得到领域模型,然后和业务一起验证该模型,逐步改善完善模型,第二步是实现业务场景,得到哪些领域对象的属性是需要持久化的,得到哪些组合的属性是需要给前端显示的(ViewModel),第三步才是设计View,使用ViewModel,设计实体类,最后才开始开发用户界面。

    作为这个系列的开篇,先和大家探讨一下领域驱动开发模式与传统表驱动开发模式的不一样之处,这里写的是我的一点感悟,由于是理论性质的,所以将“超市管理系统”的实例放到下篇讲解。

 

posted on 2011-06-24 12:07 深蓝医生 阅读(...) 评论(...) 编辑 收藏

导航

公告