基于领域分析设计的架构规范 - 充血模型之实体

探讨了领域驱动设计(DDD)中实体与服务层的角色,对比了贫血模型与充血模型,强调了实体操作的重要性,并介绍了读写隔离在系统架构中的应用。

640?wx_fmt=png

读写隔离后的世界

基于上面提到的读写隔离的思想,那么我们可以很清楚地看到上面这种情况可以看到:

640?wx_fmt=png

查询业务,从入口层(如Controller),调用Finder,而Finder调用Repository(具体实现如Hiberante,Mybatis等等均可),这一条线下来,我们全然不用考虑这个系统的增删改就是如何做的,就像他们完全处于不同的空间一样,互不干涉,互不影响,甚至,永远互不相见。某种程度上来说,这种这种架构追求的效果,一种美感。

所以,接下来,我们关注的,就是增删改这一部分了,也就是命令操作 是开始要扎扎实实地来对这个系统进行修改了

首先让我们把视野抬高一些,从整个项目产品的上空来看看

业务灵魂-状态图

除开少数非常扁平的纯技术服务项目(比如AI识别,文本分析等等),其他绝大部分企业项目都有其核心的商业逻辑,而这些逻辑,往往也会以核心的领域概念来提现,从简单粗暴一点角度映射到设计开发中,那就是类class

  • 电商系统,核心领域至少有【商品】,【订单】,【用户】,【物流】等;

  • SNS社交平台,至少有【用户】,【博文/帖子】,【私信】,【通知】等;

  • 进销存系统,至少有【账户】,【角色/权限】,【商品】,【客户/供应商】等;

  • 在线教育平台,至少有【用户】,【课程】,【订单】等;

而这其中,也有主次,大家可以回头看看自己所开发过的项目。但凡是有状态字段的类,很大可能都是整个项目的核心领域之一。其实很好理解,因为它有流程,因为它需要被各类操作来变更它的状态,所以,他很可能贯穿了这个项目中某一个关键商业逻辑,比如电商系统中,

  • 【订单】肯定有状态,从[待支付]-[已支付]-[派送中]-[已收货],甚至还有[已取消],[退款中],[退款失败],[退款成功],脑补一下,就知道会生成多少复杂的业务流程了

  • 【用户】一般来说也会有状态,比如[正常],[冻结],但是可以想到,如果某个系统没有这方面权限与安全的要求,【用户】也可能就没有状态了,那么自然也不会有对应的操作对其进行修改,可能只会有创建

所以,如果在这些系统的早期设计阶段,要我选择一个最重要的UML图,我会选择状态图,以下就是整个系统中最核心的订单状态图

640?wx_fmt=png

可以看到,把握一个核心领域的状态变更,自然而然就能归纳出来很大一部分系统的功能需求。我们在这里看到,这些所有箭头所触发的动作,其实都是命令,也其实都是会落地到各个相关领域的增删改上。

当然这里还是一个粗粒度的表示,无法单单依据这个就马上落地开发,因为即使每一个箭头所代表的功能都可以写出一个完整甚至很复杂的用例。但至少这是一个非常清晰的引导。

贫血模型的世界

我们目前所用的Spring体系,几乎都是贫血模型,也就是说,真正的实体类里,都只有各个属性的Get与Set方法。而假如我们要进行一个操作,订单取消,那么最常见的做法是什么?

//一个大而全的订单服务类	
public class OrderService{	
   public void cancelOrder(Long orderId){	
       Order order = orderRepository.getById(orderId);	
       order.setStatus(OrderStatus.CANCELLED);	
       //省略,其他属性的操作...	
   }	
}	

	
//然后在上层(如Controller层)中这么调用	
orderService.cancelOrder(10086);

这是目前行业中非常流行的做法,也是Spring的IOC机制天然形成的做法————尽可能的无状态化。这种做法,在业务迭代时对代码的变动评判标准相对简单,都往Service里放就行了,然后实体对象只需要GetSet即可,简单粗暴,非常容易上手,也正是这种特性,让这种编码风格广为流传。

以上这些话没有任何贬义,因为任何事情,存在即合理,我所经历的公司项目,几乎都是这样做的,大家合作起来没多大问题,业务也都还跑得不错。

那为什么我还想去做一些改变呢?

实体Entity的世界

因为我觉得我们需要再重新审视一下实体Entity

实体为什么要有主键?因为没有主键,那我们怎么知道时要查询/修改哪条数据呢?

