在这个万物互联的时代,物联网业务蓬勃发展,但也瞬息万变,对于开发人员来讲,这是一种挑战,但也是一种“折磨”。html
在业务发展初期,由于时间有限,咱们通常会遵循“小步快跑,迭代试错”的原则进行业务开发,用通俗的话来讲就是“no bb,先上了再说”,对于开发人员的具体实现,就是“脚本式”的开发方式,或者说是数据的 CURD,这样的开发方式,在项目早期没什么问题,但随着新业务的不断加入,业务迭代的频繁,咱们会发现,如今的业务系统变得愈来愈冗杂,新加一个需求或者改一个业务,变得无比困难,由于业务实现彼此之间模糊不清,业务规则在代码中无处不在,开发人员也就无从下手。前端
那怎么解决上面的问题呢?可能不少人会说“你这代码不行,重构呀”,是的,咱们发现了项目中的“坏代码”,好比一个类上千行,一个方法几百行,因而咱们把一些代码抽离出来,作一些内聚的实现,代码规范作一些调整,但这样只是解决如今项目代码中的问题,下次项目迭代的时候,你并不能保证写的新代码是符合规范的,并且最重要的是,重构并不能在业务代码上给一个定义,什么意思呢?好比你重构一个方法,你只能从技术的角度去重构它,并不能从业务的角度去重构,由于在整个业务系统“混乱”的状况下,你没法保证本身的“清白”。另外还有一点,即便你重构了它,但对于新加入的开发人员来讲,他并不能理解你重构的目的,换句话说,就是若是他要使用或改这个方法,他彻底不知道能不能使用或者使用了会不会影响其余业务,说白了就是,业务的边界不明确。java
那如何定义业务的边界呢?答案就是运用 Eric Evans 提出的领域驱动设计(Domain Driven Design,简称 DDD),关于 DDD 的相关概念,这边就不叙述了,网上有不少资料,须要注意的是,DDD 关注的是业务设计,并不是技术实现。数据库
物联网业务如何应用领域驱动设计?这实际上是个大命题,该怎么实现?如何下手呢?我找了我以前作的一个业务需求,来作示例,看看“脚本式”的实现,和 DDD 的实现,先后有什么不太同样的地方。bash
业务需求:针对物联网卡的当前套餐使用量,根据必定的规则,进行特定的限速设置。mybatis
需求看起来很简单,下面要具体实现了,首先,我建立了三张表:架构
speed_limit
:限速表,包含用户 ID、套餐 ID 等。speed_limit_config
:限速配置表,包含限速档位,也就是套餐使用量在什么区间,限速多少的配置。speed_limit_level
:限速级别表,包含限速的单位和具体值,主要界面选择使用。而后再建立对应“贫血”的模型对象(没有任何行为,而且属性和数据库字段一一对应):app
public class SpeedLimit { private Long id; private Integer orgId; private Long priceOfferId; //getter setter.... } public class SpeedLimitConfig { private Long id; private Long speedLimitId; private Double usageStart; private Double usageEnd; //getter setter.... } public class SpeedLimitLevel { private Long id; private String unit; private Double value; //getter setter.... }
好,数据库表和模型对象都建立好了,接下来作什么呢?CURD 啊,界面须要对这些数据进行查看和维护,因此,我建立了:dom
SpeedLimitMapper.xml
:数据库访问 SQL。SpeedLimitService.java
:调用 Mapper,并返回数据。SpeedLimitController.java
:接受前端传递参数,并调用 Service,封装返回数据。简单看下SpeedLimitService.java
中的代码:异步
public interface SpeedLimitService { List<SpeedLimit> listAll(); SpeedLimitVO getById(Long id); Boolean insert(Integer orgId, Long priceOfferId, List<SpeedLimitConfig> speedLimitConfigs); //... }
CURD 流程没啥问题吧,数据维护好了,接下来要进行限速检查了,咱们目前的实现方式是:有一个定时任务,每间隔一段时间批量执行,查询全部的限速配置(上面的speed_limit
),而后根据用户 ID 和套餐 ID,查询出符合条件的物联网卡,而后将卡号丢到 MQ 中异步处理,MQ 接受到卡号,再查询对应的限速配置(speed_limit
以及speed_limit_config
),而后再查询此卡的套餐使用量,最后根据规则匹配,进行限速设置等操做。
MQ 中的处理代码(阿里插件都已经提醒我,这个方法代码太长了):
为何代码不贴出来?由于里面的代码惨不忍睹啊,if..else..
的各类嵌套,因此,仍是眼不看为净。。。
好,到此为止,这个需求已经“脚本式”的开发完了,咱们来总结一把:
没啥问题对吧?好,如今业务迭代来了,产品经理发话了,说除了批量限速检查,还须要对单卡的限速同步处理(瞎掰的),由于是同步处理,因此我没办法发消息到 MQ 处理,只能对 MQ 中的那一坨代码进行重构,代码抽离的过程当中发现,并不能兼容新的需求,怎么搞呢?只能又重载了一个方法,把里面能抽离的抽离出来,改好以后,需求完美上线。
过了一天,产品经理又发话了。。。
而后,我把产品经理打死了。。。
为了避免我和产品经理打架,我须要作一些改变,就事论事,毕竟问题出在开发这边,面对“一锅乱粥”的代码,我决定用 DDD 这把“武器”进行改造它。
咱们知道,DDD 分为战略设计和战术设计,战略设计就是把限界上下文和核心领域搞出来,而后针对某个限界上下文,再利用战术设计进行具体的实现,这个过程通常是针对一个完整复杂的业务系统,涉及的东西不少,你可能须要和领域专家进行深刻沟通,若有必要还需画出业务领域图、限界上下文图、限界上下文映射图等等,以便理解。
针对限速设置的业务需求,我简单画了下所涉及的上下文映射图:
能够看到,咱们关注的只有一个限速上下文,物联网卡上下文、套餐流量上下文和运营商 API 上下文,咱们并不须要关心,ACL 的意思是防腐层(Anticorruption Layer),它的做用就是隔离各个上下文,以及协调上下文之间的通讯。
限速上下文内部的实现(如聚合根和实体等),其实就是战术设计的具体实现,关于概念这边就很少说了,这里说下具体的设计:
SpeedLimit
聚合根:毫无疑问,限速上下文的聚合根是限速聚合根,也能够称之为聚合根实体,这里的SpeedLimit
并非上面贫血的模型对象,而是包含限速业务逻辑的聚合对象。SpeedLimitConfig
实体:限速配置实体,在生命周期内有惟一的标识,而且依附于限速聚合根。SpeedLimitLevel
实体:其实限速级别应该设计成值对象,由于它并无生命周期和惟一标识的概念,只是一个具体的值。SpeedLimitContext
值对象:限速上下文,只包含具体的值,做用就是从应用层发起调用到领域层,能够看作是传输对象。SpeedLimitService
领域服务:由于涉及到多个上下文的协调和交互,限速聚合根并不能独立完成,因此这些聚合根完成不了的操做,能够放到领域服务中去处理。SpeedLimitRepository
仓储:限速聚合对象的管理中心,能够数据库存储,也能够其余方式存储,不要把Mapper
接口定义为Repository
接口。以上由于很差在现有项目中作改造,我就用 Spring Boot 作了一个项目示例(Spring Boot 用起来真的很爽,简洁高效,作微服务很是好),大体的项目结构:
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── qipeng
│ │ │ └── simboss
│ │ │ └── speedlimit
│ │ │ ├── SpeedLimitApplication.java
│ │ │ ├── application
│ │ │ │ ├── dto
│ │ │ │ └── service
│ │ │ │ ├── SpeedLimitApplicationService.java
│ │ │ │ └── impl
│ │ │ │ └── SpeedLimitApplicationServiceImpl.java
│ │ │ ├── domain
│ │ │ │ ├── aggregate
│ │ │ │ │ └── SpeedLimit.java
│ │ │ │ ├── entity
│ │ │ │ │ ├── SpeedLimitConfig.java
│ │ │ │ │ └── SpeedLimitLevel.java
│ │ │ │ ├── service
│ │ │ │ │ ├── SpeedLimitService.java
│ │ │ │ │ └── impl
│ │ │ │ │ └── SpeedLimitServiceImpl.java
│ │ │ │ └── valobj
│ │ │ │ └── SpeedLimitCheckContext.java
│ │ │ ├── facade
│ │ │ │ ├── CarrierApiFacade.java
│ │ │ │ ├── DeviceRatePlanFacade.java
│ │ │ │ ├── IotCardFacade.java
│ │ │ │ └── model
│ │ │ │ ├── CarrierConstants.java
│ │ │ │ ├── DeviceRatePlan.java
│ │ │ │ ├── EnumTemplate.java
│ │ │ │ ├── IotCard.java
│ │ │ │ └── SpeedLimitAction.java
│ │ │ └── repo
│ │ │ ├── dao
│ │ │ │ └── SpeedLimitDao.java
│ │ │ └── repository
│ │ │ └── SpeedLimitRepository.java
│ │ └── resources
│ │ ├── application.yml
│ │ ├── mybatis
│ │ │ ├── mapper
│ │ │ │ └── SpeedLimitMapper.xml
│ │ │ └── mybatis-config.xml
│ └── test │ └── java │ └── com │ └── qipeng │ └── simboss │ └── speedlimit │ ├── SpeedLimitApplicationTests.java │ ├── application │ │ └── SpeedLimitApplicationServiceTest.java │ └── domain │ └── SpeedLimitServiceTest.java
包路径:
import com.qipeng.simboss.speedlimit.domain.aggregate.SpeedLimit;//聚合根 import com.qipeng.simboss.speedlimit.domain.entity.*;//实体 import com.qipeng.simboss.speedlimit.domain.valobj.*;//值对象 import com.qipeng.simboss.speedlimit.domain.service.*;//领域服务 import com.qipeng.simboss.speedlimit.domain.repo.repository.*;//仓储 import com.qipeng.simboss.speedlimit.repo.dao.*;//mapper接口 import com.qipeng.simboss.speedlimit.application.service.*;//应用层服务
好,基本上这个项目设计的差很少了,须要注意的是,上面核心是com.qipeng.simboss.speedlimit.domain
包,里面包含了最重要的业务逻辑处理,其余都是为此服务的,另外,在领域模型不断完善的过程当中,须要持续对领域模型进行单元测试,以保证其健壮性,而且,设计SpeedLimit
聚合根的时候,不要先考虑数据库的实现,若是须要数据进行测试,能够在SpeedLimitRepository
中 Mock 对应的数据。
看下SpeedLimit
聚合根中的代码:
package com.qipeng.simboss.speedlimit.domain.aggregate; import com.qipeng.simboss.speedlimit.domain.entity.SpeedLimitConfig; import com.qipeng.simboss.speedlimit.facade.model.IotCard; import lombok.Data; import java.util.Date; import java.util.List; /** * 限速聚合根 */ @Data public class SpeedLimit { /** * 限速 */ private Long id; /** * 组织ID */ private Integer orgId; /** * 套餐ID */ private Long priceOfferId; /** * 限速配置集合 */ private List<SpeedLimitConfig> configs; /** * 是否删除当前限速,不持久化 */ private Boolean isDel = false; /** * 卡的限速值,不持久化 */ private Double cardSpeedLimit; /** * 获取限速值 */ public Double chooseSpeedLimit(Double usageDataVolume, Double totalDataVolume, Long cardPoolId, Boolean isRealnamePassed, Double currentSpeedLimit) { //todo this... } /** * 设置是否删除当前限速 */ private void setIsDelSpeedLimit(Double currentSpeedLimit) { //判断当前限速是否存在,若是存在,则删除现有的限速配置 //todo this... } }
上面注释写的比较多(方便理解),SpeedLimit
聚合根和以前的SpeedLimit
贫血对象相比,主要有如下改动:
SpeedLimit
聚合根并不仅是包含getter
和setter
,还包含了业务行为,而且也不和数据库表一一对应。SpeedLimit
聚合根中包含configs
对象(限速配置集合),由于限速配置实体依附于SpeedLimit
聚合根。SpeedLimit
聚合根中的chooseSpeedLimit
方法,意思是根据某种规则从限速配置中,选取当前要限速的值,这是限速的核心业务逻辑。那为何不把整个限速设置的逻辑写在SpeedLimit
聚合根中?而只是实现选取要限速的值呢?为何?为何?为何?
答案很简单,由于限速设置的整个逻辑须要涉及到多个上下文的协做,SpeedLimit
聚合根彻底 Hold 不住呀,因此要把这些逻辑写到限速领域服务中,还有最重要的是,SpeedLimit
聚合根只关注它边界内的业务逻辑,像限速设置的具体后续操做,它不须要关心,那是业务流程须要关心的,也就是限速领域服务须要去协做的。
好,那咱们就看下限速领域服务的具体实现:
package com.qipeng.simboss.speedlimit.domain.service.impl; /** * 限速领域服务 */ @Service public class SpeedLimitServiceImpl implements SpeedLimitService { @Autowired private SpeedLimitRepository speedLimitRepo; @Autowired private IotCardFacade iotCardFacade; @Autowired private DeviceRatePlanFacade deviceRatePlanFacade; @Autowired private CarrierApiFacade carrierApiFacade; /** * 批量限速检查 */ @Override public void batchSpeedLimitCheck() { List<SpeedLimit> speedLimits = speedLimitRepo.listAll(); for (SpeedLimit speedLimit : speedLimits) { List<IotCard> iotCards = iotCardFacade.listByByOrgId(speedLimit.getOrgId(), speedLimit.getPriceOfferId()); for (IotCard iotCard : iotCards) { doSpeedLimitCheck(iotCard, speedLimit); } } } /** * 单个限速检查 */ @Override public void doSpeedLimitCheck(SpeedLimitCheckContext context) { String iccid = context.getIccid(); IotCard iotCard = iotCardFacade.get(iccid); if (iotCard != null) { SpeedLimit speedLimit = speedLimitRepo.get(iotCard.getOrgId(), iotCard.getPriceOfferId()); if (speedLimit != null) { this.doSpeedLimitCheck(iotCard, speedLimit); } } } /** * 执行限速逻辑 * * @param iotCard * @param speedLimit */ private void doSpeedLimitCheck(IotCard iotCard, SpeedLimit speedLimit) { //todo this... notify(iccid, speedLimit.getCardSpeedLimit()); } /** * 修改卡的限速值,并通知用户 */ private void notify(String iccid, Double speedLimit) { if (speedLimit != null) { //todo this... System.out.println("update iotCard SpeedLimit to: " + speedLimit); System.out.println("notify..."); } } }
上面的代码看起来不少,其实干的事并不复杂,主要是业务流程:
SpeedLimitCheckContext
上下文获取iccid
,而后获取对应的限速对象和套餐流量对象。以上限速领域模型基本上比较丰富了,后面的业务迭代只须要改里面的代码便可。
好,咱们再来看下应用服务中的代码:
package com.qipeng.simboss.speedlimit.application.service.impl; @Service public class SpeedLimitApplicationServiceImpl implements SpeedLimitApplicationService { @Autowired private SpeedLimitService speedLimitService; @Override public void batchSpeedLimitCheck() { speedLimitService.batchSpeedLimitCheck(); } @Override public void doSpeedLimitCheck(String iccid) { SpeedLimitCheckContext context = new SpeedLimitCheckContext(); context.setIccid(iccid); speedLimitService.doSpeedLimitCheck(context); } }
应用服务不该包含任何的业务逻辑,只是工做流程的处理,好比接受参数,而后调用相关服务,封装返回等,若是须要持久化聚合根对象,调用仓储服务便可(可能会涉及到 UnitOfWork),另外,像限速聚合根对象的维护,也是实如今应用服务(由于不包含任何业务逻辑),好比建立限速聚合根,过程大概是这样:
SpeedLimitFactory
),或者经过构造函数建立(包含业务规则,不符合则抛出错误),固然建立还包含聚合根附属的实体。那如何改善以前 MQ 中处理的一坨代码呢?答案就是一行代码:
@Test public void doSpeedLimitCheckTest() { System.out.println("start...."); speedLimitApplicationService.doSpeedLimitCheck("1111"); System.out.println("end"); }
没错,调用下应用层的doSpeedLimitCheck
服务便可,调用方彻底不须要关内心面的业务逻辑,业务隔离。
单元测试执行结果:
关于领域驱动设计的分层架构(图片来自):
其实,我我的以为 DDD 的首要核心是肯定业务的边界(领域边界),接着把各个边界之间的关系整理清晰(上下文映射图),而后再针对具体的边界具体设计(战术设计),最后就是工做流程的处理,就像上面图中所表达同样。
好,改造完了,又能够和产品经理一块儿愉快的玩耍了。。。