在這個萬物互聯的時代,物聯網業務蓬勃發展,但也瞬息萬變,對于開發人員來說,這是一種挑戰,但也是一種“折磨”。
在業務發展初期,因為時間有限,我們一般會遵循“小步快跑,迭代試錯”的原則進行業務開發,用通俗的話來說就是“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 的首要核心是確定業務的邊界(領域邊界),接著把各個邊界之間的關系整理清晰(上下文映射圖),然后再針對具體的邊界具體設計(戰術設計),最后就是工作流程的處理,就像上面圖中所表達一樣。
好,改造完了,又可以和產品經理一起愉快的玩耍了。
作者:岳中新
-
物聯網
+關注
關注
2909文章
44701瀏覽量
373958 -
應用軟件
+關注
關注
0文章
51瀏覽量
9100
發布評論請先 登錄
相關推薦
評論