领域模型是OO分析中最重要的和经典的模型。领域驱动设计(DDD)则是有效的软件复杂性的应对之道。
领域模型其实是一种语言,领域专家与分析人员、开发人员之间交流的通用语言。 一开始,分析人员与领域专家需要对这个通用语言达成一致,双方能熟练的运用领域模型描述问题,表达、分析、处理问题。
1. 领域模型不是图,图只是让核心、关键的概念清晰的呈现出来。图的表达能力有限,模型必须配备描述(需求采集会议中的口头描述,或文档中的文字描述),将图形所代表的意义,以及图形中没有呈现出来的规则、断言、细节进行补充,才能完整地表述需求。
2. 领域模型的UML或者类UML图不能太细太完整,否则过于庞大的模型会干扰人的思维,阻碍对主要部分,或者复杂逻辑的梳理。业务总是被切分成一个个片断进行分析,在每一个片断里,画出几个主要的对象和交互逻辑,细节的部分用文字记录、描述。
3. 领域模型中不应当出现设计、技术方面的术语,也不应当出现开发人员不理解的业务术语。
关于领域模型表达方法(UML图Visio图)的选择问题,标准的形式是使用UML,但如果领域专家或开发人员根本无法对UML图例形成清晰的概念(将UML图映射到领域对象、逻辑,或代码实现),将注定DDD方式应用失败。因为没有掌握语言就无法使用它正确交流。第三节中强调建模人员要直接与开发过程接触,重要的一个作用是确认开发人员对领域模型这个语言是否正确理解,确保代码实现保持了建模者要表达的意图。因此DDD的一个关键思想是建模人员自始至终维护领域模型这个语言,以及它表达的内容,向团队的各个角色(领域专家、开发人员等)诠释各个建模元素的含义,让各个角色掌握这个语言,运用它来表达、实现实际需求。这是分析人员最关键的职责,贯穿整个过程。
领域逻辑的表达不要受UML图的约束,否则可能是问题所在,适当的描述就可以很好的解决这个问题。
关于DDD和UML相关的具体介绍可参考:
《UML和模式应用》(第三版)
《.NET.Domain.Driven.Design.with.C.Sharp》
从领域问题,场景中分析找出概念类,确定一组概念类则是OO分析的核心。以下就“试卷”这个领域来进行DDD的分析和设计尝试。
- 场景描述
试卷由多个大题组成,每个大题下有多道试题,试卷还可以有多个分卷,如分卷I,分卷II,可以统计大题下的试题信息…
- 概念类提取
从上述描述提取出概念类:试卷,分卷,大题,试题。
- UML概念类表达
- 几个设计方案
方案一:按级联关系相应设计试卷结构,形成试卷->分卷->大题->试题的结构。
方案二:将试卷的分卷,大题,试题都认为是试卷分块,试卷成为一个试卷分块的线性列表。
方案三:将试卷的分卷,大题等归属于结构相关信息,对试卷抽象出试题列表和试卷结构,
- 方案比较(略)
方案一完全依赖场景描述设计层级结构,固化了试卷结构,方案二,三的设计思路都是将试题列表设计为线性,屏蔽业务和呈现需求的差异。
- 最终方案
试卷(Paper)主要由试卷结构(PaperStruct)和试题列表(PaperQuestionList)构成,试卷结构包含试卷分块列表,试卷分块用于表示试卷中的大题概念。
试题列表是为此场景而特化设计的集合,仅仅是对试卷分块(PaperPart)中试题列表的逻辑映射,仅公开部分集合相关操作。
试卷分块主要包含试题索引范围(QuestionIndexRang)和 分块试题列表(QuestionList)的定义以及相关统计属性。
最终方案的优势:即保证了结构和业务的剥离,又使得呈现可以以直观的层级进行。
- UML详细设计模型
- 基本代码成型
试卷:
- /// <summary>
- /// 试卷
- /// </summary>
- public class Paper : ICloneable
- {
- private int _paperId;
- /// <summary>
- /// 获取试卷标识
- /// </summary>
- public int PaperId { get { return this._paperId; } }
- /// <summary>
- /// 获取或设置试卷结构
- /// </summary>
- public PaperStruct PaperStruct { get; set; }
- /// <summary>
- /// 获取试卷分块列表
- /// </summary>
- public PaperPartList PaperPartList
- {
- get { return this.PaperStruct.PaperPartList; }
- }
- /// <summary>
- /// 获取试卷试题列表
- /// </summary>
- public PaperQuestionList QuestionList
- {
- get { return PaperQuestionList.Create(this); }
- }
- /// <summary>
- /// 清理试卷内部结构
- /// </summary>
- public void Clear()
- {
- //重设分块的试题索引
- //重设试题列表索引
- }
- #region ICloneable 成员
- /// <summary>
- /// 返回一份当前试卷的副本
- /// 副本将完成可靠的拷贝逻辑
- /// </summary>
- /// <returns></returns>
- public object Clone()
- {
- Paper clone = this.MemberwiseClone() as Paper;
- //引用处理
- //...
- //标识置0
- clone._paperId = 0;
- //拷贝引用
- return clone;
- }
- #endregion
- }
试卷分块:分块将维持试题索引范围
- /// <summary>
- /// 试卷分块
- /// </summary>
- public class PaperPart : ICloneable
- {
- //构?
- public PaperPart()
- {
- this.QuestionRang = new QuestionIndexRang(-1, -1);
- this.QuestionList = new List<QuestionInfo>();
- }
- /// <summary>
- /// ?取或?置分块下的??索引?围 ?不存在??则索引??为-1
- /// </summary>
- public QuestionIndexRang QuestionRang { get; set; }
- /// <summary>
- /// ?取或?置?分块下的??列?
- /// </summary>
- public List<QuestionInfo> QuestionList { get; set; }
- #region ICloneable 成员
- public object Clone()
- {
- PaperPart clone = this.MemberwiseClone() as PaperPart;
- //处理引用
- return clone;
- }
- #endregion
- }
试卷试题列表:此设计用于映射分块中的试题,并控制对其的调用,使得对它的相关操作能映射至实际的试题。
- /// <summary>
- /// 试卷的试题列表
- /// </summary>
- public class PaperQuestionList : CustomList<QuestionInfo>
- {
- /// <summary>
- /// 所在的?卷
- /// </summary>
- public Paper Paper { get; private set; }
- /// <summary>
- /// 初始化
- /// </summary>
- /// <param name="paper"></param>
- private PaperQuestionList(Paper paper)
- : base()
- {
- this.Paper = paper;
- }
- /// <summary>
- /// 生成试卷的试题列表
- /// </summary>
- public static PaperQuestionList Create(Paper paper)
- {
- PaperQuestionList list = new PaperQuestionList(paper);
- //拼接
- list.Paper.PaperPartList.ForEach(o => { list._list.AddRange(o.QuestionList as IEnumerable<QuestionInfo>); });
- return list;
- }
- /// <summary>
- /// 重载索引器
- /// </summary>
- /// <param name="index"></param>
- /// <returns></returns>
- public override QuestionInfo this[int index]
- {
- get
- {
- return base[index];
- }
- set
- {
- bool flag = false;
- int count = 0;
- for (int i = 0; i < this.Paper.PaperPartList.Count; i++)
- {
- if (flag) break;
- for (int j = 0; j < this.Paper.PaperPartList[i].QuestionList.Count; j++)
- {
- if (count == index)
- {
- //修正实际引用
- this.Paper.PaperPartList[i].QuestionList[j] = value;
- flag = true;
- break;
- }
- count++;
- }
- }
- base[index] = value;
- }
- }
- }
- 更多考虑
对于模型的接口和属性暴露:本文所提及的设计并非采用贫血模型,模型内部需维持主要逻辑,对于对外接口和属性应慎重,如对于引用类型的属性,应考虑使用者有可能并不总是按照设计者的预期来使用,因此最小化设计原则和按需暴露接口,或者返回接口而非真实引用尤为必要。
领域模型并非为传输而设计,若希望模型同时能够支持跨层传输和序列化,则需额外注意相关属性和字段的设计和暴露。
DDD是从业务出发,关注业务,以业务价值为导向的设计方法,测试和业务的实现均是由它出发,以对象而不是关系数据库作为模型基础,而对于存储则不是业务人员需要过多关心,通常web开发者习惯了数据驱动设计,总是希望实体和数据库一一映射,那么您恐怕需要换个思路思考了,因而一个强有力的ORM框架支持也是DDD的执行的保证。
篇后语:学习和实践中,请多多指正。欢迎拍砖。