SIMBOSS:物联网业务如何应用领域驱动设计

在这个万物互联的时代,物联网业务蓬勃发展,但也瞬息万变,对于开发人员来说,这是一种挑战,但也是一种“折磨”。
在业务发展初期,因为时间有限,我们一般会遵循“小步快跑,迭代试错”的原则进行业务开发,用通俗的话来说就是“no bb,先上了再说”,对于开发人员的具体实现,就是“脚本式”的开发方式,或者说是数据的 curd,这样的开发方式,在项目早期没什么问题,但随着新业务的不断加入,业务迭代的频繁,我们会发现,现在的业务系统变得越来越冗杂,新加一个需求或者改一个业务,变得无比困难,因为业务实现彼此之间模糊不清,业务规则在代码中无处不在,开发人员也就无从下手。
那怎么解决上面的问题呢?可能很多人会说“你这代码不行,重构呀”,是的,我们发现了项目中的“坏代码”,比如一个类上千行,一个方法几百行,于是我们把一些代码抽离出来,做一些内聚的实现,代码规范做一些调整,但这样只是解决现在项目代码中的问题,下次项目迭代的时候,你并不能保证写的新代码是符合规范的,而且最重要的是,重构并不能在业务代码上给一个定义,什么意思呢?比如你重构一个方法,你只能从技术的角度去重构它,并不能从业务的角度去重构,因为在整个业务系统“混乱”的情况下,你无法保证自己的“清白”。另外还有一点,即使你重构了它,但对于新加入的开发人员来说,他并不能理解你重构的目的,换句话说,就是如果他要使用或改这个方法,他完全不知道能不能使用或者使用了会不会影响其他业务,说白了就是,业务的边界不明确。
那如何定义业务的边界呢?答案就是运用 eric evans 提出的领域驱动设计(domain driven design,简称 ddd),关于 ddd 的相关概念,这边就不叙述了,网上有很多资料,需要注意的是,ddd 关注的是业务设计,并非技术实现。
物联网业务如何应用领域驱动设计?这其实是个大命题,该怎么实现?如何下手呢?我找了我之前做的一个业务需求,来做示例,看看“脚本式”的实现,和 ddd 的实现,前后有什么不太一样的地方。
脚本式的开发
业务需求:针对物联网卡的当前套餐使用量,根据一定的规则,进行特定的限速设置。
需求看起来很简单,下面要具体实现了,首先,我创建了三张表:
speed_limit:限速表,包含用户 id、套餐 id 等。
speed_limit_config:限速配置表,包含限速档位,也就是套餐使用量在什么区间,限速多少的配置。
speed_limit_level:限速级别表,包含限速的单位和具体值,主要界面选择使用。
然后再创建对应“贫血”的模型对象(没有任何行为,并且属性和数据库字段一一对应):
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 啊,界面需要对这些数据进行查看和维护,所以,我创建了:
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..的各种嵌套,所以,还是眼不看为净。
好,到此为止,这个需求已经“脚本式”的开发完了,我们来总结一把:
条理清晰,开发效率贼高,完全符合“先上了再说”的开发原则。
数据的 curd 和业务逻辑处理隔离开,用到的地方_x001d_“单独处理”,似乎没啥问题。
没啥问题对吧?好,现在业务迭代来了,产品经理发话了,说除了批量限速检查,还需要对单卡的限速同步处理(瞎掰的),因为是同步处理,所以我没办法发消息到 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;
/**
* 限速聚合根
*/@datapublic 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聚合根只关注它边界内的业务逻辑,像限速设置的具体后续操作,它不需要关心,那是业务流程需要关心的,也就是限速_x001d_领域服务需要去协作的。
好,那我们就看下限速领域服务的具体实现:
package com.qipeng.simboss.speedlimit.domain.service.impl;
/**
* 限速领域服务
*/@servicepublic 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;
@servicepublic 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 中处理的一坨代码呢?答案就是一行代码:
@testpublic void dospeedlimitchecktest() {
system.out.println("start....");
speedlimitapplicationservice.dospeedlimitcheck("1111");
system.out.println("end");

没错,调用下应用层的dospeedlimitcheck服务即可,调用方完全不需要关心里面的业务逻辑,业务隔离。
单元测试执行结果:
结语
关于领域驱动设计的分层架构:
其实,我个人觉得 ddd 的首要核心是确定业务的边界(领域边界),接着把各个边界之间的关系整理清晰(上下文映射图),然后再针对具体的边界具体设计(战术设计),最后就是工作流程的处理,就像上面图中所表达一样。
好,改造完了,又可以和产品经理一起愉快的玩耍了。
作者:岳中新

冲着这些就必须升级ios10.2!
USB固件编程学习
薄膜氧化物半导体评估系统的系统概述
亚马逊和苹果出击,谷歌遭遇的机遇和隐患
为何苹果近几年的市场情况大不如前?
SIMBOSS:物联网业务如何应用领域驱动设计
小米6多少钱?2199元起:小米6 Plus顶配卖3699元你信吗?
悄购诺基亚震惊业界 微软冲刺Windows Phone市占
什么是面筋数量和质量测定仪,它的功能特点是什么
吐槽变压器使用寿命短?原因找对了吗?
看这就足够!2017 P&E最受关注的新品相机汇总
安防智能视频分析技术的应用与发展
Altium大中国区CEO沈宇豪称Altium助力中国设计
AGIGA Tech推新型快速NV存储器,无需设置电池
均衡颜值之作,魅族MX6简评
SK海力士的M16工厂将用上最先进的EUV光刻工艺
面向开发者开放系统能力 vivo正式上线政企开放平台
高性能MEMS传感器在无人机中的应用
恩智浦旗舰4D成像雷达芯片投入生产 瑞萨电子推出R-Car V4H SOC
低功耗SensorTag蓝牙智能套件解决设计难题