前言

在这个万物互联的时代,物联网业务蓬勃发展,但也瞬息万变,对于开发人员来说,这是一种挑战,但也是一种“折磨”。

在业务发展初期,因为时间有限,我们一般会遵循“小步快跑,迭代试错”的原则进行业务开发,用通俗的话来说就是“no bb,先上了再说”,对于开发人员的具体实现,就是“脚本式”的开发方式,或者说是数据的 CURD,这样的开发方式,在项目早期没什么问题,但随着新业务的不断加入,业务迭代的频繁,我们会发现,现在的业务系统变得越来越冗杂,新加一个需求或者改一个业务,变得无比困难,因为业务实现彼此之间模糊不清,业务规则在代码中无处不在,开发人员也就无从下手。

那怎么解决上面的问题呢?可能很多人会说“你这代码不行,重构呀”,是的,我们发现了项目中的“坏代码”,比如一个类上千行,一个方法几百行,于是我们把一些代码抽离出来,做一些内聚的实现,代码规范做一些调整,但这样只是解决现在项目代码中的问题,下次项目迭代的时候,你并不能保证写的新代码是符合规范的,而且最重要的是,重构并不能在业务代码上给一个定义,什么意思呢?比如你重构一个方法,你只能从技术的角度去重构它,并不能从业务的角度去重构,因为在整个业务系统“混乱”的情况下,你无法保证自己的“清白”。另外还有一点,即使你重构了它,但对于新加入的开发人员来说,他并不能理解你重构的目的,换句话说,就是如果他要使用或改这个方法,他完全不知道能不能使用或者使用了会不会影响其他业务,说白了就是,业务的边界不明确。

那如何定义业务的边界呢?答案就是运用 Eric Evans 提出的领域驱动设计(Domain Driven Design,简称 DDD),关于 DDD 的相关概念,这边就不叙述了,网上有很多资料,需要注意的是,DDD 关注的是业务设计,并非技术实现。

物联网业务如何应用领域驱动设计?这其实是个大命题,该怎么实现?如何下手呢?我找了我之前做的一个业务需求,来做示例,看看“脚本式”的实现,和 DDD 的实现,前后有什么不太一样的地方。

脚本式的开发

业务需求:针对物联网卡的当前套餐使用量,根据一定的规则,进行特定的限速设置。

需求看起来很简单,下面要具体实现了,首先,我创建了三张表:

  • speed_limit:限速表,包含用户 ID、套餐 ID 等。
  • speed_limit_config:限速配置表,包含限速档位,也就是套餐使用量在什么区间,限速多少的配置。
  • speed_limit_level:限速级别表,包含限速的单位和具体值,主要界面选择使用。

然后再创建对应“贫血”的模型对象(没有任何行为,并且属性和数据库字段一一对应):

  1. public class SpeedLimit {
  2. private Long id;
  3. private Integer orgId;
  4. private Long priceOfferId;
  5. //getter setter....
  6. }
  7. public class SpeedLimitConfig {
  8. private Long id;
  9. private Long speedLimitId;
  10. private Double usageStart;
  11. private Double usageEnd;
  12. //getter setter....
  13. }
  14. public class SpeedLimitLevel {
  15. private Long id;
  16. private String unit;
  17. private Double value;
  18. //getter setter....
  19. }

好,数据库表和模型对象都创建好了,接下来做什么呢?CURD 啊,界面需要对这些数据进行查看和维护,所以,我创建了:

  • SpeedLimitMapper.xml:数据库访问 SQL。
  • SpeedLimitService.java:调用 Mapper,并返回数据。
  • SpeedLimitController.java:接受前端传递参数,并调用 Service,封装返回数据。

简单看下SpeedLimitService.java中的代码:

  1. public interface SpeedLimitService {
  2. List<SpeedLimit> listAll();
  3. SpeedLimitVO getById(Long id);
  4. Boolean insert(Integer orgId, Long priceOfferId, List<SpeedLimitConfig> speedLimitConfigs);
  5. //...
  6. }