这个回答没有问题,只是这句话里其实还蕴藏更深的含义

  1. 这个实体是一个真实存在的东西(对,哪怕它看不见摸不着,但也是存在的),而且会以一种形态被“存储/持久化”在一个存储介质里,比如说数据库;

  2. 当我们需要对某个实体进行操作时,我们需要通过一种手段将它“加载/读取/获得”出来,就像你取快递时,快递员根据你提供的编号,从包裹里把那个东西取出来,完全一样;

  3. 取出来了怎么办?那自然就是要对它进行操作了。没错,这个操作,就是对我们找出来的实体进行操作,而不是别的东西。

所以,从“拿取”,到“操作”,这两步,一切顺理成章,行云流水,所以,以领域驱动设计的做法,或者说,充血模型的做法,会是这样:

//应用层入口类,这里以Controller为例	
public class OrderController{	
   	
   @PostMapping("/cancel")	
   @Transactional	
   public ActionResponse cancelOrder(@RequestBody CancelOrderRequest request){	
   	
       //拿取:根据标识符定位到我们要操作的实体	
       Order order = orderRepository.getById(request.getOrderId());	
     	
       //操作:对,没错,说的就是你 order,就是对你,进行操作,不是别人!	
       order.cancel();	
     	
       //返回结果	
       return ActionResponse.ok();	
   }	
}	

	
//真正的业务逻辑,就是在Order实体里	
@Entity	
public class Order{	

	
  private OrderStatus status;	
  private String customerName;	
  //...	
  	
  public void cancel(){	
      //变更状态	
      status = OrderStatus.CANCELLED;	
      //一些其他属性变动,略	
  }	
}

好,依旧有不少值得探讨的地方:

  1. 我们这里直接在Controller中就开启了Transactional,可能看起来有点反常规,但我个人觉得没什么问题,除了有点不习惯,仔细想想,本身都只不过是Spring的一种组件而已

  2. 所以如果你用的诸如Hibernate之类的JDBC框架,可以无需再进行多余的类似save操作,这也更好的提现了领域设计的思想,因为这时,这个order就是一个实实在在被我们找出来的实体,对它的改动,自动映射到底层持久化,很自然,也必然。

最更容易引发槽点的地方,就是order.cancel(),也就是充血模型的精髓,将行为定位到一个实体类上,而不是不加思考地直接扔进OrderService里。

业界一直有一种非常“美妙”地说法,曾经我一度非常向往,就是“让代码成诗”。换句话说,就是既然追求可读性,那么我们要尽可能的让代码天然具有一种“主谓宾”的感觉,就拿上面“取消订单”做比方,我们是否会觉得:

订单好端端的在那里放着,它自己又不能对自己做什么,自然应该“别人”对他进行了操作:	

	
OrderService.cancel(orderId);	
  某某某      取消了 这个订单	

	
Perfect! 这样读起来,才非常通顺,可读性才更好!

我曾经也是这种风格死忠,而Spring广为流传的无状态架构模式也将这种风格发扬光大。只是我现在,在经历了越来越多复杂业务,长事务的开发需求后,越来越觉得,这个还有有些硬伤

  • 如果一定要读得通畅,更应该是someOperator.cancel(orderId)即某个操作人取消了订单,而不是OrderService,谁都知道OrderService就是一个无状态的代码大集合,一个冰冷的代码而已。但显然someOperator.cancel(orderId)这种做法也是更加不可能实现的,原因就不用过多解释了。

  • order.cancel(),只有2个部分,{操作目标是谁}.{做了什么事情},清晰明了,言简意赅。我相信绝大多数人的阅读习惯也都是从左往右,那么视线第一下扫到的目标一定是最左边的执行对象,也就是order,那么可以在第一时间明确,这个行为是发生在谁身上,而如果是orderService.cancel(orderId),无形中,orderService是一个占据了视线最有力位置的一个巨大的噪点——因为它没有任何的业务意义,你要看的,反而是后面的方法和参数,这在阅读上百行甚至几百行的复合长业务的时候,你会很快困顿,迷失方向。很多时候,我们真的不是技术不达,而是身心疲惫。

相关链接:

