05-07实现面向对象领域模型-停车案例

学习视频来源:DDD独家秘籍视频合集 https://space.bilibili.com/24690212/channel/collectiondetail?sid=1940048&ctype=0

源代码地址:https://github.com/ByteBlizzard

本篇文章是讲视频中的3期内容合并为一起。

文章目录

  • 需求
  • 模型
    • 命令
    • 聚合
    • 领域事件
    • 查询
    • 领域外功能
  • 源码
    • 1.项目结构
    • 2.领域模块
      • 命令
      • 聚合对象
      • 领域事件
      • 领域事件队列定义
      • 领域事件监听器定义
      • 仓储定义
      • 报警策略监听器
      • 报警策略定义
    • 3.领域外模块
      • 接口
      • 命令代理类
      • 领域事件分发器
      • 领域事件队列实现
      • 领域事件监听器实现
      • 仓储接口实现
      • 报警策略实现
      • 查询

需求

做一个停车计费程序,具体需求如下:

  • 车牌识别系统会在车辆入场和出场的时候调用计费程序
  • 付费后15分钟内可以离场,超过15分钟要补费
  • 车辆入场出场失败的时候,要给管理员报警
  • 计费程序提供查询当前某个车牌号应付款
  • 支付系统通知计费程序车辆已付款
  • 用户要能够查看某个车牌号过去的停车记录
  • 管理员要查看在场车辆总数,每日营业额
  • 计费规则是每小时1块,不足一小时当成1小时
  • 黑名单限制

模型

在这里插入图片描述

命令

车辆入场、车辆出场、计算费用、付费、添加车辆到黑名单、从黑名单移除车辆。

聚合

聚合对象:停车

领域事件

车辆已出场、车辆已退场、车辆已付费、出场失败、入场失败。可以看到领域事件都是已发生确定的有意义事件 。

查询

在场数量、停车记、每日收入。

领域外功能

报警策略。和仓储一样,属于外的东西,只在领域内定义和使用接口,但是具体实现是在领域外。

源码

1.项目结构

在这里插入图片描述
项目分为领域模块和领域外模块,领域外模块依赖领域模块。
领域模块是我们的核心功能,包含命令、处理命令的handler、聚合、领域事件、存储和发送领域事件队列的定义、领域事件监听器的定义、仓储相关定义、报警策略定义。
领域外模块包括接口、仓储、报警策略实现、仓储相关接口的实现、发送领域事件队列的实现、领域事件监听器实现。

2.领域模块

命令

定义一个命令和处理这个命令的处理器。命令包括车牌号和入场时间。处理器通过车牌号查询聚合对象(parking),调用聚合的处理入场命令的方法,并保存到数据库。

class CheckInCommand(val plate: Plate,val checkInTime: LocalDateTime
)@Component
class CheckInCommandHandler(private val parkingRepository: ParkingRepository
) {fun handle(eventQueue: EventQueue, command: CheckInCommand): Boolean {val parking = parkingRepository.findByIdOrError(command.plate)val result = parking.handle(eventQueue, command)parkingRepository.save(parking)return result}
}

聚合对象

聚合对象包含项目的核心功能。根据业务逻辑,处理各种命令等等,具体是在handle方法内处理,
handle方法的入参是一个队列,处理命令过程中产生的领域事件,就是放到这里。