CURD 流程没啥问题吧,数据维护好了,接下来要进行限速检查了,我们目前的实现方式是:有一个定时任务,每间隔一段时间批量执行,查询所有的限速配置(上面的speed_limit),然后根据用户 ID 和套餐 ID,查询出符合条件的物联网卡,然后将卡号丢到 MQ 中异步处理,MQ 接受到卡号,再查询对应的限速配置(speed_limit以及speed_limit_config),然后再查询此卡的套餐使用量,最后根据规则匹配,进行限速设置等操作。

MQ 中的处理代码(阿里插件都已经提醒我,这个方法代码太长了):

为什么代码不贴出来?因为里面的代码惨不忍睹啊,if..else..的各种嵌套,所以,还是眼不看为净。。。

好,到此为止,这个需求已经“脚本式”的开发完了,我们来总结一把:

  • 条理清晰,开发效率贼高,完全符合“先上了再说”的开发原则。
  • 数据的 CURD 和业务逻辑处理隔离开,用到的地方“单独处理”,似乎没啥问题。

没啥问题对吧?好,现在业务迭代来了,产品经理发话了,说除了批量限速检查,还需要对单卡的限速同步处理(瞎掰的),因为是同步处理,所以我没办法发消息到 MQ 处理,只能对 MQ 中的那一坨代码进行重构,代码抽离的过程中发现,并不能兼容新的需求,怎么搞呢?只能又重载了一个方法,把里面能抽离的抽离出来,改好之后,需求完美上线。

过了一天,产品经理又发话了。。。

然后,我把产品经理打死了。。。

领域驱动设计

为了避免我和产品经理打架,我需要做一些改变,就事论事,毕竟问题出在开发这边,面对“一锅乱粥”的代码,我决定用 DDD 这把“武器”进行改造它。

我们知道,DDD 分为战略设计和战术设计,战略设计就是把限界上下文和核心领域搞出来,然后针对某个限界上下文,再利用战术设计进行具体的实现,这个过程一般是针对一个完整复杂的业务系统,涉及的东西很多,你可能需要和领域专家进行深入沟通,如有必要还需画出业务领域图、限界上下文图、限界上下文映射图等等,以便理解。

针对限速设置的业务需求,我简单画了下所涉及的上下文映射图:

可以看到,我们关注的只有一个限速上下文,物联网卡上下文、套餐流量上下文和运营商 API 上下文,我们并不需要关心,ACL 的意思是防腐层(Anticorruption Layer),它的作用就是隔离各个上下文,以及协调上下文之间的通信。

限速上下文内部的实现(如聚合根和实体等),其实就是战术设计的具体实现,关于概念这边就不多说了,这里说下具体的设计:

  • SpeedLimit聚合根:毫无疑问,限速上下文的聚合根是限速聚合根,也可以称之为聚合根实体,这里的SpeedLimit并不是上面贫血的模型对象,而是包含限速业务逻辑的聚合对象。
  • SpeedLimitConfig实体:限速配置实体,在生命周期内有唯一的标识,并且依附于限速聚合根。
  • SpeedLimitLevel实体:其实限速级别应该设计成值对象,因为它并没有生命周期和唯一标识的概念,只是一个具体的值。
  • SpeedLimitContext值对象:限速上下文,只包含具体的值,作用就是从应用层发起调用到领域层,可以看做是传输对象。
  • SpeedLimitService领域服务:因为涉及到多个上下文的协调和交互,限速聚合根并不能独立完成,所以这些聚合根完成不了的操作,可以放到领域服务中去处理。
  • SpeedLimitRepository仓储:限速聚合对象的管理中心,可以数据库存储,也可以其他方式存储,不要把Mapper接口定义为Repository接口。