<think>我们正在设计一个基于充血模型领域驱动设计(DDD)的参数配置中心动态表单架构。动态表单通常用于灵活配置表单字段,允许用户通过配置而非硬编码来定义表单结构。我们将结合DDD的分层架构和充血模型来实现。 ### 设计目标 1. 使用DDD分层架构,确保领域模型的核心地位。 2. 采用充血模型,将表单配置的业务逻辑封装在领域实体中。 3. 支持动态表单的配置(如字段定义、验证规则、布局等)。 4. 保证可扩展性和可维护性。 ### DDD分层架构 我们参考传统DDD分层架构,并结合清洁架构(Clean Architecture)的思想,设计如下层次: 1. **用户界面层(Interface Layer)**:负责处理用户请求和返回响应,例如Web控制器。 2. **应用层(Application Layer)**:协调领域层对象,实现用例逻辑。它不包含业务规则,而是将任务委托给领域层。 3. **领域层(Domain Layer)**:包含业务逻辑的核心,包括实体、值对象、领域服务、领域事件等。这里我们将使用充血模型,将业务逻辑放在实体中。 4. **基础设施层(Infrastructure Layer)**:提供技术实现,如数据库持久化、消息队列等。 ### 动态表单领域模型 在参数配置中心,动态表单的核心领域概念包括: - **表单(Form)**:一个表单由多个字段组成,包含表单的基本信息(如名称、编码等)。 - **表单字段(FormField)**:定义字段的属性,如字段名、类型(文本、数字、下拉框等)、验证规则、是否必填等。 - **表单布局(FormLayout)**:定义字段在表单中的排列方式(如行、列、分组等)。 #### 领域实体设计充血模型) 在充血模型中,实体不仅包含数据属性,还包含与这些数据相关的业务逻辑。例如: 1. **表单实体(Form)**: - 属性:表单ID、名称、编码、描述、状态(启用/禁用)、创建时间等。 - 行为: -字段(addField):添一个字段到表单中,并验证字段的唯一性。 - 移除字段(removeField):从表单中移除指定字段。 - 发布表单(publish):将表单状态设置为启用,并验证表单的完整性(例如,至少有一个字段)。 - 禁用表单(disable):将表单状态设置为禁用。 2. **表单字段实体(FormField)**: - 属性:字段ID、字段名称、字段编码(唯一标识)、字段类型、默认值、验证规则(如正则表达式)、是否必填、是否唯一等。 - 行为: - 验证字段值(validate):根据字段的验证规则验证输入值是否合法。 - 修改字段类型(changeType):修改字段类型,并重置相关的验证规则(例如,将文本类型改为数字类型时,重置验证规则为数字规则)。 3. **表单布局值对象(FormLayout)**: - 由于布局可能仅作为配置信息,不包含复杂行为,可以设计为值对象。它描述字段的排列顺序和分组信息。 ### 领域服务 当业务逻辑涉及多个实体时,使用领域服务。例如: - **表单配置服务(FormConfigurationService)**:用于处理跨多个表单和字段的操作,如复制表单配置、批量更新字段等。 ### 应用层服务 应用层服务负责协调领域层对象,处理事务、安全等。例如: - **表单应用服务(FormAppService)**: - 提供创建表单、更新表单、发布表单等用例。 - 调用领域实体领域服务。 ### 基础设施层 - **持久化**:使用仓储模式(Repository)实现表单和字段的持久化。仓储接口定义在领域层,实现在基础设施层。 - **动态表单渲染**:提供将表单配置渲染为前端表单的能力(如生成JSON Schema)。 ### 动态表单配置的存储 我们可以将表单配置存储在数据库中,设计如下表结构(仅为示例): - **form_table**:存储表单基本信息。 - id, name, code, status, created_time, updated_time - **form_field_table**:存储字段信息。 - id, form_id, field_name, field_code, field_type, default_value, validation_rules, is_required, is_unique, order_index - **form_layout_table**(可选):存储布局信息,如字段分组、行列位置等。 ### 代码示例(领域实体) 以下是一个简化的表单实体和字段实体的代码示例(使用Java,但重点在模型设计): ```java // 表单实体 public class Form { private String id; private String name; private String code; private boolean enabled; private List<FormField> fields = new ArrayList<>(); // 添字段 public void addField(FormField field) { // 检查字段编码是否重复 if (fields.stream().anyMatch(f -> f.getCode().equals(field.getCode()))) { throw new IllegalArgumentException("字段编码重复"); } fields.add(field); } public void removeField(String fieldCode) { fields.removeIf(field -> field.getCode().equals(fieldCode)); } public void publish() { if (fields.isEmpty()) { throw new IllegalStateException("表单必须包含至少一个字段"); } this.enabled = true; } public void disable() { this.enabled = false; } } // 表单字段实体 public class FormField { private String id; private String name; private String code; private FieldType type; private String defaultValue; private String validationRule; private boolean required; private boolean unique; public boolean validate(String value) { // 根据类型和验证规则验证值 // 例如,如果是数字类型,验证是否为数字;如果有正则表达式,用正则验证 // 返回验证结果 return true; } public void changeType(FieldType newType) { this.type = newType; // 重置验证规则为默认规则(根据新类型) this.validationRule = newType.getDefaultValidationRule(); } } // 字段类型枚举 public enum FieldType { TEXT, NUMBER, SELECT, DATE; public String getDefaultValidationRule() { // 返回默认的验证规则(正则表达式) switch (this) { case NUMBER: return "^\\d+$"; // ... 其他类型 default: return null; } } } ``` ### 上下文映射和微服务划分 参数配置中心可以作为一个独立的微服务,负责管理所有动态表单的配置。它与其他微服务(如业务服务)的交互通过API或事件进行。例如,业务服务可以通过参数配置中心的API获取表单配置,然后渲染表单。 ### 总结 通过以上设计,我们实现了: - 使用DDD分层架构,明确各层职责。 - 采用充血模型,将业务逻辑封装在领域实体中。 - 支持动态表单的配置和扩展。 §§相关问题§§ 1. 如何实现动态表单的版本控制? 2. 在DDD中如何处理动态表单的跨领域业务逻辑? 3. 如何设计动态表单的渲染引擎?</think>基于充血模型领域驱动设计(DDD)的参数配置中心动态表单架构设计,需结合领域模型的核心业务逻辑封装和分层架构思想。以下是分步设计--- ### 一、架构分层设计 1. **用户界面层** - 提供表单配置的RESTful API和前端管理界面 - 接收动态表单的创建/修改请求,返回渲染后的表单数据 ```typescript // 示例:表单配置API POST /api/forms/{formId}/fields Body: { fieldType: "number", validationRules: ["min=0"] } ``` 2. **应用层** - 协调领域层服务,处理事务边界 - 实现表单发布、版本控制等应用逻辑 ```java public class FormAppService { @Transactional public void publishForm(String formId) { Form form = formRepository.findById(formId); domainService.validateFormCompleteness(form); // 调用领域服务 form.publish(); // 调用领域实体充血方法 } } ``` 3. **领域层(核心)** **充血模型实现**: - **实体**:`Form`(表单)、`FormField`(字段) - **值对象**:`ValidationRule`(验证规则)、`LayoutConfig`(布局配置) - **领域服务**:`FormCompositionService`(处理跨实体逻辑) ```java // 表单实体充血模型) public class Form { private String id; private List<FormField> fields; private FormStatus status; // 充血方法:业务逻辑内聚 public void addField(FormField field) { if (this.status == FormStatus.PUBLISHED) { throw new DomainException("已发布表单不可修改"); } this.fields.add(field); } public void publish() { this.status = FormStatus.PUBLISHED; DomainEventPublisher.publish(new FormPublishedEvent(this.id)); // 领域事件 } } // 字段实体 public class FormField { private String name; private FieldType type; private List<ValidationRule> rules; // 充血方法:字段级验证 public boolean validate(Object input) { return rules.stream().allMatch(rule -> rule.check(input)); } } ``` 4. **基础设施层** - 仓储实现:`FormRepositoryImpl`(数据库持久化) - 集成外部服务:JSON Schema生成器、规则引擎等 --- ### 二、动态表单关键设计 1. **动态字段建模** - 使用`FieldType`枚举支持文本/数字/下拉框等类型 - 值对象`ValidationRule`封装规则(如正则表达式、范围校验) ```java public class ValidationRule { private RuleType type; private String expression; // 如 "min=0;max=100" } ``` 2. **动态渲染机制** - 领域服务`FormRenderService`将`Form`实体转换为JSON Schema - 前端通过统一接口获取Schema并自动渲染 ```json // 输出示例 { "fields": [ { "name": "age", "type": "number", "rules": ["min=18"] } ] } ``` 3. **变更控制** - 领域事件`FormPublishedEvent`触发配置生效 - 版本管理:`Form`实体关联`version`值对象,支持历史配置回溯 --- ### 三、技术实现要点 1. **聚合根设计** - `Form`作为聚合根,管理字段的增删改,确保一致性边界 2. **工厂模式** - `FormFactory`负责复杂表单的创建逻辑 3. **防腐层(ACL)** - 转换外部规则引擎数据为领域内的`ValidationRule`对象 --- ### 四、优势与验证 - **充血模型优势**: - 表单完整性校验(`validateFormCompleteness()`)内聚在领域服务 - 字段验证逻辑封装在`FormField.validate()`,避免贫血模型 - **DDD分层价值**: - 领域层独立于基础设施,支持更换存储方式(如DB → NoSQL) - 领域事件解耦表单发布与下游通知 > 引用说明:领域层通过实体充血模型实现业务逻辑内聚[^1],六边形架构确保领域层与技术实现解耦[^2],上下文映射明确微服务边界[^3]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值