关于DDD的模型选择,应该是在05年的时候,从充血模型转换到贫血模型,那时候的资料太少,自己是通过项目体会出来的,架构经过这些年的升级改进,从模型方面这一块基本应该是不再有大的变化了。至少这些年的这么多项目,用起来非常顺手,从分析、设计、编码一路映射下来,现在又加个工作流、静态图,也只是对框架的完善。
我说说自己的理解。
//---------------------------------------
说DDD,先上标准的图和解释:
1. 用户界面/展现层
负责向用户展现信息以及解释用户命令。更细的方面来讲就是:
a) 请求应用层以获取用户所需要展现的数据;
b) 发送命令给应用层要求其执行某个用户命令。
2. 应用层
很薄的一层,定义软件要完成的所有任务。对外为展现层提供各种应用功能(包括查询或命令),对内调用领域层(领域对象或领域服务)完成各种业务逻辑,应用层不包含业务逻辑。
3. 领域层
负责表达业务概念,业务状态信息以及业务规则,领域模型处于这一层,是业务软件的核心。
4. 基础设施层
本层为其他层提供通用的技术能力;提供了层间的通信;为领域层实现持久化机制;总之,基础设施层可以通过架构和框架来支持其他层的技术需求。
//--------------------------------------
1、界面层:就是获取显示数据,把数据发送到服务端,并告诉服务端这些数据干什么用。
2、应用层?我有些不理解:作用是什么呢?存在价值是什么?如果用的是充血模型,如果对客户端通信的话,可以在此解耦,以值类型实现远程通信,包括SOA的支持等。如果想走SOA,充血模型应该是行不通的,特别是如果用的ORM后,可能有的是动态类,序列化与反序列化,都是一个大问题。如果是贫血模型,这一层好像是多余的,或者也有是为了SOA或者其他之类的服务,或者技术难题,这是可以理解的。如果是作为流程编排层,那么与应用层的定义就有差异了。在我的框架里面,就不存在应用层,但是存在流程编排层,但不是作为独立的层次存在,从有篇随笔上已经有表现了。
3、领域层:我想可以理解为业务逻辑层。如果说一定要纯粹的OO,一个类一定要有属性、方法才是完整意义上的OO,那好像有些太本本主义了。重要是要好用,能够方便的实现问题的解决。 如果以Service对外公开服务也未偿不可,或者说方法,方法分几4种,1是public对客户端公开的粗粒度的服务,相当于SOA用到的服务;2是internal对DLL公开的内部可以访问的服务,对模型内有效;3是protected 对继承的对象有效;4是private只对内部有效。组合起来,应该能够实现领域层的要求,不足就是本来一个对象一个类可以实现的东东,硬生生把人分家。有人提到公开了增删改的操作,没有必要公开,多余了,其实不就是4个操作嘛,也差不多哪里去了。没有十全十美的方法,有缺点,也有优点,可以直接对客户端提供服务,统一模型。
4、基础设施层:可能更多的是ORM,持久化之类的吧。当然会有一些通用服务等等。
还是一句话,没有十全十美的。不同的理解会造就不同的模型,产生可能相当大的差异。
上一段代码,看看我的理解:
1、实体对象。 这是一个主从结构 ,从下面的代码上面已经能够明细看的出来了
namespace eWMS.Data { using System; using System.Collections.Generic; using EES.Common; using EES.Common.Data; using EES.Common.Model; using System.ComponentModel; [EESData("移库单")] [Contract()] public class MOVHeader : EESObject { #region 字段属性 private String id; private String corpCode; private String code; private String moc; private String orderStatus; #endregion #region 映射属性 ////// Id /// public virtual String Id { get { return id; } set { if ((id != value)) { this.OnPropertyChanging("Id"); id = value; this.OnPropertyChanged("Id"); } } } /// /// 企业编码 /// public virtual String CorpCode { get { return corpCode; } set { if ((corpCode != value)) { this.OnPropertyChanging("CorpCode"); corpCode = value; this.OnPropertyChanged("CorpCode"); } } } /// /// 单号 /// [Key()] public virtual String Code { get { return code; } set { if ((code != value)) { this.OnPropertyChanging("Code"); code = value; this.OnPropertyChanged("Code"); } } } /// /// 纸面单据号 /// public virtual String Moc { get { return moc; } set { if ((moc != value)) { this.OnPropertyChanging("Moc"); moc = value; this.OnPropertyChanged("Moc"); } } } /// /// 单据状态 /// public virtual String OrderStatus { get { return orderStatus; } set { if ((orderStatus != value)) { this.OnPropertyChanging("OrderStatus"); orderStatus = value; this.OnPropertyChanged("OrderStatus"); } } } #endregion private DataCollection detailCollection; public virtual DataCollection DetailCollection { get { return detailCollection; } set { detailCollection = value; } } private DataCollection movDataCollection; public virtual DataCollection MOVDataCollection { get { return movDataCollection; } set { movDataCollection = value; } } } }
1 namespace eWMS.Data 2 { 3 using System; 4 using System.Collections.Generic; 5 using EES.Common; 6 using EES.Common.Data; 7 using EES.Common.Model; 8 using System.ComponentModel; 9 10 11 [EESData("单明细")] 12 [Contract()] 13 public class MOVDetail : EESObject 14 { 15 16 #region 字段属性 17 private String id; 18 19 private String corpCode; 20 21 private String ordCode; 22 23 private Int32 rowNo; 24 25 private String rowStatus; 26 27 28 #endregion 29 30 #region 映射属性 31 ///32 /// Id 33 /// 34 [Key()] 35 public virtual String Id 36 { 37 get 38 { 39 return id; 40 } 41 set 42 { 43 if ((id != value)) 44 { 45 this.OnPropertyChanging("Id"); 46 id = value; 47 this.OnPropertyChanged("Id"); 48 } 49 } 50 } 51 52 /// 53 /// 公司编码 54 /// 55 public virtual String CorpCode 56 { 57 get 58 { 59 return corpCode; 60 } 61 set 62 { 63 if ((corpCode != value)) 64 { 65 this.OnPropertyChanging("CorpCode"); 66 corpCode = value; 67 this.OnPropertyChanged("CorpCode"); 68 } 69 } 70 } 71 72 /// 73 /// 到货单号 74 /// 75 public virtual String OrdCode 76 { 77 get 78 { 79 return ordCode; 80 } 81 set 82 { 83 if ((ordCode != value)) 84 { 85 this.OnPropertyChanging("OrdCode"); 86 ordCode = value; 87 this.OnPropertyChanged("OrdCode"); 88 } 89 } 90 } 91 92 /// 93 /// 行号 94 /// 95 public virtual Int32 RowNo 96 { 97 get 98 { 99 return rowNo; 100 } 101 set 102 { 103 if ((rowNo != value)) 104 { 105 this.OnPropertyChanging("RowNo"); 106 rowNo = value; 107 this.OnPropertyChanged("RowNo"); 108 } 109 } 110 } 111 112 /// 113 /// 行状态 114 /// 115 public virtual String RowStatus 116 { 117 get 118 { 119 return rowStatus; 120 } 121 set 122 { 123 if ((rowStatus != value)) 124 { 125 this.OnPropertyChanging("RowStatus"); 126 rowStatus = value; 127 this.OnPropertyChanged("RowStatus"); 128 } 129 } 130 } 131 132 133 #endregion 134 } 135 }
namespace eWMS.Data { using System; using System.Collections.Generic; using EES.Common; using EES.Common.Data; using EES.Common.Model; using System.ComponentModel; [EESData("编码")] [Contract()] public class MOVData : EESObject { #region 字段属性 private String id; private String ordCode; private String detailId; private String productCode; private String productSKU; private String code; #endregion #region 映射属性 ////// Id /// [Key()] public virtual String Id { get { return id; } set { if ((id != value)) { this.OnPropertyChanging("Id"); id = value; this.OnPropertyChanged("Id"); } } } /// /// 单号 /// public virtual String OrdCode { get { return ordCode; } set { if ((ordCode != value)) { this.OnPropertyChanging("OrdCode"); ordCode = value; this.OnPropertyChanged("OrdCode"); } } } /// /// 明细Id /// public virtual String DetailId { get { return detailId; } set { if ((detailId != value)) { this.OnPropertyChanging("DetailId"); detailId = value; this.OnPropertyChanged("DetailId"); } } } /// /// 产品编码 /// public virtual String ProductCode { get { return productCode; } set { if ((productCode != value)) { this.OnPropertyChanging("ProductCode"); productCode = value; this.OnPropertyChanged("ProductCode"); } } } /// /// 产品SKU /// public virtual String ProductSKU { get { return productSKU; } set { if ((productSKU != value)) { this.OnPropertyChanging("ProductSKU"); productSKU = value; this.OnPropertyChanged("ProductSKU"); } } } /// /// 追踪码 /// public virtual String Code { get { return code; } set { if ((code != value)) { this.OnPropertyChanging("Code"); code = value; this.OnPropertyChanged("Code"); } } } #endregion } }
2、服务:或者说方法。
namespace eWMS.Service { using System; using System.Collections.Generic; using EES.Common; using EES.Common.Data; using EES.Common.Model; using EES.Common.Query; using EES.Common.Injectors.Handlers; using eWMS.Data; using T.BC.Service; [EESBO("移库单服务")] public class MOVHeaderService : MOVHeaderImp{ [Operation(ScopeOption.Required)] public virtual void WriteTrans(MOVHeader h) { trans.Id = GUID.NewGuid(); trans.OrderCode = h.Code; trans.OrderType = typeof(MOVHeader).ToString(); trans.ProductCode = detail.ProductCode; trans.ProductSKU = detail.ProductSKU; trans.BatchCode = detail.BatchCode; trans.StoreCode = detail.StoreCode; trans.StoreZone = detail.StoreZone; trans.Qty = 0; trans.UOM = detail.UOM; trans.RowNo = detail.RowNo; trans.PackCode = detail.PackCode; getProxyTrans().Save(trans); } protected override void OnSaved(MOVHeader t) { getProxyMovDetail().SaveAll(t.DetailCollection); getProxyMovData().SaveAll(t.MOVDataCollection); base.OnSaved(t); } private MOVDetailService proxyMovDetail; private MOVDetailService getProxyMovDetail() { if (proxyMovDetail == null) proxyMovDetail = Factory.getProxy (); return proxyMovDetail; } private MOVDataService proxyMovData; private MOVDataService getProxyMovData() { if (proxyMovData == null) proxyMovData = Factory.getProxy (); return proxyMovData; } private OrderTransactionService proxyTrans; private OrderTransactionService getProxyTrans() { if (proxyTrans == null) proxyTrans = Factory.getProxy (); return proxyTrans; } } }
为什么叫方法或者服务呢,结合框架这些方法可以直接公开出来,可以远程访问的,不需要加额外的编码,另外流程引擎也可以直接调用这些服务的,然后流程引擎会组合成新的服务对客户端公开。对于事务的处理,有[Operation(ScopeOption.Required)]这一句就可以了,也可以通过配置文件加上去。异常可以通过服务直接抛到客户端,如果在事务内则整个参与事务的所有服务的事务全部回滚,可能会涉及到多个服务的嵌套调用以及递归调用,全部会回滚,所涉及到的数据库连接会自动释放掉,缓存清理掉。
3、客户端调用:已经到了界面显示层。对于Web客户端与Form客户端的调用一样,已经到了界面显示层了。
namespace eWMS.Forms { using System; using System.Collections.Generic; using System.Text; using System.Drawing; using System.Windows.Forms; using EES.Common; using EES.Common.Data; using EES.Controls.Win; using EES.Common.Query; using EES.Common.MVC; using eWMS.Data; using eWMS.Service; [View("移动类型管理", EntryType.入口, "移动类型", "移动类型管理")] public sealed partial class BillTypeLForm : EForm { private BillTypeService proxy; public BillTypeLForm() { this.InitializeComponent(); // 绑定数据错误不提示 this.gridView.DataError += GrivViewDataError; this.gridView.CellValidated += GrivViewCellValidated; // 数据源 this.gridView.DataSource = this.bindingSource; // this.bindingSource.AddingNew += AddingNew; this.Input = new DataCollection(); DataLoad(); } private BillTypeService getProxy() { if(proxy==null) proxy=Factory.getProxy () ; return proxy; } private void AddingNew(object sender, System.ComponentModel.AddingNewEventArgs e) { BillType obj = Factory.Create (); // 更多初始化 // e.NewObject = obj; } /// /// 刷新 /// [Func("刷新", Ordinal=10)] public void DataLoad() { this.Input = this.getProxy().FindAll(); } [Func("删除",Ordinal=35)] public void DataDelete() { BillType data = this.Current as BillType; if (data == null) throw new EESException("请选择要删除的数据"); if (MessageBox.Show("确定要删除数据吗?", "删除数据", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == System.Windows.Forms.DialogResult.OK) { if (data.DataState != DataState.Created) { this.getProxy().Delete(data); } data.DirectRemove(); MessageBox.Show("删除 成功"); } } [Func("保存", Ordinal=30)] public void DataSave() { object data = this.Input; if (data is DataCollection ) { this.Input = this.getProxy().SaveAll(((DataCollection )(data))); MessageBox.Show("保存 成功"); } } /// /// 查询 /// [Func("查询", Ordinal = 40)] public void DataFind() { BillTypeFForm form = new BillTypeFForm(); form.OnOK += delegate(object sender, TEventArgs > e){this.Input=e.Data;}; form.ShowDialog(); } } }
还没有说完,后面再继续吧
有不到之处,请大家批评指正……
谢谢!
补充:
关于对象模型,是一个可以上下追溯的递归结构,支持级联触发绑定的。根对象可以通过关联关系访问下级对象,在我的对象模型里面,底层的对象也仍然可以访问到上级的父对象,从而访问其他的兄弟对象。级联触发,则可以实现最底层对象的属性或增删,会冒泡一路通知到上顶层对象。在远程传输的时候,仍然可以记录数据状态的变化,传输过程中丢弃变化之前的数据,但会传输已经删除了的记录,服务端再作进一步的处理,这些过程都是自动实现的。
举个例子,就按上面客户端的调用来说。
[Func("保存", Ordinal=30)] public void DataSave() { object data = this.Input; if (data is DataCollection) { this.Input = this.getProxy().SaveAll(((DataCollection )(data))); MessageBox.Show("保存 成功"); } }
当在界面上删除一条记录后,后台会自动记录已经删除了的记录,如果绑定的数据变化了,则会冒泡通知顶层对象的状态发生了变化,会由Unchanged->Modified。调用this.Input的时候,会把界面的数据写入内存里去,默认情况下是自动处理,也可以人工修改代码的逻辑。通过绑定处理,则会大大降低代码的差错率。
private BillTypeService proxy; private BillTypeService getProxy() { if(proxy==null) proxy=Factory.getProxy() ; return proxy; }
Proxy则是统一了服务端调用与客户端调用。当为客户端调用的时候,会自动加载客户端的配置文件,调用远程的服务;如果是服务端调用,则会创建ProxyClass,实现方法的调用。
[Func("保存", Ordinal=30)]
是为了实现基本菜单的配置,流程相关菜单通过流程引擎定义,在没有流程引擎之前,是通过这种方式分功能权限的。
客户端的代码量还是比较小的,UI的主要目的就是数据的显示与获取,交与服务端处理业务逻辑。当然人性化,友好就是另一回事了,对于模型来说没有影响。
对于DDD的领域工作单元,有点类似把服务或方法的组合起来。我的做法是一层对一层负责,比如:订单服务,对外的接口就是订单,因为所有明细的状态变化都会影响到订单抬头,提交的时候是以订单为单元的,当然,所以变化的信息也会一并提交;订单服务可以调用订单明细的服务,但不会调用订单明细分配数据的服务,订单明细会调用订单明细分配数据的服务,任务可以一层层分解,通常每一层的都不是非常复杂。中间服务调用的时候,类似于普通的调用,数据增删改查,可以一并处理,并且会保证事务的一致性,单数据库与多数据库统一处理。并且可以通过配置提供远程服务,并不需要额外的代码。
这是我的做法,不能说是对的,框架用了不少年了,只能说用起来挺顺手的,项目中测试的工作量非常小,很少会出现BUG,另外,需求信息的传递应该还是非常完整的,只要流程出来了,开发人员能够很快的实现代码。很多时候是分析人员的速度跟不上开发人员的速度。