interface Parking {fun handle(eventQueue: EventQueue, command: CheckInCommand): Booleanfun calculateFeeNow(now: LocalDateTime): Intfun handle(eventQueue: EventQueue, command: NotifyPayCommand)fun handle(eventQueue: EventQueue, command: CheckOutCommand): Boolean
}const val FEE_PER_HOUR = 1;class ParkingImpl(val id: Plate,var checkInTime: LocalDateTime? = null,var lastPlayTime: LocalDateTime? = null,var totalPaid: Int = 0
): Parking {override fun handle(eventQueue: EventQueue, command: CheckInCommand): Boolean {if (inPark()) {eventQueue.enqueue(CheckInFailedEvent(id, command.checkInTime))return false}eventQueue.enqueue(CheckedInEvent(id, command.checkInTime))this.checkInTime = command.checkInTimereturn true}override fun handle(eventQueue: EventQueue, command: NotifyPayCommand) {if (!inPark()) {throw DomainException("车辆不在场,不能付费")}lastPlayTime = command.payTimetotalPaid += command.amounteventQueue.enqueue(PaidEvent(plate = id, amount = command.amount, payTime = command.payTime))}override fun handle(eventQueue: EventQueue, command: CheckOutCommand): Boolean {if (!inPark()) {eventQueue.enqueue(CheckOutFailedEvent(plate = id, time = command.time, message = "车辆不在场"))return false}if (calculateFeeNow(command.time) > 0) {return false}this.checkInTime = nullthis.totalPaid = 0this.lastPlayTime= nulleventQueue.enqueue(CheckedOutEvent(plate = id, time = command.time))return true}override fun calculateFeeNow(now: LocalDateTime): Int {val currentCheckInTime = checkInTime ?: throw DomainException("车辆尚未入场")val lastPayTimeCurrent = lastPlayTime ?: return feeBetween(currentCheckInTime, now)if (totalPaid < feeBetween(currentCheckInTime, lastPayTimeCurrent)) {return feeBetween(currentCheckInTime, now) - totalPaid}if (lastPayTimeCurrent.plusMinutes(15).isAfter(now)) {return 0}return feeBetween(currentCheckInTime, now) - totalPaid}private fun feeBetween(start: LocalDateTime, end: LocalDateTime): Int {return hoursBetween(start, end) * FEE_PER_HOUR}private fun hoursBetween(start: LocalDateTime, end: LocalDateTime): Int {val minutes = Duration.between(start, end).toMinutes()val hours = minutes / 60return (if (hours * 60 == minutes) hours else hours + 1).toInt()}fun inPark(): Boolean {return checkInTime != null}
}

领域事件

class CheckedInEvent(val plate: Plate,val time: LocalDateTime
): DomainEvent

领域事件队列定义

在领域模块仅仅是定义,实现在领域外。

interface EventQueue {fun enqueue(event: DomainEvent)fun queue(): List<DomainEvent>
}

领域事件监听器定义

我觉得这个也可以定义到领域外模块主要是报警策略的定义用到了监听器,所以只能放领域内了。

interface DomainEventListener {fun onEvent(event: DomainEvent)
}

仓储定义

仓储在领域模块内仅仅是定义,实现在领域外模块。

interface ParkingRepository {fun findByIdOrError(plate: Plate): Parkingfun save(parking: Parking)
}

报警策略监听器

class AlarmPolicy(private val alarmService: AlarmService
) : DomainEventListener{override fun onEvent(event: DomainEvent) {if (event is CheckInFailedEvent) {alarmService.alarm(event.plate, "入场失败")return}if (event is CheckOutFailedEvent) {alarmService.alarm(event.plate, event.message)}}
}

报警策略定义

interface AlarmService {fun alarm(plate: Plate, message: String)
}

3.领域外模块

接口

将请求转成命令,CheckInReq->CheckInCommand,通过一个代理类,调用事件handler处理命令。

@Controller
class CheckInController(private val checkInCommandHandler: CheckInCommandHandler,private val commandInvoker: CommandInvoker
) {@MutationMappingfun checkIn(@Argument("req") req: CheckInReq): Boolean {return commandInvoker.invoke {checkInCommandHandler.handle(it,CheckInCommand(plate = Plate(req.plate),checkInTime = LocalDateTime.parse(req.time)))}}
}class CheckInReq(val plate: String,val time: String
)

命令代理类

使用代理类主要是为了简化代码。因为每个命令都是调相关的handler,把处理命令过程中的事件存储到事件eventQueue 队列中,最后调用时间分发器domainEventDispatcher把队列中的事件分发出去。每个命令的处理流程都是这几个步骤,且这几个步骤在一个事务之中,所以这里用了代理类。

interface CommandInvoker {fun <R> invoke(run: (EventQueue) -> R): R
}@Component
class OneTransactionCommandInvoker(transactionManager: PlatformTransactionManager,private val domainEventDispatcher: DomainEventDispatcher
) : CommandInvoker {private val transactionTemplate: TransactionTemplate = TransactionTemplate(transactionManager)override fun <R> invoke(run: (EventQueue) -> R): R {return transactionTemplate.execute { s ->val eventQueue = SimpleEventQueue()val result = run(eventQueue)this.domainEventDispatcher.dispatchNow(eventQueue)return@execute result} ?: throw IllegalStateException()}}

领域事件分发器