以上因为不好在现有项目中做改造,我就用 Spring Boot 做了一个项目示例(Spring Boot 用起来真的很爽,简洁高效,做微服务非常好),大致的项目结构:

  1. ├── src
  2.    ├── main
  3.       ├── java
  4.          └── com
  5.          └── qipeng
  6.          └── simboss
  7.          └── speedlimit
  8.          ├── SpeedLimitApplication.java
  9.          ├── application
  10.             ├── dto
  11.             └── service
  12.             ├── SpeedLimitApplicationService.java
  13.             └── impl
  14.             └── SpeedLimitApplicationServiceImpl.java
  15.          ├── domain
  16.             ├── aggregate
  17.                └── SpeedLimit.java
  18.             ├── entity
  19.                ├── SpeedLimitConfig.java
  20.                └── SpeedLimitLevel.java
  21.             ├── service
  22.                ├── SpeedLimitService.java
  23.                └── impl
  24.                └── SpeedLimitServiceImpl.java
  25.             └── valobj
  26.             └── SpeedLimitCheckContext.java
  27.          ├── facade
  28.             ├── CarrierApiFacade.java
  29.             ├── DeviceRatePlanFacade.java
  30.             ├── IotCardFacade.java
  31.             └── model
  32.             ├── CarrierConstants.java
  33.             ├── DeviceRatePlan.java
  34.             ├── EnumTemplate.java
  35.             ├── IotCard.java
  36.             └── SpeedLimitAction.java
  37.          └── repo
  38.          ├── dao
  39.             └── SpeedLimitDao.java
  40.          └── repository
  41.          └── SpeedLimitRepository.java
  42.       └── resources
  43.       ├── application.yml
  44.       ├── mybatis
  45.          ├── mapper
  46.             └── SpeedLimitMapper.xml
  47.          └── mybatis-config.xml
  48.    └── test
  49.    └── java
  50.    └── com
  51.    └── qipeng
  52.    └── simboss
  53.    └── speedlimit
  54.    ├── SpeedLimitApplicationTests.java
  55.    ├── application
  56.       └── SpeedLimitApplicationServiceTest.java
  57.    └── domain
  58.    └── SpeedLimitServiceTest.java

包路径:

  1. import com.qipeng.simboss.speedlimit.domain.aggregate.SpeedLimit;//聚合根
  2. import com.qipeng.simboss.speedlimit.domain.entity.*;//实体
  3. import com.qipeng.simboss.speedlimit.domain.valobj.*;//值对象
  4. import com.qipeng.simboss.speedlimit.domain.service.*;//领域服务
  5. import com.qipeng.simboss.speedlimit.domain.repo.repository.*;//仓储
  6. import com.qipeng.simboss.speedlimit.repo.dao.*;//mapper接口
  7. import com.qipeng.simboss.speedlimit.application.service.*;//应用层服务

好,基本上这个项目设计的差不多了,需要注意的是,上面核心是com.qipeng.simboss.speedlimit.domain包,里面包含了最重要的业务逻辑处理,其他都是为此服务的,另外,在领域模型不断完善的过程中,需要持续对领域模型进行单元测试,以保证其健壮性,并且,设计SpeedLimit聚合根的时候,不要先考虑数据库的实现,如果需要数据进行测试,可以在SpeedLimitRepository中 Mock 对应的数据。

看下SpeedLimit聚合根中的代码:

  1. package com.qipeng.simboss.speedlimit.domain.aggregate;
  2. import com.qipeng.simboss.speedlimit.domain.entity.SpeedLimitConfig;
  3. import com.qipeng.simboss.speedlimit.facade.model.IotCard;
  4. import lombok.Data;
  5. import java.util.Date;
  6. import java.util.List;
  7. /**
  8. * 限速聚合根
  9. */
  10. @Data
  11. public class SpeedLimit {
  12. /**
  13. * 限速
  14. */
  15. private Long id;
  16. /**
  17. * 组织ID
  18. */
  19. private Integer orgId;
  20. /**
  21. * 套餐ID
  22. */
  23. private Long priceOfferId;
  24. /**
  25. * 限速配置集合
  26. */
  27. private List<SpeedLimitConfig> configs;
  28. /**
  29. * 是否删除当前限速,不持久化
  30. */
  31. private Boolean isDel = false;
  32. /**
  33. * 卡的限速值,不持久化
  34. */
  35. private Double cardSpeedLimit;
  36. /**
  37. * 获取限速值
  38. */
  39. public Double chooseSpeedLimit(Double usageDataVolume, Double totalDataVolume, Long cardPoolId,
  40. Boolean isRealnamePassed, Double currentSpeedLimit) {
  41. //todo this...
  42. }
  43. /**
  44. * 设置是否删除当前限速
  45. */
  46. private void setIsDelSpeedLimit(Double currentSpeedLimit) {
  47. //判断当前限速是否存在,如果存在,则删除现有的限速配置
  48. //todo this...
  49. }
  50. }

