万字长文解析,领域驱动设计(DDD)落地设计
- 2023-06-12 北京
本文字数:10341 字
阅读完需:约 34 分钟
前言
领域驱动设计(简称 ddd)概念来源于 2004 年著名建模专家 Eric Evans 发表的他最具影响力的书籍:《领域驱动设计——软件核心复杂性应对之道》(Domain-Driven Design –Tackling Complexity in the Heart of Software),简称 Evans DDD,领域驱动设计思想进入软件开发者的视野。在将近 20 年的发展中领域模型设计一直占据着非常重要的位置,但其直接面向业务场景的设计思想,更适合在具有特定业务场景的模型构建。在日常我们见到的 DDD 模型多数是具有特定业务背景的特定业务(Domain-Specific Modeling)特定领域建模工具。
近两年,随着新一代 WEB 技术以及,微服务、中台技术,云原生应用的推广;领域驱动模型(DDD)再次成为软件设计领域的热点。 而低代码/无代码平台是进近几年持续高速发展的一个技术领域。
一,OneCode-工具集 简介
OneCode-DSM(以下简称 DSM)工具集是建立是以 OneCode 低代码引擎为基础专注于低代码建模应用的高阶建模工具。
在 OneCode 引擎中,出了为普通用户提供无代码的拖动设计器,低代码的业务逻辑编排器,之外还提供了供专业业务领域专家的使用的 DSM 建模工具。
OneCode-DSM 应用
(1)可视化设计器
以可视化设计器引擎为主体的表单报表工具,在日常常用的表单报表中是以无代码的方式来实现业务流审批以及数据大屏展现设计,移动展现等应用。
(2)低代码服务集成工具
(3)DSM 建模工具
二,OneCode-DSM 工具
(1)工具组成概览
DSM 建模工具是 OneCode 建模的辅助工具,提供了资源库管理模块,领域模型构建模块,以及视图工厂配置模块。
仓储模型模块,主要功能是辅助用户将用户的数据库,外部 API 接口,以及已有的“代码”应用通过转换器转变为可被 DSM 识别的资源部格式。
领域模型模块是 DSM 核心工具,在领域模型中导入的资源会同具体场景下的值对象,场景菜单、通用域服务根据具体的业务场景完成领域模型的建模工作。
视图工厂是领域模型的具体实现,在领域模型应用中建模输出的产物会通过出码工厂输出位视图应用,这些视图应用会通过视图工厂进一步加工处理输出为用户交互应用。
(2)OneCode 语言(注解)组成
(3)建模流程
三,仓储库建模
(1)通过数据库建模
仓储工具中使用频率最高的是数据库的转换应用,用户通过数据库工具完成数据源配置。
数据源配置
库表映射代码示例:
@DBTable(configKey="fdt",cname="领导信息",tableName="FDT_LINGDAO",primaryKey="uuid")
@Repository(repositoryId="54fa1fd2-cae4-44b0-8387-f10f98bdd63c")
public interface FdtLingdao {
@Uid
@DBField(cnName="主键",dbFieldName="uuid")
public String getUuid();
public void setUuid(String uuid);
@CustomAnnotation(caption="名称")
@DBField(cnName="名称",length=20,dbFieldName="test")
public String getTest();
public void setTest(String test);
@CustomAnnotation(caption="领导姓名")
@DBField(cnName="领导姓名",length=20,dbFieldName="name")
public String getName();
public void setName(String name);
@CustomAnnotation(caption="职务")
@DBField(cnName="职务",length=100,dbFieldName="duty")
public String getDuty();
public void setDuty(String duty);
@CustomAnnotation(caption="创建时间")
@DBField(cnName="创建时间",length=0,dbType=ColType.DATETIME,dbFieldName="createtime")
public Date getCreatetime();
public void setCreatetime(Date createtime);
}库表装载器 CRUD 代码示例:
@Aggregation(type = AggregationType.aggregationEntity,entityClass=FdtLingdao.class,rootClass= FdtLingdaoService.class)
public interface FdtLingdaoService {
@APIEventAnnotation(bindMenu = {CustomMenuItem.reload})
public List<FdtLingdao> findAll()throws JDSException;
public List<FdtLingdao> findByWhere(String where)throws JDSException;
@APIEventAnnotation(bindMenu = {CustomMenuItem.search})
public List<FdtLingdao> find(FdtLingdao fdtLingdao)throws JDSException;
@APIEventAnnotation(bindMenu = {CustomMenuItem.add,CustomMenuItem.editor})
public FdtLingdao getFdtLingdaoInfo(String uuid) throws JDSException;
@APIEventAnnotation(bindMenu = {CustomMenuItem.save}, callback = {CustomCallBack.ReloadParent, CustomCallBack.Close})
public FdtLingdao update( FdtLingdao fdtLingdao) throws JDSException;
@APIEventAnnotation(bindMenu = {CustomMenuItem.delete}, callback = {CustomCallBack.Reload})
public Boolean delete(String uuid) throws JDSException;
}
(2)通过实体建模仓储实体实例
@Entity@Aggregation(type = AggregationType.aggregationRoot,sourceClass = Role.class, rootClass = Role.class)
public interface Role extends java.io.Serializable {
@MethodChinaName(cname = "角色标识")
@Uid
public String getRoleId();
public void setRoleId(String roleId);
@MethodChinaName(cname = "名称")
@Caption
public String getName();
public void setName(String name);
@MethodChinaName(cname = "角色类型")
public RoleType getType();
public void setType(RoleType type);
@MethodChinaName(cname = "级数")
public String getRoleNum();
public void setRoleNum(String num);
@MethodChinaName(cname = "系统ID")
@Pid
public String getSysId();
public void setSysId(String sysId);
@MethodChinaName(cname = "人员")
@Ref(ref = RefType.m2m, view = ViewType.grid)
public List<Person> getPersonList();
@MethodChinaName(cname = "部门")
@Ref(ref = RefType.m2m, view = ViewType.grid)
public List<Org> getOrgList();
public List<String> getOrgIdList();
}
(3) 实体关系
仓储建模的一个核心目的是将结构化的数据转变为面向对象的模式,而这其中非常重要的一点则是实体关系的处理,DSM 设计中针对数据库表允许用户在导入数据库后再次进行实体关系建模,将数据库表按 1:1 ,1:N, N:N 模型建立关系。完成建模后在出码的过程中会根据业务模板设定,进行实体模型的转变,在实体代码中以 @Ref 关系标签完成建模应用。
实体模型关系在基础的模型上根据仓储模型设计,独立扩展了
public enum RefType implements IconEnumstype {
ref("引用", "spafont spa-icon-c-databinder"),
o2m("一对多", "spafont spa-icon-c-treeview"),
m2o("多对一", "spafont spa-icon-alignwh"),
f2f("自循环", "spafont spa-icon-cancel"),
o2o("一对一", "spafont spa-icon-hmirror"),
find("查找", "xui-icon-search"),
m2m("多对多", "spafont spa-icon-alignwh");
private final String imageClass;
private final String name;
RefType(String name, String imageClass) {
this.name = name;
this.imageClass = imageClass;
}
}
(4) 仓储建模模板
(5)仓储模型常用注解
四,领域建模
(1)领域建模组成
(2)领域建模原理
模型渲染原理
OneCode 本身基于 JAVA 语言体系,是在 Java Spring 注解基础上的一套扩展子集,混合编译引擎器通过扩展注解构建完整的 Domain 模型,通过读取标准 Spring 注解完成普通 Web 数据交付及调度过程。
示例注解说明:
完整示例代码:
@Controller@RequestMapping("/admin/org/deparment/")
@MethodChinaName(cname = "部门管理", imageClass = "bpmfont bpmgongzuoliuzuhuzicaidan")
@Aggregation(sourceClass = IDeparmentService.class, rootClass = Org.class)
public interface IDeparmentAPI {
@RequestMapping(method = RequestMethod.POST, value = "Persons")
@GridViewAnnotation()
@ModuleAnnotation(caption = "人员列表")
@APIEventAnnotation(bindMenu = {CustomMenuItem.treeNodeEditor})
@ResponseBodypublic <K extends IPersonGrid> ListResultModel<List<K>> getPersons(String orgId);
@RequestMapping(method = RequestMethod.POST, value = "loadChild")
@APIEventAnnotation(bindMenu = {CustomMenuItem.loadChild})
@ResponseBody
public <T extends IDeparmentTree> ListResultModel<List<T>> loadChild(String parentId);
@MethodChinaName(cname = "部门信息")
@RequestMapping(method = RequestMethod.POST, value = "DeparmentInfo")
@NavGroupViewAnnotation(saveUrl = "admin.org.deparment.saveOrg")
@DialogAnnotation
@ModuleAnnotation(caption = "编辑部门信息", width = "600", height = "480")
@APIEventAnnotation(callback = {CustomCallBack.ReloadParent, CustomCallBack.Close}, bindMenu = {CustomMenuItem.editor})
@ResponseBody
public <K extends IDeparmentNav> ResultModel<K> getDeparmentInfo(String orgId);
@MethodChinaName(cname = "添加部门")
@RequestMapping(method = RequestMethod.POST, value = "addDeparment")
@FormViewAnnotation()
@DialogAnnotation
@ModuleAnnotation(caption = "添加部门", width = "350", height = "260")
@APIEventAnnotation(bindMenu = {CustomMenuItem.add}, autoRun = true)
@ResponseBody
public <M extends IAddDeparmentForm> ResultModel<M> addDeparment(String parentId);
@MethodChinaName(cname = "保存机构信息")
@RequestMapping(value = {"saveOrg"}, method = {RequestMethod.GET, RequestMethod.POST})
@APIEventAnnotation(callback = {CustomCallBack.ReloadParent, CustomCallBack.Close}, bindMenu = {CustomMenuItem.save})
public @ResponseBody
ResultModel<Boolean> saveOrg(@RequestBody IAddDeparmentForm deparmentForm);
@MethodChinaName(cname = "删除部门")
@RequestMapping(value = {"delOrg"}, method = {RequestMethod.GET, RequestMethod.POST})
@APIEventAnnotation(callback = {CustomCallBack.Reload, CustomCallBack.ReloadParent}, bindMenu = {CustomMenuItem.delete})
public @ResponseBody
ResultModel<Boolean> delOrg(String orgId);
;
}
(3)聚合配置
建模配置
接口配置
窗体配置
执行事件传递
(4)聚合分类
(5)聚合根
聚合根设计,通常来标识其独立的域应用,具有全局唯一性的特点。 在 通用域中,具有时间轴维度的对象描述,如工作流中的流程示例(ProcessInst)对象。在获取聚合根后,可以依据时间轴一次向前获取,流程定义对象,向后获取可以取得流程历史数据,而根据当前节点则可以获取权限,数据表单、业务示例状态的等等实体数据。
流程示例聚合根配置
流程实例聚合根源码
@Entity@MethodChinaName(cname = "流程实例")
@Aggregation(type = AggregationType.aggregationRoot, sourceClass = ProcessInst.class, rootClass = ProcessInst.class)
public interface ProcessInst extends java.io.Serializable {
@MethodChinaName(cname = "流程实例UUID")
@Uidpublic String getProcessInstId();
@MethodChinaName(cname = "流程定义UUID")
@Pid
public String getProcessDefId();
@MethodChinaName(cname = "流程定义版本UUID")
@Pid
public String getProcessDefVersionId();
@MethodChinaName(cname = "流程实例名称")
public String getName();
@MethodChinaName(cname = "紧急程度")
public String getUrgency();
@MethodChinaName(cname = "流程实例状态")
public ProcessInstStatus getState();
@MethodChinaName(cname = "流程实例副本数量")
public int getCopyNumber();
@MethodChinaName(cname = "程实例启动时间")
public Date getStartTime();
@MethodChinaName(cname = "流程实例办结时间")
public Date getEndTime();
@MethodChinaName(cname = " 流程实例时间限制")
public Date getLimitTime();
@MethodChinaName(cname = "流程实例状态")
public ProcessInstStatus getRunStatus();
@MethodChinaName(cname = "流程定义版本")
@Ref(ref = RefType.m2o, view = ViewType.grid)
public ProcessDefVersion getProcessDefVersion() throws BPMException;
@MethodChinaName(cname = "流程定义")
@Ref(ref = RefType.m2o, view = ViewType.dic)
public ProcessDef getProcessDef() throws BPMException;
@MethodChinaName(cname = "活动实例")
@Ref(ref = RefType.o2m, view = ViewType.grid)
public List<ActivityInst> getActivityInstList() throws BPMException;
@MethodChinaName(cname = "流程属性值", returnStr = "getWorkflowAttribute($R('attName'))", display = false)
public Object getWorkflowAttribute(ProcessInstAtt name);
@MethodChinaName(cname = "权限属性值", returnStr = "getRightAttribute($R('attName'))", display = false)
public Object getRightAttribute(ProcessInstAtt name);
@MethodChinaName(cname = "应用属性值", returnStr = "getAppAttribute($R('attName'))", display = false)
public Object getAppAttribute(ProcessInstAtt name);
@MethodChinaName(cname = "定制属性值", returnStr = "getAttribute($R('attName'))", display = false)
public String getAttribute(String name);
@MethodChinaName(cname = "取得流程中的所有属性值", returnStr = "getAllAttribute()", display = false)
@Ref(ref = RefType.o2m, view = ViewType.grid)
public List<AttributeInst> getAllAttribute();
@MethodChinaName(cname = "个人定制属性值", returnStr = "getAttribute($R('personId'),$R('attName'))", display = true)
public String getPersonAttribute(String personId, String name);
@MethodChinaName(cname = "设置定制属性", returnStr = "setAttribute($R('attName'),$R('value'))", display = false)
public void setAttribute(String name, String value) throws BPMException;
@MethodChinaName(cname = "设置个人定制属性", returnStr = "setAttribute($R('personId'),$R('attName'),$R('value'))", display = false)
public void setPersonAttribute(String personId, String name, String value) throws BPMException;
@MethodChinaName(cname = "更新流程实例名称(公文标题)", returnStr = "updateProcessInstUrgency($R('processInstName'))")
public ReturnType updateProcessInstName(String name)
throws BPMException;
@MethodChinaName(cname = "更新流程实例紧急程度", returnStr = "updateProcessInstUrgency($R('urgency'))")
public ReturnType updateProcessInstUrgency(
String urgency) throws BPMException;
@MethodChinaName(cname = "流程实例挂起", returnStr = "suspendProcessInst()", display = false)
public ReturnType suspendProcessInst()
throws BPMException;
@MethodChinaName(cname = "继续流程实例", returnStr = "resumeProcessInst()", display = false)
public ReturnType resumeProcessInst()
throws BPMException;
@MethodChinaName(cname = "取得活动的历史数据, 根据流程实例")
public List<ActivityInstHistory> getActivityInstHistoryListByProcessInst() throws BPMException;
@MethodChinaName(cname = "中止流程实例", returnStr = "abortProcessInst()", display = false)
public ReturnType abortProcessInst()
throws BPMException;
@MethodChinaName(cname = "流程实例完成", returnStr = "completeProcessInst()", display = false)
public ReturnType completeProcessInst()
throws BPMException;
@MethodChinaName(cname = "删除流程实例", returnStr = "deleteProcessInst()", display = false)
public ReturnType deleteProcessInst()
throws BPMException;
@MethodChinaName(cname = "获取表单数据")
public DataMap getFormValues() throws BPMException;
@MethodChinaName(cname = "更新表单数据")
public void updateFormValues(DataMap dataMap) throws BPMException;
}
(6)聚合实体
聚合实体,通常用来描述独立实体机构,例如业务表单中单表或简单关联表关系。通常只包含简单的值关系,功能上也仅限于,查询列表、保存表单等简单应用。
单表表单
代码示例
@Controller@RequestMapping("/test/fdtlingdaoservice/")
@Aggregation(type=AggregationType.aggregationEntity,sourceClass=FdtLingdaoService.class)public interface FdtLingdaoAPI {
@APIEventAnnotation(bindMenu=CustomMenuItem.search)
@RequestMapping(value="searchUrl")
@ModuleAnnotation@GridViewAnnotation@ResponseBody
public ListResultModel<List<FindGridView>> find (FdtLingdao fdtLingdao);
@APIEventAnnotation(bindMenu={CustomMenuItem.add,CustomMenuItem.editor})
@RequestMapping(value="addPath")
@ModuleAnnotation
@FormViewAnnotation
@ResponseBody
public ResultModel<GetFdtLingdaoInfoView> getFdtLingdaoInfo (String uuid);
@APIEventAnnotation(bindMenu=CustomMenuItem.save,callback={CustomCallBack.ReloadParent,CustomCallBack.Close})
@RequestMapping(value="update")
@ResponseBody
public ResultModel<FdtLingdao> update (@RequestBody FdtLingdao fdtLingdao);
@RequestMapping(value="findByWhere")
@ResponseBody
public ListResultModel<List<FdtLingdao>> findByWhere (String where);
@APIEventAnnotation(bindMenu=CustomMenuItem.delete,callback=CustomCallBack.Reload)
@RequestMapping(value="delete")
@ResponseBody
public ResultModel<Boolean> delete (String uuid);
@APIEventAnnotation(bindMenu=CustomMenuItem.reload)
@RequestMapping(value="dataUrl")
@ModuleAnnotation
@GridViewAnnotation
@ResponseBody
public ListResultModel<List<FindAllGridView>> findAll ();
}
(6)动作菜单
动作菜单在 DDD 模型中并未定义,但在低代码应用却有着很重要的一席。
动作菜单建模中,主要将菜单展现与关联动作结合在一起。
工作流发送菜单建模
菜单展现位置
编辑器右键菜单
常用菜单注解
@Controller@RequestMapping(value = {"/java/agg/context/"})
@MenuBarMenu(menuType = CustomMenuType.component, caption = "菜单")
@Aggregation(type = AggregationType.menu)
public class JavaAggPackageMenu {
@RequestMapping(method = RequestMethod.POST, value = "paste")
@CustomAnnotation(imageClass = "spafont spa-icon-paste", index = 1, caption = "粘贴")
@APIEventAnnotation(customRequestData = {RequestPathEnum.treeview, RequestPathEnum.sTagVar}, callback = CustomCallBack.TreeReloadNode)
public @ResponseBody
TreeListResultModel<List<JavaAggTree>> paste(String sfilePath, String packageName, String domainId, String projectName) {
TreeListResultModel<List<JavaAggTree>> result = new TreeListResultModel<List<JavaAggTree>>();
try {
DSMFactory dsmFactory = DSMFactory.getInstance();
File desFile = new File(sfilePath);
DomainInst domainInst = dsmFactory.getAggregationManager().getDomainInstById(domainId);
JavaSrcBean srcBean = dsmFactory.getTempManager().genJavaSrc(desFile, domainInst, null);
DSMFactory.getInstance().getBuildFactory().copy(srcBean, packageName);
result.setIds(Arrays.asList(new String[]{domainId + "|" + packageName}));
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
@RequestMapping(method = RequestMethod.POST, value = "reLoad")
@CustomAnnotation(imageClass = "xuicon xui-refresh", index = 1, caption = "刷新")
@APIEventAnnotation(customRequestData = RequestPathEnum.treeview, callback = CustomCallBack.TreeReloadNode)
public @ResponseBody
TreeListResultModel<List<JavaAggTree>> reLoad(String packageName, String domainId, String filePath) {
TreeListResultModel<List<JavaAggTree>> result = new TreeListResultModel<List<JavaAggTree>>();
String id = domainId + "|" + packageName;
result.setIds(Arrays.asList(new String[]{id}));
return result;
}
@RequestMapping(value = {"split"})
@Split
@CustomAnnotation(index = 2)
@ResponseBody
public ResultModel<Boolean> split2() {
ResultModel<Boolean> result = new ResultModel<Boolean>();
return result;
}
@RequestMapping(value = "UploadFile")
@APIEventAnnotation(autoRun = true)
@DialogAnnotation(width = "450", height = "380", caption = "上传JAVA文件")
@ModuleAnnotation
@FormViewAnnotation
@CustomAnnotation(caption = "上传", index = 3, imageClass = "xui-icon-upload", tips = "上传")
public @ResponseBody
ResultModel<UPLoadFile> uploadFile(String domainId, String packageName) {
ResultModel<UPLoadFile> resultModel = new ResultModel<UPLoadFile>();
try {
UPLoadFile upLoadFile = new UPLoadFile(domainId, packageName);
resultModel.setData(upLoadFile);
} catch (Exception e) {
e.printStackTrace();
}
return resultModel;
}
@RequestMapping(value = {"split"})
@Split
@CustomAnnotation(index = 4)
@ResponseBody
public ResultModel<Boolean> split6() {
ResultModel<Boolean> result = new ResultModel<Boolean>();
return result;
}
@RequestMapping(method = RequestMethod.POST, value = "newApp")
@CustomAnnotation(imageClass = "xuicon xui-uicmd-add", index = 5)
@MenuBarMenu(menuType = CustomMenuType.sub, caption = "新建", imageClass = "xuicon xui-uicmd-add", index = 5)
public JavaAggNewMenu getJavaJarAction() {
return new JavaAggNewMenu();
}
@RequestMapping(method = RequestMethod.POST, value = "importAgg")
@CustomAnnotation(imageClass = "xuicon xui-uicmd-add", index = 6)
@MenuBarMenu(menuType = CustomMenuType.sub, caption = "导入", imageClass = "spafont spa-icon-html", index = 5)
public JavaAggImportMenu importAgg() {
return new JavaAggImportMenu();
}
@RequestMapping(value = {"split"})
@Split
@CustomAnnotation(index = 7)
@ResponseBody
public ResultModel<Boolean> split7() {
ResultModel<Boolean> result = new ResultModel<Boolean>();
return result;
}
@MethodChinaName(cname = "删除")
@RequestMapping(method = RequestMethod.POST, value = "delete")
@CustomAnnotation(imageClass = "xuicon xui-icon-minus", index = 8)
@APIEventAnnotation(customRequestData = RequestPathEnum.treeview, callback = CustomCallBack.TreeReloadNode)
public @ResponseBody
TreeListResultModel<List<JavaAggTree>> delete(String domainId, String parentId, String filePath, String javaTempId) {
TreeListResultModel<List<JavaAggTree>> result = new TreeListResultModel<List<JavaAggTree>>();
try {
File desFile = new File(filePath);
DSMFactory dsmFactory = DSMFactory.getInstance();
DomainInst domainInst = dsmFactory.getAggregationManager().getDomainInstById(domainId);
if (desFile.exists()) {
if (desFile.isDirectory()) {
JavaPackage javaPackage = domainInst.getPackageByFile(desFile);
dsmFactory.getTempManager().deleteJavaPackage(javaPackage);
} else {
JavaSrcBean srcBean = dsmFactory.getTempManager().genJavaSrc(desFile, domainInst, javaTempId);
dsmFactory.getTempManager().deleteJavaFile(srcBean);
}
}
result.setIds(Arrays.asList(new String[]{parentId}));
} catch (JDSException e) {
e.printStackTrace();
}
return result;
}
public ESDChrome getCurrChromeDriver() {
Object handleId = JDSActionContext.getActionContext().getParams("handleId");
ChromeDriver chrome = null;
if (handleId != null) {
chrome = ESDEditor.getInstance().getChromeDriverById(handleId.toString());
}
if (chrome == null) {
chrome = ESDEditor.getInstance().getCurrChromeDriver();
}
return new ESDChrome(chrome);
}
}
(7)通用域
通用域将系统中常用服务进行了独立分类可以在工程构建时导入进来。
通用域管理
(8)API 服务接口
api 服务接口是手工代码接入的域服务,在普通 java 类上加上聚和接口后会统一归类到该类型管理。
(9)领域模型常用注解
高宏数智低代码
专注于,DDD领域模型设计工具开发推广。 2022-09-07 加入
还未添加个人简介
评论