【渐进】浅尝DDD,对"试卷"建模

时间:2022-08-31 13:45:14

      领域模型是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概念类表达

【渐进】浅尝DDD,对"试卷"建模

  • 几个设计方案

     方案一:按级联关系相应设计试卷结构,形成试卷->分卷->大题->试题的结构。

     方案二:将试卷的分卷,大题,试题都认为是试卷分块,试卷成为一个试卷分块的线性列表。

     方案三:将试卷的分卷,大题等归属于结构相关信息,对试卷抽象出试题列表和试卷结构,

  • 方案比较(略)

     方案一完全依赖场景描述设计层级结构,固化了试卷结构,方案二,三的设计思路都是将试题列表设计为线性,屏蔽业务和呈现需求的差异。

  • 最终方案

     试卷(Paper)主要由试卷结构(PaperStruct)和试题列表(PaperQuestionList)构成,试卷结构包含试卷分块列表,试卷分块用于表示试卷中的大题概念。

     试题列表是为此场景而特化设计的集合,仅仅是对试卷分块(PaperPart)中试题列表的逻辑映射,仅公开部分集合相关操作。

     试卷分块主要包含试题索引范围(QuestionIndexRang)和 分块试题列表(QuestionList)的定义以及相关统计属性。

     最终方案的优势:即保证了结构和业务的剥离,又使得呈现可以以直观的层级进行。

  • UML详细设计模型

【渐进】浅尝DDD,对"试卷"建模

  • 基本代码成型

     试卷:

  1.   /// <summary>
  2.   /// 试卷
  3.   /// </summary>
  4.   public class Paper : ICloneable
  5.   {
  6.       private int _paperId;
  7.       /// <summary>
  8.       /// 获取试卷标识
  9.       /// </summary>
  10.       public int PaperId { get { return this._paperId; } }
  11.       /// <summary>
  12.       /// 获取或设置试卷结构
  13.       /// </summary>
  14.       public PaperStruct PaperStruct { get; set; }
  15.       /// <summary>
  16.       /// 获取试卷分块列表
  17.       /// </summary>
  18.       public PaperPartList PaperPartList
  19.       {
  20.           get { return this.PaperStruct.PaperPartList; }
  21.       }
  22.       /// <summary>
  23.       /// 获取试卷试题列表
  24.       /// </summary>
  25.       public PaperQuestionList QuestionList
  26.       {
  27.           get { return PaperQuestionList.Create(this); }
  28.       }
  29.       /// <summary>
  30.       /// 清理试卷内部结构
  31.       /// </summary>
  32.       public void Clear()
  33.       {
  34.           //重设分块的试题索引
  35.           //重设试题列表索引
  36.       }
  37.  
  38.       #region ICloneable 成员
  39.       /// <summary>
  40.       /// 返回一份当前试卷的副本
  41.       /// 副本将完成可靠的拷贝逻辑
  42.       /// </summary>
  43.       /// <returns></returns>
  44.       public object Clone()
  45.       {
  46.           Paper clone = this.MemberwiseClone() as Paper;
  47.  
  48.           //引用处理
  49.           //...
  50.  
  51.           //标识置0
  52.           clone._paperId = 0;
  53.  
  54.           //拷贝引用
  55.  
  56.           return clone;
  57.       }
  58.       #endregion
  59.  
  60.  
  61.   }

 

    试卷分块:分块将维持试题索引范围

  1. /// <summary>
  2. /// 试卷分块
  3. /// </summary>
  4. public class PaperPart : ICloneable
  5. {
  6.     //构?
  7.  
  8.     public PaperPart()
  9.     {
  10.         this.QuestionRang = new QuestionIndexRang(-1, -1);
  11.         this.QuestionList = new List<QuestionInfo>();
  12.     }
  13.  
  14.     /// <summary>
  15.     /// ?取或?置分块下的??索引?围 ?不存在??则索引??为-1
  16.     /// </summary>
  17.     public QuestionIndexRang QuestionRang { get; set; }
  18.     /// <summary>
  19.     /// ?取或?置?分块下的??列?
  20.     /// </summary>
  21.     public List<QuestionInfo> QuestionList { get; set; }
  22.  
  23.     #region ICloneable 成员
  24.  
  25.     public object Clone()
  26.     {
  27.         PaperPart clone = this.MemberwiseClone() as PaperPart;
  28.  
  29.         //处理引用
  30.  
  31.         return clone;
  32.     }
  33.  
  34.     #endregion
  35. }

 

    试卷试题列表:此设计用于映射分块中的试题,并控制对其的调用,使得对它的相关操作能映射至实际的试题。

  1. /// <summary>
  2. /// 试卷的试题列表
  3. /// </summary>
  4. public class PaperQuestionList : CustomList<QuestionInfo>
  5. {
  6.     /// <summary>
  7.     /// 所在的?卷
  8.     /// </summary>
  9.     public Paper Paper { get; private set; }
  10.     /// <summary>
  11.     /// 初始化
  12.     /// </summary>
  13.     /// <param name="paper"></param>
  14.     private PaperQuestionList(Paper paper)
  15.         : base()
  16.     {
  17.         this.Paper = paper;
  18.     }
  19.     /// <summary>
  20.     /// 生成试卷的试题列表
  21.     /// </summary>
  22.     public static PaperQuestionList Create(Paper paper)
  23.     {
  24.         PaperQuestionList list = new PaperQuestionList(paper);
  25.  
  26.         //拼接
  27.         list.Paper.PaperPartList.ForEach(o => { list._list.AddRange(o.QuestionList as IEnumerable<QuestionInfo>); });
  28.  
  29.         return list;
  30.     }
  31.     /// <summary>
  32.     /// 重载索引器
  33.     /// </summary>
  34.     /// <param name="index"></param>
  35.     /// <returns></returns>
  36.     public override QuestionInfo this[int index]
  37.     {
  38.         get
  39.         {
  40.             return base[index];
  41.         }
  42.         set
  43.         {
  44.             bool flag = false;
  45.             int count = 0;
  46.             for (int i = 0; i < this.Paper.PaperPartList.Count; i++)
  47.             {
  48.                 if (flag) break;
  49.  
  50.                 for (int j = 0; j < this.Paper.PaperPartList[i].QuestionList.Count; j++)
  51.                 {
  52.                     if (count == index)
  53.                     {   
  54.                         //修正实际引用
  55.                         this.Paper.PaperPartList[i].QuestionList[j] = value;
  56.                         flag = true;
  57.                         break;
  58.                     }
  59.                     count++;
  60.                 }
  61.             }
  62.             base[index] = value;
  63.         }
  64.     }
  65. }

 

  • 更多考虑

     对于模型的接口和属性暴露:本文所提及的设计并非采用贫血模型,模型内部需维持主要逻辑,对于对外接口和属性应慎重,如对于引用类型的属性,应考虑使用者有可能并不总是按照设计者的预期来使用,因此最小化设计原则和按需暴露接口,或者返回接口而非真实引用尤为必要。

     领域模型并非为传输而设计,若希望模型同时能够支持跨层传输和序列化,则需额外注意相关属性和字段的设计和暴露。

     DDD是从业务出发,关注业务,以业务价值为导向的设计方法,测试和业务的实现均是由它出发,以对象而不是关系数据库作为模型基础,而对于存储则不是业务人员需要过多关心,通常web开发者习惯了数据驱动设计,总是希望实体和数据库一一映射,那么您恐怕需要换个思路思考了,因而一个强有力的ORM框架支持也是DDD的执行的保证。

 

     篇后语:学习和实践中,请多多指正。欢迎拍砖。