上面注释写的比较多(方便理解),SpeedLimit聚合根和之前的SpeedLimit贫血对象相比,主要有以下改动:

  • SpeedLimit聚合根并不只是包含gettersetter,还包含了业务行为,并且也不和数据库表一一对应。
  • SpeedLimit聚合根中包含configs对象(限速配置集合),因为限速配置实体依附于SpeedLimit聚合根。
  • SpeedLimit聚合根中的chooseSpeedLimit方法,意思是根据某种规则从限速配置中,选取当前要限速的值,这是限速的核心业务逻辑。

那为什么不把整个限速设置的逻辑写在SpeedLimit聚合根中?而只是实现选取要限速的值呢?为什么?为什么?为什么?

答案很简单,因为限速设置的整个逻辑需要涉及到多个上下文的协作,SpeedLimit聚合根完全 Hold 不住呀,所以要把这些逻辑写到限速领域服务中,还有最重要的是,SpeedLimit聚合根只关注它边界内的业务逻辑,像限速设置的具体后续操作,它不需要关心,那是业务流程需要关心的,也就是限速领域服务需要去协作的。

好,那我们就看下限速领域服务的具体实现:

  1. package com.qipeng.simboss.speedlimit.domain.service.impl;
  2. /**
  3. * 限速领域服务
  4. */
  5. @Service
  6. public class SpeedLimitServiceImpl implements SpeedLimitService {
  7. @Autowired
  8. private SpeedLimitRepository speedLimitRepo;
  9. @Autowired
  10. private IotCardFacade iotCardFacade;
  11. @Autowired
  12. private DeviceRatePlanFacade deviceRatePlanFacade;
  13. @Autowired
  14. private CarrierApiFacade carrierApiFacade;
  15. /**
  16. * 批量限速检查
  17. */
  18. @Override
  19. public void batchSpeedLimitCheck() {
  20. List<SpeedLimit> speedLimits = speedLimitRepo.listAll();
  21. for (SpeedLimit speedLimit : speedLimits) {
  22. List<IotCard> iotCards = iotCardFacade.listByByOrgId(speedLimit.getOrgId(), speedLimit.getPriceOfferId());
  23. for (IotCard iotCard : iotCards) {
  24. doSpeedLimitCheck(iotCard, speedLimit);
  25. }
  26. }
  27. }
  28. /**
  29. * 单个限速检查
  30. */
  31. @Override
  32. public void doSpeedLimitCheck(SpeedLimitCheckContext context) {
  33. String iccid = context.getIccid();
  34. IotCard iotCard = iotCardFacade.get(iccid);
  35. if (iotCard != null) {
  36. SpeedLimit speedLimit = speedLimitRepo.get(iotCard.getOrgId(), iotCard.getPriceOfferId());
  37. if (speedLimit != null) {
  38. this.doSpeedLimitCheck(iotCard, speedLimit);
  39. }
  40. }
  41. }
  42. /**
  43. * 执行限速逻辑
  44. *
  45. * @param iotCard
  46. * @param speedLimit
  47. */
  48. private void doSpeedLimitCheck(IotCard iotCard, SpeedLimit speedLimit) {
  49. //todo this...
  50. notify(iccid, speedLimit.getCardSpeedLimit());
  51. }
  52. /**
  53. * 修改卡的限速值,并通知用户
  54. */
  55. private void notify(String iccid, Double speedLimit) {
  56. if (speedLimit != null) {
  57. //todo this...
  58. System.out.println("update iotCard SpeedLimit to: " + speedLimit);
  59. System.out.println("notify...");
  60. }
  61. }
  62. }

