SIMBOSS:物聯網業務如何應用領域驅動設計?

前言

在這個萬物互聯的時代,物聯網業務蓬勃發展,但也瞬息萬變,對於開發人員來講,這是一種挑戰,但也是一種「折磨」。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..的各類嵌套,因此,仍是眼不看爲淨。。。

好,到此爲止,這個需求已經「腳本式」的開發完了,咱們來總結一把:

  • 條理清晰,開發效率賊高,徹底符合「先上了再說」的開發原則。
  • 數據的 CURD 和業務邏輯處理隔離開,用到的地方「單獨處理」,彷佛沒啥問題。

沒啥問題對吧?好,如今業務迭代來了,產品經理髮話了,說除了批量限速檢查,還須要對單卡的限速同步處理(瞎掰的),由於是同步處理,因此我沒辦法發消息到 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聚合根並不僅是包含gettersetter,還包含了業務行爲,而且也不和數據庫表一一對應。
  • 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 的首要核心是肯定業務的邊界(領域邊界),接着把各個邊界之間的關係整理清晰(上下文映射圖),而後再針對具體的邊界具體設計(戰術設計),最後就是工做流程的處理,就像上面圖中所表達同樣。

好,改造完了,又能夠和產品經理一塊兒愉快的玩耍了。。。

相關文章
相關標籤/搜索