队列中的每一个事件,分发给每一个事件监听器。监听器会接收每一个事件,但只处理自己关心的事件,不关心的不处理。

@Component
class SyncDomainEventDispatcher(private val listeners: List<DomainEventListener>
) : DomainEventDispatcher {override fun dispatchNow(eventQueue: EventQueue) {eventQueue.queue().forEach { event ->listeners.forEach { listener ->listener.onEvent(event)}}}
}

领域事件队列实现

用一个LinkedList模拟消息队列,比较简单。

class SimpleEventQueue: EventQueue {private val queue: MutableList<DomainEvent> = LinkedList()override fun enqueue(event: DomainEvent) {queue.add(event)}override fun queue(): List<DomainEvent> {return queue}
}

领域事件监听器实现

监听不同的事件,并生产查询记录。用于CQRS架构查询模型查询。只处理自己关心的事件,不关心的不处理。

@Component
class ParkingHistoryMaterializer(private val parkingViewDao: ParkingViewDao
): DomainEventListener {override fun onEvent(event: DomainEvent) {when (event) {is CheckedInEvent -> insertHistory(event)is CheckedOutEvent -> updateOnCheckOut(event)is PaidEvent -> updateOnPaid(event)}}private fun updateOnPaid(event: PaidEvent) {val parkingViewRecord = parkingViewDao.findTopByPlateOrderByIdDesc(event.plate.value)parkingViewRecord.payAmount += event.amountparkingViewDao.save(parkingViewRecord)}private fun updateOnCheckOut(event: CheckedOutEvent) {val parkingViewRecord = parkingViewDao.findTopByPlateOrderByIdDesc(event.plate.value)parkingViewRecord.checkOutTime = event.timeparkingViewDao.save(parkingViewRecord)}private fun insertHistory(event: CheckedInEvent) {parkingViewDao.save(ParkingViewTable(id = null,plate = event.plate.value,checkInTime = event.time,checkOutTime = null,payAmount = 0))}
}

仓储接口实现

实现业务需要与数据库的交互的接口。

class ParkingRepositoryImpl(private val parkingDao: ParkingDao
): ParkingRepository {override fun findByIdOrError(plate: Plate): Parking {return parkingDao.findById(plate.value).map {ParkingImpl(id = plate,checkInTime = it.checkInTime,lastPlayTime = it.lastPlayTime,totalPaid = it.totalPaid)}.orElse(ParkingImpl(id = plate,checkInTime = null))}override fun save(parking: Parking) {if (parking !is ParkingImpl) {throw UnsupportedOperationException("不支持的类型: ${parking.javaClass}")}parkingDao.save(ParkingTable(id = parking.id.value,checkInTime = parking.checkInTime,lastPlayTime = parking.lastPlayTime,totalPaid = parking.totalPaid))}
}

报警策略实现

属于领域之外的模块,其定义AlarmService也是在领域模块内的。

@Component
class AlarmServiceDBImpl(private val alarmDao: AlarmDao
) : AlarmService {override fun alarm(plate: Plate, message: String) {alarmDao.save(AlarmTable(id = null,plate = plate.value,msg = message,time = LocalDateTime.now()))}
}

查询

项目用到了CQRS同事务存储分离的架构方式。这里就是查询功能,只是对数据库做一些统计,比较简单。