上面的代码看起来很多,其实干的事并不复杂,主要是业务流程:

  • 通过SpeedLimitCheckContext上下文获取iccid,然后获取对应的限速对象和套餐流量对象。
  • 通过限速聚合根获取需要设置的限速值(核心业务)。
  • 调用相关接口进行添加/删除限速。
  • 修改卡的限速值,并通知用户。

以上限速领域模型基本上比较丰富了,后面的业务迭代只需要改里面的代码即可。

好,我们再来看下应用服务中的代码:

  1. package com.qipeng.simboss.speedlimit.application.service.impl;
  2. @Service
  3. public class SpeedLimitApplicationServiceImpl implements SpeedLimitApplicationService {
  4. @Autowired
  5. private SpeedLimitService speedLimitService;
  6. @Override
  7. public void batchSpeedLimitCheck() {
  8. speedLimitService.batchSpeedLimitCheck();
  9. }
  10. @Override
  11. public void doSpeedLimitCheck(String iccid) {
  12. SpeedLimitCheckContext context = new SpeedLimitCheckContext();
  13. context.setIccid(iccid);
  14. speedLimitService.doSpeedLimitCheck(context);
  15. }
  16. }

应用服务不应包含任何的业务逻辑,只是工作流程的处理,比如接受参数,然后调用相关服务,封装返回等,如果需要持久化聚合根对象,调用仓储服务即可(可能会涉及到 UnitOfWork),另外,像限速聚合根对象的维护,也是实现在应用服务(因为不包含任何业务逻辑),比如创建限速聚合根,过程大概是这样:

  • 应用服务接受参数,然后调用创建限速聚合根工厂(如SpeedLimitFactory),或者通过构造函数创建(包含业务规则,不符合则抛出错误),当然创建还包含聚合根附属的实体。
  • 限速聚合根创建好了,调用仓储服务持久化对象。
  • 返回操作结果。

那如何改善之前 MQ 中处理的一坨代码呢?答案就是一行代码:

  1. @Test
  2. public void doSpeedLimitCheckTest() {
  3. System.out.println("start....");
  4. speedLimitApplicationService.doSpeedLimitCheck("1111");
  5. System.out.println("end");
  6. }

没错,调用下应用层的doSpeedLimitCheck服务即可,调用方完全不需要关心里面的业务逻辑,业务隔离。

单元测试执行结果:

结语

关于领域驱动设计的分层架构(图片来自):

其实,我个人觉得 DDD 的首要核心是确定业务的边界(领域边界),接着把各个边界之间的关系整理清晰(上下文映射图),然后再针对具体的边界具体设计(战术设计),最后就是工作流程的处理,就像上面图中所表达一样。

好,改造完了,又可以和产品经理一起愉快的玩耍了。。。

SIMBOSS:物联网业务如何应用领域驱动设计?的更多相关文章

  1. DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对?

    写在前面 阅读目录: 具体业务场景 业务需求变化 "愚蠢"的应对 消息列表实现 消息详情页实现 消息发送.回复.销毁等实现 回到原点的一些思考 业务需求变化,领域模型变化了吗? 对 ...

  2. DDD 领域驱动设计-两个实体的碰撞火花

    上一篇:<DDD 领域驱动设计-领域模型中的用户设计?> 开源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代码已更新) 在 ...

  3. [.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店

    一.前言 在前面专题一中,我已经介绍了我写这系列文章的初衷了.由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这 ...

  4. (转)EntityFramework之领域驱动设计实践

    EntityFramework之领域驱动设计实践 - 前言 EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject EntityFramework之领 ...

  5. EntityFramework之领域驱动设计实践

    EntityFramework之领域驱动设计实践 - 前言 EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject EntityFramework之领 ...

  6. DDD领域驱动设计基本理论知识总结(转)

    领域驱动设计之领域模型 为什么建立一个领域模型是重要的 领域通用语言(UBIQUITOUS LANGUAGE) 将领域模型转换为代码实现的最佳实践 领域建模时思考问题的角度 领域驱动设计的经典分层架构 ...

  7. IDDD 实现领域驱动设计-一个简单业务用例的回顾和理解

    上一篇:<IDDD 实现领域驱动设计-由贫血导致的失忆症> 这篇博文是对<实现领域驱动设计>第一章后半部分内容的理解. Domain Experts-领域专家 这节点内容是昨天 ...

  8. DDD 领域驱动设计-看我如何应对业务需求变化,领域模型调整?

    写在前面 上一篇:DDD 领域驱动设计-看我如何应对业务需求变化,愚蠢的应对? "愚蠢的应对",这个标题是我后来补充上的,博文中除了描述需求变化.愚蠢应对和一些思考,确实没有实质性 ...

  9. .NET应用架构设计—面向查询的领域驱动设计实践(调整传统三层架构,外加维护型的业务开关)

    阅读目录: 1.背景介绍 2.在业务层中加入核心领域模型(引入DomainModel,让逻辑.数据有家可归,变成一个完整的业务对象) 3.统一协调层Application Layer(加入协调层来转换 ...

随机推荐

  1. python实现DFA模拟程序(附java实现代码)

    DFA(确定的有穷自动机) 一个确定的有穷自动机M是一个五元组: M=(K,∑,f,S,Z) K是一个有穷集,它的每个元素称为一个状态. ∑是一个有穷字母表,它的每一个元素称为一个输入符号,所以也陈∑ ...

  2. Shiro authentication for Apache Zeppelin

    Overview Apache Shiro is a powerful and easy-to-use Java security framework that performs authentica ...

  3. 一次使用InfluxDB数据库的总结

    前言 因当前的项目需要记录每秒钟服务器的状态信息,例如负载.cpu等等信息,这些数据都是和时间相关联的. 因为一秒钟就要存储挺多的数据.而且我还在前端做了echart的折线图,使用websocket实 ...

  4. 个人永久性免费-Excel催化剂功能第68波-父子结构表转换之父子关系BOM表拆分篇

    Excel中制造业行业中,有一个非常刚需的需求是对BOM(成品物料清单)的拆解,一般系统导出的BOM表,是经过压缩处理的,由父子表结构的方式存储数据.对某些有能力使用SAP等专业ERP软件的工厂来说, ...

  5. 《VR入门系列教程》之20---使用Oculus移动端SDK

    使用Oculus移动端SDK     在基于安卓系统的GearVR上开发应用需要用到Oculus的移动端SDK,下面的网址可以下载SDK:http://developer.oculus.com     ...

  6. 💡我们的表单解决方案 el-form-renderer

    前言 本文将介绍我们的表单解决方案 @femessage/el-form-renderer,展示我们在 Vue 技术栈下,我们是如何处理以下问题的: 表单项动态显示或隐藏 表单数据联动 表单输入/输出 ...

  7. 【杂谈】Hash表与平衡树

    hash表与平衡树查询数据的时间复杂度是多少? hash表为O(1),平衡树为O(logn) 这个时间复杂度是如何得出的? 时间复杂度是按照最糟糕的情况来的.但即使是最糟糕的情况,hash表也只需要计 ...

  8. python查漏补缺 --- 基础概念及控制结构

    python  是一种面向对象的解释型计算机程序设计语言,在运行时由解释器处理,在执行程序之前不需要编译程序.Python就是一句话,写得快,跑得慢. 下面的内容是平时工作中容易忽略掉的小细节,希望借 ...

  9. js的位运算(其它语言也通用)

    左移运算符(<<) 该运算符有2个运算数,a<<b,将a左移相当于a乘以2的b次方,2个运算符要求是整数,或可以转换成整数的. 如:1<<2 =4 "1& ...

  10. middleware中间件

    django 中的中间件(middleware),在django中,中间件其实就是一个类,在请求到来和结束后,django会根据自己的规则在合适的时机执行中间件中相应的方法. 在django项目的se ...