@Component
class ParkingHistoryMaterializer(private val parkingViewDao: ParkingViewDao
): DomainEventListener {override fun onEvent(event: DomainEvent) {when (event) {is CheckedInEvent -> insertHistory(event)is CheckedOutEvent -> updateOnCheckOut(event)is PaidEvent -> updateOnPaid(event)}}private fun updateOnPaid(event: PaidEvent) {val parkingViewRecord = parkingViewDao.findTopByPlateOrderByIdDesc(event.plate.value)parkingViewRecord.payAmount += event.amountparkingViewDao.save(parkingViewRecord)}private fun updateOnCheckOut(event: CheckedOutEvent) {val parkingViewRecord = parkingViewDao.findTopByPlateOrderByIdDesc(event.plate.value)parkingViewRecord.checkOutTime = event.timeparkingViewDao.save(parkingViewRecord)}private fun insertHistory(event: CheckedInEvent) {parkingViewDao.save(ParkingViewTable(id = null,plate = event.plate.value,checkInTime = event.time,checkOutTime = null,payAmount = 0))}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/461921.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何批量裁剪图片?5个软件帮助你快速进行图片裁剪

如何批量裁剪图片&#xff1f;5个软件帮助你快速进行图片裁剪 批量裁剪图片可以通过多种工具轻松实现&#xff0c;以下5个软件可以帮助你快速裁剪大量图片&#xff1a; 万能图片编辑器 这是一款支持批量图像处理的多功能小工具&#xff0c;功能丰富且界面简单&#xff0c;支持…

vxe-table v4.8+ 与 v3.10+ 虚拟滚动支持动态行高,虚拟渲染更快了

Vxe UI vue vxe-table v4.8 与 v3.10 解决了老版本虚拟滚动不支持动态行高的问题&#xff0c;重构了虚拟渲染&#xff0c;渲染性能大幅提升了&#xff0c;行高自适应和列宽拖动都支持&#xff0c;大幅降低虚拟渲染过程中的滚动白屏&#xff0c;大量数据列表滚动更加流畅。 自适…

期权懂|开通ETF股票期权需要什么条件?ETF股票期权佣金是多少?

期权小懂每日分享期权知识&#xff0c;帮助期权新手及时有效地掌握即市趋势与新资讯&#xff01; 开通ETF股票期权需要什么条件&#xff1f;ETF股票期权佣金是多少&#xff1f; 一、开通ETF股票期权需满足以下条件&#xff1a; ‌&#xff08;1&#xff09;资金要求‌&#xf…

Lucene的概述与应用场景(1)

文章目录 第1章 Lucene概述1.1 搜索的实现方案1.1.1 传统实现方案1.1.2 Lucene实现方案 1.2 数据查询方法1.1.1 顺序扫描法1.1.2 倒排索引法 1.3 Lucene相关概念1.3.1 文档对象1.3.2 域对象1&#xff09;分词2&#xff09;索引3&#xff09;存储 1.3.3 常用的Field种类 1.4 分词…

在服务器运维过程中,发现服务器时间倒退以及DNS无法解析域名造成yum不可用的问题解决

目录 一.问题描述 二.问题排查过程 2.1yum下载NTP 2.2排查DNS 三.问题解决过程 3.1修复DNS 3.2更新yum源 3.3下载ntp 四.问题解决结果 4.1ntp服务情况检查 4.2服务器时间检查 4.3软件系统时间检查 一.问题描述 对服务器进行运维的过程中&#xff0c;发现服务器时间…

Redis高频面试题

一、Redis有什么好处? 高性能:Redis是一个基于内存的数据存储系统,相比于传统的基于磁盘的数据库系统,它能够提供更高的读写性能。支持丰富的数据类型:Redis支持多种数据结构,包括字符串、哈希、列表、集合、有序集合等,这使得它可以用于多种不同的应用场景。持久化:Re…

[POI2014] PTA-Little Bird(单调队列优化 DP)

luogu 传送门https://www.luogu.com.cn/problem/P3572 解题思路 先设 表示到 的最小劳累值。 很容易得出转移&#xff1a; 其中 由 和 的大小关系决定&#xff0c;并且 。 很显然&#xff0c;直接暴力是 的&#xff0c;会超时。 于是&#xff0c;考虑优化。 我们发现…

如何在Linux系统中使用Apache HTTP Server

如何在Linux系统中使用Apache HTTP Server Apache简介 安装Apache 在Debian/Ubuntu系统中安装 在CentOS/RHEL系统中安装 启动Apache服务 验证Apache是否正在运行 访问Apache默认页面 配置Apache虚拟主机 创建虚拟主机配置文件 示例虚拟主机配置 创建网站根目录 准备静态网站内…

ISME Comm | 西南大学时伟宇团队在功能基因水平揭示植被演替过程中磷限制对土壤微生物碳代谢潜力的抑制作用机制

本文首发于“生态学者”微信公众号&#xff01; 植被群落长期演替过程中&#xff0c;生态系统普遍受养分限制&#xff0c;微生物群落代谢功能在生态系统物质循环中尤为关键。西南大学时伟宇教授团队联合国内外学者&#xff0c;在功能基因水平&#xff0c;将微生物群落功能纳入生…

Unity控制物体透明度的改变

目录标题 效果图代码调用注意事项 效果图 代码 注意&#xff1a;在控制全部的模型进行透视时&#xff0c;已经隐藏的子物体仍然要处理。 using System.Collections; using System.Collections.Generic; using UnityEngine; using DG.Tweening; public class FadeModel {priva…

工业网络监控中的IP保护与软件授权革新

未来的智能工厂离不开稳定而高效的通信网络&#xff0c;这些网络在支撑生产流程的同时&#xff0c;也面临着复杂的管理与安全挑战。PROCENTEC推出了一系列硬件和软件产品&#xff0c;如Atlas、Mercury和Osiris&#xff0c;以提供全面的网络监控和故障排除能力。然而&#xff0c…

springboot 整合 抖音 移动应用 授权

后端开发&#xff0c;因为没有JavaSDK&#xff0c;maven依赖&#xff0c;用到的是API接口去调用 抖音API开发文档 开发前先申请好移动应用&#xff0c;抖音控制台-移动应用 之后还需要开通所有能开通的能力 拿到应用的 clientKey 和 clientSecret&#xff0c;就可以进入开发了 …

后台管理系统的通用权限解决方案(七)SpringBoot整合SpringEvent实现操作日志记录(基于注解和切面实现)

1 Spring Event框架 除了记录程序运行日志&#xff0c;在实际项目中一般还会记录操作日志&#xff0c;包括操作类型、操作时间、操作员、管理员IP、操作原因等等&#xff08;一般叫审计&#xff09;。 操作日志一般保存在数据库&#xff0c;方便管理员查询。通常的做法在每个…

视频设备一体化监控运维方案

随着平安城市、雪亮工程等项目建设的号召&#xff0c;视频监控系统的建设如火如荼地开展。无论在公共场所、企业单位、住宅小区、矿山工地还是交通枢纽&#xff0c;视频监控系统已成为保障安全、维护秩序和提升管理效率的重要工具。但由于对视频监控系统中的前端设备&#xff0…

二十八、Python基础语法(面向对象-下)

一、self 从函数的语法上来看, self 是形参 , 是一个普通的参数,那么在调用的时候,就需要传递实参值。从调用上看, 我们没有给 self 这个形参传递实参值, 但是 Python 解释器会自动的将调用这个方法的对象&#xff0c;作为实参值传递给 self。 class Dog:def eat(self):print…

【Leecode】Leecode刷题之路第37天之解数独

题目出处 37-解数独-题目出处 题目描述 个人解法 思路&#xff1a; todo代码示例&#xff1a;&#xff08;Java&#xff09; todo复杂度分析 todo官方解法 37-解数独-官方解法 方法1&#xff1a;回溯 思路&#xff1a; 代码示例&#xff1a;&#xff08;Java&#xff09; p…

【golang/navmesh】使用recast navigation进行寻路

目录 说在前面安装使用可视化 说在前面 go version&#xff1a;1.20.2 linux/amd64操作系统&#xff1a;wsl2detour-go版本&#xff1a;v0.2.0github&#xff1a;这里&#xff0c;求star! 安装 使用go mod安装即可go get github.com/o0olele/detour-go使用 使用场景模型构建n…

qt QFormLayout详解

QFormLayout 是 Qt 框架中用于创建表单布局的一个类&#xff0c;适合于将标签和输入控件整齐地排列在一起。它可以帮助开发者轻松构建用户输入界面&#xff0c;尤其是在处理表单时。 QFormLayout以两列的形式展示其子项&#xff0c;常用于创建“标签-字段”对的布局。其中&…

电脑小白必看|电脑安装常用软件简单小技巧

前言 最近同事换了新电脑&#xff0c;问我怎么下载常用软件&#xff1f; 我反问了一下&#xff1a;什么常用软件呢&#xff1f; 她说&#xff1a;微信、QQ、钉钉、酷狗、wps这种类型的软件。 哦豁&#xff0c;那其实很简单&#xff0c;但很多人还是没学会。小白之前分享过一…

RocketMQ 消息消费失败的处理机制

在分布式消息系统中&#xff0c;处理消费失败的消息是非常关键的一环。 RocketMQ 提供了一套完整的消息消费失败处理机制&#xff0c;下面我将简要介绍一下其处理逻辑。 截图代码版本&#xff1a;4.9.8 步骤1 当消息消费失败时&#xff0c;RocketMQ会发送一个code为36的请求到…