主頁 > 軟體設計 > 領域驅動設計DDD系列(四)——汽車定價車業務遷移案例

領域驅動設計DDD系列(四)——汽車定價車業務遷移案例

2020-12-10 18:42:08 軟體設計

專案介紹

定價車業務是天貓汽車行業最核心的業務之一,解決的是消費者在4S店買車無法砍價到最低價格的問題,天貓可以從主機廠(貨源方)BD一個最低的一口價,用戶通過在天貓下定金單,到線下門店通過電子憑證核銷、付尾款,最終履約提車的交易模式,完成在線購買新車的完整的交易鏈路倍訓,培養“上天貓,開新車”的心智,其整個鏈路涉及到了復雜的交易鏈路、門店新零售的核銷和POS付尾款或車秒貸金融、網商銀行的墊資鏈路、匯金和FP的保證金凍結、分傭(到平臺和門店)、代扣等等,整體鏈路非常長而復雜,有大量的邏輯分支,需要平臺小二、貨源方、門店、用戶的多次配置和互動,且涉及到資金和交易,對業務邏輯的正確性有著極高的要求,

定價車業務從7月中KO到9.9上線,中間僅有不到一個月的開發時間,團隊里幾個開發和測驗同學帶著幾個外包基本上是一整個月“007”完成了這個業務的上線,并且取得了一定的業務結果,但是在這個程序中,由于時間太緊,全鏈路需要從0到1,作業量太大,而且業務需求在不停的變化,導致了這個專案的設計缺少了架構上和代碼質量上的思考,基本上都是膠水代碼,帶來的大量的維護成本、健壯性差、業務的準確性無法保障,對未來變化的回應速度會比較慢,但更嚴重的問題是,因為代碼量大、質量比較差,極有可能會因為未來的一次評估不到位的變更導致線上故障,甚至產生資損,而這個是我們無論怎么壓測也無法解決的穩定性問題,急需要解決,

從雙11之后開始,我們淘系行業團隊啟動了一個架構升級戰役,就是為了能通過架構的改造升級,解決行業團隊里大量的長尾應用維護成本高,業務邏輯不清晰,穩定性無法保障的問題,而定價車業務,作為一個汽車行業的核心業務,有很高的業務價值,同時在復雜度上也是行業數一數二,所以被選為第一個改造的業務,而通過這次改造,我們希望能沉淀設計模式和實戰經驗,為未來其他業務的改造打好基礎,

業務邏輯簡介

定價車的核心業務邏輯是以天貓汽車為主體開設天貓旗艦店,然后通過統一貨源方授權代理特定車型,另一方面,與門店代理商簽署協議,通過旗艦店+線下門店的方式代銷代理的車型,在這個業務模式下,業務流程需要旗艦店、門店、TP方、貨源方的參與,流程包括資訊流、物流、資金流在各方之間的流轉,

image-20200507175423401.png

一個簡化版的業務流程圖如下:
image-20200507180250523.png

幾個核心節點:

  • 定價車車型管理:涉及到車型寶貝的配置、分傭的設定、可售賣門店的設定、倉庫的配置等,
  • 下單鏈路:通過電子憑證多階段訂單的方式,解決在線付少量定金,但是定價全款的模式,同時會生成一個定價車的自有訂單,
  • 核銷電子憑證:生成尾款訂單,同時給天貓分傭,
  • 墊付尾款:對接了網商銀行的大額支付功能,在簽約后由門店墊資到貨源方,貨源方才可以發貨
  • 到店支付尾款:用云POS支付尾款,同時給門店分傭,

重點問題

定價車第一版代碼的幾個核心問題包括:

重Service層:定價車代碼有及其重的Service(HSF)層,幾乎所有的業務邏輯都實作在了7個Service中,每個Service最多有20-30個介面,依賴了10-20個外部依賴,每個介面實作都是長長的腳本代碼,且Service間有互相呼叫的問題,具體案例代碼太長不展示了,大家可以想象到,這個直接導致的問題就是代碼理解成本高:當一個檔案的代碼超過200行左右時,其功能就已經不“純粹”了,很難去理解他的核心邏輯,也無法從檔案名和方法名上就理解一個檔案是做什么的,這個增加了理解成本和除錯對接成本,在定價車業務中,最大的一個檔案有近3000行代碼,且有較深的呼叫鏈路,光去看一遍代碼就已經是很有挑戰的事情了,更不要說去理解業務含義,

邏輯重復:因為每個方法都是獨立的流水賬,也就導致了同一個業務邏輯很有可能出現在多個地方,前期業務剛開始的時候還好,但是到了后期當業務邏輯變更時,很有可能會漏掉某個實作,導致故障,在定價車業務中,關于分傭比例校驗的邏輯在2處出現、尾款金額計算出現在3個地方、而最重要的分傭金額計算也出現了3個地方,可以想象未來的一次分傭邏輯變更有可能會導致資損,

“貧血”域模型:對于絕大部分域物件,從回傳給前端的資料、到DbMapper使用的出入參都是同一個貧血DO物件,僅包含資料,導致業務邏輯散落在Service層中,一個最常見的問題就是定價車訂單的狀態流轉,類似的狀態判斷和推進邏輯代碼散落在多個地方:

FpcarOrderDO fpcarOrderDO = orderDOList.get(0);
if (fpcarOrderDO.getVechileStatus() < FpcarOrderDO.VECHILE_RELEASED) {
   result.setError(ErrorInfoEnum.UPDATE_ERROR);
   result.setMsgInfo("訂單還沒有發車!");
   return result;
}

if (!(fpcarOrderDO.getVechileStatus() < FpcarOrderDO.VECHILE_ARRIVE_WAREHOUSE)) {
   result.setError(ErrorInfoEnum.UPDATE_ERROR);
   result.setMsgInfo("車已到倉!");
   return result;
}

fpcarOrderDO.setVechileStatus(FpcarOrderDO.VECHILE_ARRIVE_WAREHOUSE);
fpcarOrderDO.setArriveWarehouseTime(new Date());

同時由于業務前臺展示形態和后臺存盤格式不一致,導致有大量的轉化邏輯,而由于使用了基本資料格式,業務邏輯里有很多“魔法值”,導致代碼很難理解且容易出錯,下面的一段代碼就是計算門店傭金的邏輯:

//計算分傭金額
String commissionBmount = "0";
BigDecimal amount = new BigDecimal(fpcarConfigDO.getStoreCommissionAmount());
if (2 == fpcarConfigDO.getCommissionType()) {
   //基于分傭基數,按比例分傭
   BigDecimal total = new BigDecimal(fpcarConfigDO.getCommissionBasePrice());

   //total是分,amount是比例的數字,轉換成元要除以10000
   BigDecimal commionB = total.multiply(amount).divide(new BigDecimal("10000"), 2,
       BigDecimal.ROUND_HALF_UP);

   commissionBmount = commionB.toString();
} else {
   //如果固定金額的,則直接按配置的金額扣傭(需要將分轉換為元)
   BigDecimal commionB = new BigDecimal(fpcarConfigDO.getStoreCommissionAmount()).
       divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP);

   commissionBmount = commionB.toString();
}

如果沒有注釋,很難看出來commissionType == 2是什么意思,為啥要除以10000和100等等,而這里面又涉及到了金額的各種轉化,在其他的代碼里也有很多yuanToFen和fenToYuan之類的轉化,在這里就不一一展示了,

可測驗性差、無UT覆寫:由于絕大部分方法都或多或少的直接依賴了DAO和一些二方服務介面,導致這些代碼基本上不可被單元測驗,只能集成測驗,而集成測驗由于外部依賴的不可靠性,資料的不完備等不可控因素,導致測驗效率極低,這個問題的直接后果就是定價車的代碼是沒有被測驗覆寫過的代碼,未來代碼變更也沒有明確的回歸用例,代碼質量無法保障,出現問題無法被及時發現,

除了以上的代碼問題之外,一個最大的“風格”上的問題,但又直接嚴重影響了開發效率的問題是:除非你很了解整個業務邏輯和呼叫鏈路,否則一個人很難從現在的代碼上理解完整的業務流程,從代碼層面能看到的僅僅是近百個介面,但每個介面是在什么時候被呼叫到的、為什么被呼叫到、以及哪個上游呼叫到的都很難說清楚,這就像是在看一本小說,但是所有的章節都完全打散無序,你只能從字里行間試圖去拼裝一個故事,可想而知其難度和糟糕的體驗,更別說試圖去改變書中的內容,這個風格問題,在簡單的CRUD應用中也許影響還不是很大,但是在這種擁有復雜流程,分支邏輯的業務流程中,對未來的維護成本和穩定性來說也許是致命的,

升級方案 - Aslan Framework + SDK

(一)框架簡介
Aslan Framework是我們淘系行業架構小組【南晏、逸翰、瑜進、玄麟、殷浩】研發出來的一套DDD+CQRS的框架,運行在Spring Boot/Pandora Boot之上,實作了一套基于六邊形架構(Hexagonal Architecture)的DDD規范,通過不同分層的POM隔離和依賴關系降低了核心業務邏輯和膠水邏輯之間的關聯度(關于六邊形架構的分解可以參考我之前的文章),而通過CQRS規范,我們做到了對外介面層(HSF、MTOP、TOP、HTTP等)和內部業務邏輯(Application層)的完整解耦,讓內部邏輯邊界清晰可見,

其次,為了解決行業應用常見的依賴沖突、代碼可測驗性差等問題,我們將行業依賴的大部分外部二方依賴都封裝了基于Spring Boot Starter的防腐層,形成了一個整體的面向垂直業務的SDK層,其價值是讓垂直業務接入橫向能力更簡單,其中,防腐層的Facade介面讓業務Application層不再對集團的各個二方包產生依賴,而僅僅依賴了干凈的介面和DTO類,確保了核心業務邏輯無外部依賴,干凈可測驗;統一的POM管理確保引入的依賴之間沒有二方包版本沖突,由SDK維護團隊統一解決和確保不同Starter包之間的兼容性;統一的starter配置管理可以確保同一個應用中的不同的業務可以共存不沖突【進展中,定價車一期不需要】,

最后,結合了CQRS架構、用例驅動等理念,我們產出了一套業務流程可視化的方案,通過Application層的流程模塊化,第一次做到了業務身份隔離、流程可視化,讓“代碼即檔案”,今天哪怕是你手里沒有一個PRD,也可以通過代碼注解找到業務流程的前后關系,能夠知道在業務流程中每一步都能做什么、誰在做、哪個業務在做,

下面,我詳細介紹一下我們怎么將定價車業務重構的,

(二)目錄及模塊規范化

1.模塊劃分問題
carcenter應用是一個老的Spring MVC應用,其Maven模塊比較符合傳統業務應用常見的模塊規范,除了應用需要的模塊之外,只分為carcenter-client和carcenter-core兩個模塊,截圖如下:

image-20200113170418509.png

其中,client為對外提供的二方包,core為介面的實作,這種打包的問題在于所有的核心代碼都在core里,無法做到分層隔離,在寫代碼時很容易導致Bean間的回圈依賴,

2.目錄結構問題

再看一下其中模塊的目錄結構,截圖如下:

image-20200113155903718.png

這里面能看出來一個更大的問題,那就是carcenter的目錄結構是根據 “檔案功能” 拆分的,由于carcenter應用包含了很多個不同的業務(直租、專車專用、車秒貸等等),當這些業務都放在一起時,很難從多個不同的目錄里面抽離出來哪些代碼是僅屬于定價車業務的,另外的一個大風險是按照這種目錄結構劃分,很有可能出現定價車和其他業務共用一個工具類的情況,

簡單來說,這種client / core的模塊的劃分和按功能劃分目錄結構的方法,很不利于業務之間的隔離,而未來因為其他業務的變更很有可能會導致定價車業務出故障(BTW,這種問題在行業應用里經常會出現),

3.基于DDD的模塊和目錄規范

基于DDD架構,我們將整個應用的代碼結構重新按照業務做了劃分,并且在每個業務里劃分出了client、domain、application、infrastructure和interface這5個模塊,結構如下:

image-20200113171331981.png

其中,carcenter2-start是Pandora Boot的啟動包,相當于一個啟動類的容器,而定價車相關的所有代碼都放在了fpcar這個Maven模塊中,bidcar則是另一個業務“暗標”的代碼,不同的業務之間沒有pom依賴關系,徹底的做到了業務之間代碼的隔離,fpcar下面的模塊功能如下:

client:包含了對外介面、DTO、CQRS用到的Command、Query、Event,以及一些無業務屬性的值物件,
domain:核心業務邏輯的承載,包括了物體(Entity)和聚合根(Aggregate)、有業務屬性的值物件(Domain Primitive)、域服務(Domain Service)、和Repository的介面類,Domain只依賴Client、外部二方包的Facade介面,不依賴任何其他框架(包括Spring),這就確保了Domain層是完整可單測的,
application:業務流程的封裝,包括了應用服務(Application Service)、DTO轉化器等傳統Application層包含的東西,同時在這次架構升級中,引入了業務流程可視化的元素,詳細在后面講,Application層在原則上只做業務流程的編排,不做核心業務邏輯,所有判斷必須下沉到Domain層,

infrastructure:主要是Repository的實作、DAO、資料映射物件DO(Data Object),DO和Entity的轉化類等,Infrastructure只依賴資料庫、Tair等跟存盤相關的依賴,以及Domain層,不依賴Application層,

interface:對外介面類的實作,包括HSF、MTOP、TOP、定時任務、訊息等等多種來源,通過CQRS的架構,我們做到了interface和application之間的解耦,所以interface只依賴了中間件和client包,

interface-old:這個是為了做到無縫遷移用的,在新應用中不需要,interface-old實作了老業務client包的功能,確保介面和資料一致性,但是HSF版本升為2.0,這樣業務的遷移成本就極大的降低了,

all:all只是一個打包POM,引入了application、infrastructure、interface和interface-old這幾個依賴,

對于所有外部依賴,業務側代碼僅需要在client包引入第三方facade,在應用最外部引入第三方starter/SDK即可,Spring框架的依賴注入和SpringBoot的AutoConfiguration會保證真實業務中會呼叫到必須的服務,

所有的包依賴關系如下圖:

image-20200113182703322.png

4.namespace/目錄結構和命名規范

命名和目錄結構可能是編碼里最“燒腦”的問題,所以我們制定了一套規范,讓同學們能從namespace中就知道其功能,另一個核心是讓不同的業務可以做到完全隔離:

前綴:com.(tmall|taobao).(應用名).(子業務名).(垂直業務域名 | 流程名)

其中,天貓業務用com.tmall,淘寶業務用com.taobao
應用名:比如,carcenter2
子業務名:比如,定價車 fpcar
垂直業務域名 | 流程名:比如,定價車倉庫 warehouse、定價車交易 trade、定價車入駐 entry
例子:定價車倉庫域 com.tmall.carcenter2.fpcar.warehouse
例子:定價車交易流程 com.tmall.carcenter2.fpcar.trade
例子:定價車入駐流程 com.tmall.carcenter2.fpcar.entry
完整namespace:(前綴).(功能)

domain:域物件,主要是Entity、Enum、Constant
repository:Repository介面
repository.impl:Repository實作類
repository.converter:DataConverter類(DO <-> Entity)
data:資料庫映射DO物件、Mybatis Mapper介面類
types、command、query、event、dto:ValueObject類、Command、Event、Query類、DTO類
application:業務流程
module: 流程Workflow模塊類
assembler:DTO Assembler物件
service:HSF介面類
service.impl:HSF介面實作類
message:訊息Listener類
controller:Controller類
這么做會出現的問題是一些公共的模塊,比如Utils類應該放到哪里?這個結構的原則是公共功能也完全不共享,寧可copy出一份一摸一樣的,這樣一個業務的全部代碼一定會在fpcar這個父目錄下,真正可以做到“拎包即走”,

(三)沉淀業務邏輯 - Domain層
在傳統架構中,核心業務邏輯是寫在Service層的,每個Service的方法代表了一個具體的操作或查詢,導致核心業務邏輯散亂在多個大檔案中,很難被理解,所以第二步我們需要做的事情是抽象核心業務邏輯到Domain層,

但首先我們先看一下為什么原架構中核心業務邏輯會散亂,以下是一個比較簡單,但是又比較典型的案例,具體的業務邏輯是在定金訂單支付后,收到支付訊息后去更新訂單狀態和一些欄位,僅僅展現了部分代碼:

// FpcarDealService.class

@Override
public Result<String> paidOrder(BizOrderDO bizOrderDO, PayOrderDO payOrderDO) {
    // (1)
    Result<String> result = new Result<>();
    result.setSuccess(true);
    result.setObject("ok");

    // (1)
    try {
        // (2)
        //定價車標:auto_fixPrice=1-定金訂單; auto_fixPrice=2-尾款訂單
        String orderFlag = bizOrderDO.getAttribute(FPCAR_ORDER_TAG_NAME);
        if (StringUtils.equalsIgnoreCase(orderFlag, FPCAR_ORDER1_TAG)) {
            // (3)
            Map<String, Object> paraMap = new HashMap<>();
            paraMap.put("earnestOrderId", bizOrderDO.getBizOrderId());
            List<FpcarOrderDO> orderDOList = fpcarOrderDAO.queryList(paraMap);

            if (!CollectionUtils.isEmpty(orderDOList)) {
                FpcarOrderDO fpcarOrderDO = orderDOList.get(0);
                                // (4)
                //生成自有的訂單號,供某些環節中使用
                fpcarOrderDO.setFpOrderId(FpcarOrderDO.FPORDER_PREFIX + fpcarOrderDO.getEarnestOrderId());
                //定金支付時間
                fpcarOrderDO.setEarnestOrderPayTime(payOrderDO.getPayTime());

                //定金訂單支付成功狀態
                fpcarOrderDO.setupStatus(FpcarOrderDO.EARNEST_ORDER_PAID);

                // (3)
                fpcarOrderDAO.update(fpcarOrderDO);
            }
        }
    }
    // (1)
    catch (Exception ex) {
            logger.error("createOrderException=", ex);
            result.setSuccess(false);
            result.setObject(null);
    }

    return result;
}

在上面這段代碼里:

(1):屬于回傳給呼叫方的Result和try/catch兜底,屬于介面層(interface)邏輯,不是業務邏輯

(2):判斷邏輯強依賴了訂單BizOrderDO的實作,而這個判斷邏輯有可能散落在多個地方,造成重復,同樣的,這個不屬于核心業務邏輯,屬于設施層(infrastructure)邏輯,針對于訂單具體實作的邏輯需要被隔離開,

(3):查詢和儲存強依賴了DAO的具體實作,很難測驗且和資料庫底層強耦合,同樣屬于infrastructure邏輯

(4):真正的業務邏輯,但是在操作FpcarOrderDO這個“貧血”模型,

從上面可以看出來,之所以傳統的Service層非常臃腫,其中一個原因在于傳統的Service層除了核心業務邏輯之外,同時包含了介面、設施、第三方等等邏輯,導致了業務邏輯強耦合第三方邏輯,無法有效的隔離和測驗,另一個原因是FpcarOrderDO物件實際上是一個”貧血“域模型,也就是說該物件僅包含資料,不包含行為,導致行為代碼散落在多個Service方法里,第一個問題我們在后面解決,這里我們先解決第二個問題,

用充血域模型承載核心業務邏輯

為了承載核心業務邏輯,我們新建了一個領域模型,叫FpcarOrder,省略后代碼如下:

/**
 * 定價車訂單
 */
@Data
public class FpcarOrder implements Aggregate<FpcarOrderId> {

    /**
     * fpOrderId前綴
     */
    public static final String FPORDER_PREFIX = "fpcar_";

    /**
     * 定價車訂單ID
     */
    private FpcarOrderId id;

    /**
     * 自有購買單號
     */
    private String fpOrderId;

    /**
     * 定金訂單狀態
     */
    private FpcarOrderStatus orderStatus = FpcarOrderStatus.UNKNOWN;

    /**
     * 定金訂單id
     */
    private BizOrderId earnestOrderId;

    /**
     * 定金訂單支付成功時間
     */
    private Date earnestOrderPayTime;

    /**
     * 支付定金訂單
     * @param payTime 支付時間
     */
    public void payEarnestOrder(Date payTime) {
        //生成自有的訂單號,供某些環節中使用
        this.setFpOrderId(FPORDER_PREFIX + this.getEarnestOrderId().getValue());

        //定金支付時間
        this.setEarnestOrderPayTime(payTime);

        //定金訂單支付成功狀態
        this.setOrderStatus(FpcarOrderStatus.EARNEST_ORDER_PAID);
    }

}

幾個重點:

FpcarOrder實作了Aggregate,這個是阿斯蘭框架的一個DDD的規范,顯式宣告FpcarOrder是一個聚合根,且ID欄位是FpcarOrderId強型別,(關于聚合根(Aggregate Root)和物體(Entity)的關系和理論在另外一篇文章會詳細介紹)
FpcarOrderId、BizOrderId、以及FpcarOrderStatus都是Value Object或Enum強型別(原始代碼里都是long或int),通過強型別聚合校驗邏輯,且確保不會出現賦值到錯誤欄位的問題,(關于為何要用強型別,參考我的其他文章)
payEarnestOrder方法封裝了對核心業務邏輯:幾個欄位的賦值和狀態流轉,
沉淀核心業務邏輯到Entity后,并且加入Repository(見下文)之后,呼叫方邏輯變成了:

// 查詢
FpcarOrder fpcarOrder = orderRepository.findByEarnestOrderId(bizOrder.getBizOrderId());

// 更新狀態
fpcarOrder.payEarnestOrder(payOrder.getPayTime());

// 保存
orderRepository.save(fpcarOrder);

雖然上面的呼叫貌似僅僅是把上面(4)里面的邏輯改到了Entity里,但這個操作最大的意義是所有FpcarOrder物件相關的賦值、狀態流轉代碼都集中到一個類里面,不單單有助于降低代碼重復,而且解決了未來業務邏輯變化時,業務“流程”不需要跟著變化的問題,

(四) 隔離持久化邏輯 - Infrastructure層
在原有的代碼中,由于業務邏輯直接操作了貧血DO模型和DAO,導致邏輯中跟持久化相關的邏輯很重;同時因為貧血模型無法保證資料的完整性和一致性,很容易導致bug,在前面我們通過封裝了一個FpcarOrder的Entity類,把所有行為相關的邏輯全部封裝到Entity里,可以避免直接在業務邏輯中直接處理資料持久化,使用方只需要呼叫Repository.save方法,Repository的具體實作規范比較復雜,參考我之前的一篇文章,在這里不再贅述,

使用了Repository模式帶來的一個好處是,原來很多復雜的、容易出錯的資料轉化邏輯,現在都可以有專門的類負責封裝,參考之前所說的計算門店傭金的邏輯:

//計算分傭金額
String commissionBmount = "0";
BigDecimal amount = new BigDecimal(fpcarConfigDO.getStoreCommissionAmount());
if (2 == fpcarConfigDO.getCommissionType()) {
    //基于分傭基數,按比例分傭
    BigDecimal total = new BigDecimal(fpcarConfigDO.getCommissionBasePrice());

    //total是分,amount是比例的數字,轉換成元要除以10000
    BigDecimal commionB = total.multiply(amount).divide(new BigDecimal("10000"), 2,
        BigDecimal.ROUND_HALF_UP);

    commissionBmount = commionB.toString();
} else {
    //如果固定金額的,則直接按配置的金額扣傭(需要將分轉換為元)
    BigDecimal commionB = new BigDecimal(fpcarConfigDO.getStoreCommissionAmount()).
        divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP);

    commissionBmount = commionB.toString();
}

這段代碼之所以復雜的最核心原因是,在專案初期設計時,把分傭固定金額(分)和比例(百分比)這兩個完全不同型別的數值存到了同一個資料庫欄位(String型別)里,然后通過另一個commissionType的欄位做了區分,導致了大量的格式轉化邏輯,在業務上線之后,已經很難再去修改底層資料儲存的邏輯了,只能通過上層的封裝降低對底層邏輯的依賴和復雜度,

在使用Entity+Repository模式改造時,我們把分傭計算邏輯封裝到了一個Value Object中:

@Value
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class CommissionConfig {

    private static final Money THRESHOLD_AMOUNT = new Money("1000");
    private static final BigDecimal HUNDRED_PERCENT = new BigDecimal("100");

    /**
     * 分傭方式:1-固定金額; 2-按比例扣傭
     */
    private CommissionType commissionType;

    /**
     * 按比例分傭時的價格基數
     * 這個欄位只有commissionType = 2 時 才生效
     */
    private Money commissionBasePrice;

    /**
     * 車源方分傭給 旗艦店 的 金額
     * 這個欄位只有commissionType = 1 時 才生效
     * NOTE:不要直接取這個欄位!用getSourceCommission()
     */
    private Money sourceCommissionAmount;

    /**
     * 車源方分傭給 旗艦店 的 比例,百分比
     * 這個欄位只有commissionType = 2 時 才生效
     * NOTE:不要直接取這個欄位!用getSourceCommission()
     */
    private BigDecimal sourceCommissionPercent;

    /**
     * 車源方分傭給 門店 的 金額
     * 這個欄位只有commissionType = 1 時 才生效
     * NOTE:不要直接取這個欄位!用getStoreCommission()
     */
    private Money storeCommissionAmount;

    /**
     * 車源方分傭給 門店 的 比例,百分比
     * 這個欄位只有commissionType = 2 時 才生效
     * NOTE:不要直接取這個欄位!用getStoreCommission()
     */
    private BigDecimal storeCommissionPercent;

    /**
     * 獲取平臺應該分傭的金額
     */
    public Money getSourceCommission() {
        // 如果 金額 和 比例 都是0,回傳0
        if (getSourceCommissionAmount().isZero() && getSourceCommissionPercent().compareTo(BigDecimal.ZERO) == 0) {
            return Money.ZERO;
        }

        //計算分傭金額
        switch (commissionType) {
            case FixedAmount:
                return getSourceCommissionAmount();
            case Percentage:
                BigDecimal multiplier = getSourceCommissionPercent().divide(HUNDRED_PERCENT, RoundingMode.HALF_EVEN);
                return getCommissionBasePrice().multiply(multiplier);
        }

        return Money.ZERO;
    }

    /**
     * 獲取門店應該分傭的金額
     */
    public Money getStoreCommission() {
        // 如果 金額 和 比例 都是0,回傳0
        if (getStoreCommissionAmount().isZero() && getStoreCommissionPercent().compareTo(BigDecimal.ZERO) == 0) {
            return Money.ZERO;
        }

        //計算分傭金額
        switch (commissionType) {
            case FixedAmount:
                return getStoreCommissionAmount();
            case Percentage:
                BigDecimal multiplier = getStoreCommissionPercent().divide(HUNDRED_PERCENT, RoundingMode.HALF_EVEN);
                return getCommissionBasePrice().multiply(multiplier);
        }

        return Money.ZERO;
    }

    private static boolean isValid(Integer commissionType, Money baseMoney,
                                   Money sourceMoney, BigDecimal sourcePercent,
                                   Money storeMoney, BigDecimal storePercent, Money itemPrice) {
        if (commissionType != 1 && commissionType != 2) {
            return false;
        }

        // 1000元以內價格 或 分傭基數為0的 或 分傭金額<0的 有問題
        if (itemPrice.isLessThanOrEqualTo(THRESHOLD_AMOUNT)
                || baseMoney.isLessThanOrEqualTo(Money.ZERO)
                || sourceMoney.isLessThan(Money.ZERO)
                || storeMoney.isLessThan(Money.ZERO)) {
            return false;
        }

        CommissionType type = CommissionType.valueOf(commissionType);

        if (type == CommissionType.FixedAmount) {
            //天貓分傭、門店分傭固定金額 取值應該在 0-傭金計算基數 范圍內
            if (sourceMoney.add(storeMoney).isGreaterThan(baseMoney)) {
                return false;
            }
        } else if (type == CommissionType.Percentage) {
            //天貓分傭、門店分傭對應比例 取值應該在 0-100 范圍內
            if (sourcePercent.add(storePercent).compareTo(HUNDRED_PERCENT) > 0) {
                return false;
            }
        }
        return true;
    }
}

然后在FpcarConfig中只需要呼叫CommissionConfig的計算邏輯:

@Data
public class FpcarConfig implements Aggregate<FpcarConfigId> {

    /**
     * 分傭資訊物件封裝
     */
    private CommissionConfig commissionConfig;

    /**
     * 獲取平臺應該分傭的金額
     */
    public Money getSourceCommission() {
        if (commissionConfig != null) {
            return commissionConfig.getSourceCommission();
        }
        return Money.ZERO;
    }

    /**
     * 獲取門店應該分傭的金額
     */
    public Money getStoreCommission() {
        if (commissionConfig != null) {
            return commissionConfig.getStoreCommission();
        }
        return Money.ZERO;
    }

}

通過新建一個轉化器,可以解決FpcarConfig和FpcarConfigDO之間的轉化邏輯:

@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
        uses = {IdConverters.class, MoneyConverter.class})
public interface FpcarConfigDataConverter {

    FpcarConfigDataConverter INSTANCE = Mappers.getMapper(FpcarConfigDataConverter.class);

    @InheritInverseConfiguration
    FpcarConfig fromData(FpcarConfigDO data);

    FpcarConfigDO toData(FpcarConfig entity);

    @AfterMapping
    default void fillCommissionConfig(FpcarConfigDO configDO, @MappingTarget FpcarConfig entity) {

        Money baseMoney = Money.fromCents(Long.valueOf(configDO.getCommissionBasePrice()));
        Money sourceMoney = Money.fromCents(Long.valueOf(configDO.getSourceCommissionAmount()));
        BigDecimal sourcePercent = new BigDecimal(configDO.getSourceCommissionAmount());
        Money storeMoney = Money.fromCents(Long.valueOf(configDO.getStoreCommissionAmount()));
        BigDecimal storePercent = new BigDecimal(configDO.getStoreCommissionAmount());
        Money itemPrice = Money.fromCents(configDO.getItemPrice());

        CommissionConfig commissionConfig =
                CommissionConfig.buildFromDO(configDO.getCommissionType(), baseMoney, sourceMoney, sourcePercent,
                        storeMoney, storePercent, itemPrice);

        entity.setCommissionConfig(commissionConfig);
    }

    @AfterMapping
    default void fillDOCommission(FpcarConfig entity, @MappingTarget FpcarConfigDO configDO) {
        CommissionConfig comm = entity.getCommissionConfig();
        configDO.setCommissionType(comm.getCommissionType().getValue());
        configDO.setCommissionBasePrice(comm.getCommissionBasePrice().getCents() + "");
        if (comm.getCommissionType() == CommissionType.FixedAmount) {
            configDO.setSourceCommissionAmount(comm.getSourceCommissionAmount().getCents() + "");
            configDO.setStoreCommissionAmount(comm.getStoreCommissionAmount().getCents() + "");
        } else if (comm.getCommissionType() == CommissionType.Percentage) {
            configDO.setSourceCommissionAmount(comm.getSourceCommissionPercent().toString());
            configDO.setStoreCommissionAmount(comm.getStoreCommissionPercent().toString());
        }
    }

}

呼叫方在使用時只需要,而不再需要關注具體的儲存方式:

fpcarConfig.getSourceCommission()

而最重要的是,以上的所有邏輯,基本上都可以100%做到單元測驗覆寫,而不依賴底層的儲存,這些對于系統的穩定性都是極其重要的,

(五) 業務流程模塊化、可視化 - Application層
如同前文提到,對于定價車這種復雜的業務,我們在代碼組織上最大的問題是如何去管理所有的介面和事件,用前面的那個paidOrder方法舉例,單純從一個介面上很難看出上游的呼叫方是誰,以及什么時候、為什么被呼叫到,這個在理解代碼的時候是比較困難的一件事,同時,當FpcarDealService有超過20個類似的方法時,未來的理解和維護成本,會隨著業務的發展變得越來越高,所以針對這個問題,我們根據CQRS的思想做了一個模塊化方案,

業務流程模塊化

在DDD的架構內,Application層的核心是組織業務”流程“,定價車的業務流程主要分為2部分:1.入駐/管理、2.交易,為了讓未來的維護方能快速理解這兩個流程,我們在代碼結構上對原有的幾個Service做了拆解和重新組織,如下(entry是入駐流程、trade是交易流程):

image-20200505133934065.png

其中,我們將代碼結構拆為兩個層級:

Flow:代表業務流程
Module:代表業務流程中的某個節點
具體案例代碼如下:

@BizFlow(name = "定價車交易流程", version = "1.0.0")
public class FpcarTradeFlow {}
@BizFlowModule(flow = FpcarTradeFlow.class, name = "定價車定金訂單支付模塊", parents = {CreateOrderModule.class}, role = "用戶")
public class PayEarnestOrderModule {

    @Autowired
    private FpcarOrderRepository orderRepository;

    @EventHandler(name = "定金支付成功")
    public void handle(@NotNull FpcarEarnestOrderPaidEvent event) {
        BizOrderDTO bizOrder = event.getBizOrder();
        PayOrderDTO payOrder = event.getPayOrder();

        //先檢查大訂單是否存在了
        FpcarOrder fpcarOrder = orderRepository.findByEarnestOrderId(bizOrder.getBizOrderId());
        if (fpcarOrder == null) {
            throw new IllegalStateException("定價車訂單還未創建過");
        }

        // 更新狀態
        fpcarOrder.payEarnestOrder(payOrder.getPayTime());

        //保存
        orderRepository.save(fpcarOrder);
    }

}

在上面的代碼里,我們通過 @BizFlow 和 @BizFlowModule 這兩個注解,對PayEarnestOrderModule這個模塊做了“檔案”化,每個模塊代表了業務流程的一部分,其中,flow 引數表示了該模塊對應的Flow流程、name 提供了該模塊的檔案、parents 是該模塊的前置模塊,role 是該模塊的操作方,

那這些資訊該如何自動化的識別呢?在我們自己的開發者后臺中,會根據代碼中的注解自動生成一個業務流程圖:

在這里插入圖片描述
在這里插入圖片描述
這個的好處是哪怕一個新同學完全沒看過需求PRD檔案,也能通過代碼、注解及其生成的流程圖,快速上手一個復雜的業務,同時,哪怕未來的代碼變化了但是檔案沒跟上,我們也可以從最新的代碼中快速理解一個業務流程,做到”代碼即檔案“,

模塊化的一些規范

在這個流程中,我們經常需要回答的問題是如何合理的劃分不同的模塊?傳統的一些簡單的Service,是根據領域模型來劃分邊界,一般來說要包含“增、刪、改、查”,以及一些針對領域物件的操作,但是在DDD架構下,很多增刪改查和操作的邏輯應該都抽象到了對應的Entity和Repository中,而Application Service層的邏輯更多的是流程的封裝,也就是說通常需要跨多個領域物體,在這個架構下傳統的Service劃分方式將不再適用,這時候我們需要有一套合理的規范來約束我們的模塊劃分,在這里我們依賴了單一職責原則(Single Responsibility Principle,SRP),

SRP的定義其實很多人都會誤解,SRP的概念不是一個代碼模塊/類應該“只做一件事情”,而是一個代碼模塊應該“只因為一件事情而改變”,也就是說,我們在判斷一個模塊是否符合SRP原則的時候,只要去看這個類是否會因為多件事而改變,如果我們用SRP原則去看一些傳統的Service,能明顯的看到幾乎所有的Service都不符合SRP的原則,因為一般來說,一個領域模型的增、刪、改、查基本上都在業務流程中的不同部分,會因為多個需求的變化而變化,

所以在做模塊化的程序中,我們需要考慮的是如何讓一個模塊能符合SRP,同時盡可能的提升模塊的內聚性,降低模塊間的關聯性,在定價車業務中,我們采用的一個規范是根據前端頁面的操作來劃分模塊,也就是說一個頁面上的所有操作都內聚到一個模塊中,例子如下:

針對于業務流程中的用戶核銷后提交資料和墊付車款的需求,

在這里插入圖片描述
在這里插入圖片描述

代碼如下:

@BizFlowModule(flow = FpcarTradeFlow.class, name = "門店墊資資訊", parents = {CreateOrderModule.class}, role = "門店")
public class StoreSubmitInvestDataModule {

    @Autowired
    private FpcarOrderRepository orderRepository;

    @Autowired
    private PartnerRepository partnerRepository;

    @Autowired
    private MyBankFacade mybankFacade;

    @CommandHandler(name = "門店墊資")
    public FpcarInvestDTO handleInvest(@NotNull FpcarInvestCommand cmd) {
        // 省略
    }

    @CommandHandler(name = "門店提交資訊")
    public Boolean handleSubmitData(@NotNull FpcarSubmitDataCommand cmd) {
        // 省略
    }

    @QueryHandler(name = "查詢門店墊資資訊")
    public FpcarInvestDTO queryInvestInfo(@NotNull FpcarInvestQuery query) {
        // 省略
    }

}

可以看出來,在這個步驟中,該頁面如果有任何需要修改的邏輯,全部都可以在這一個Module類中實作變更,而不需要任何其他Module的變更,符合了SRP原則,

CQRS引數顯性化

在上面的代碼里,有幾個和傳統Service引數不一樣的地方:

第一,所有Module的公開方法的入參都是Command、Query或者Event,這是我們采用CQRS的代碼規范,例子如下:

// 門店提交資料指令
@Data
public class FpcarSubmitDataCommand implements Command<Boolean> {
    private Long storeId;
    private Long id;
    private String investData;
}

// 其中T是該指令應該的回傳值
public interface Command<T> {}

這個跟傳統的Service介面的方式有明顯的差異:

Result<Boolean> submitData(Map<String, Object> paramsMap); // (1)

Result<Boolean> submitData(Long storeId, Long id, String investData); // (2)

在傳統的方法中,入參一般有以下兩種方式:

(1)通用的大Map,可以保證介面兼容性,但需要在代碼里解決Map決議和例外處理的問題,

(2)固定引數,但如果未來的需求變更,可能需要介面變更,造成可能的介面不兼容,通常需要通過增加介面方式向前兼容,

我們通過對所有的入參做強型別顯性化包裝,可以確保1.介面的兼容、以及2.引數的可拓展性,同時,通過對多個引數的顯性封裝,我們能更明確的理解該方法的“目的性”,不需要再僅僅通過方法名去理解方法的目的,

第二,在公開方法上我們增加了@CommandHandler、@QueryHandler、和@EventHandler注解,這些注解不僅僅是起到了一定的檔案作用,同時在啟動時,Aslan框架會對所有標記了注解的方法做入參/出參匹配校驗,確保方法的出參符合預期(這個功能后續可以考慮改為靜態編譯時執行),具體的實作比較復雜不在此處贅述,因為有了注解和規范,關于該模塊的方法資訊可以上報到研發平臺,供開發快速了解每個模塊的方法,如下:

image-20200507153050174.png

第三(可選),增加了注解的方法,Aslan框架在啟動時會注冊到一個注冊中心,讓呼叫方更加簡單的去使用該方法,而不必太關注該方法所在的具體模塊,同樣的案例如下,為了做到和原有HSF介面兼容,我們做了一次轉換:

// 原來的HSF介面
public Result<Boolean> submitData(Map<String, Object> paramsMap) {
    Result<Boolean> result = new Result<>();
    try {
        Long storeId = getStoreId(paramsMap);
        String investData = (String) paramsMap.get("investData");
        Long id = (Long) paramsMap.get("id");
        // 省略部分校驗代碼

        FpcarSubmitDataCommand cmd = new FpcarSubmitDataCommand();
        cmd.setStoreId(storeId);
        cmd.setId(id);
        cmd.setInvestData(investData);
        Boolean success = commandBus.dispatch(cmd); // (1)
        result.setObject(success);
    } catch (FpcarException fpe) {
        result.setSuccess(false);
        result.setObject(false);
        result.setErrorMessage(fpe.getMessage());
    } catch (Exception e) {
        result.setError(ErrorInfoEnum.SERVICE_NOT_USE);
    }
    return result;
}

在上面的代碼里,(1)的commandBus可以通過全域注冊查找該Command對應的CommandHandler,然后通過dispatch來間接呼叫該Handler,這種呼叫方式避免了需要直接引入StoreSubmitInvestDataModule這個依賴,也就是說可以讓Interface層和Application層解耦,CommandBus/EventBus/QueryBus的dispatch方法都是同步呼叫,相當于直接參考,另外提供的dispatchAsync方法,可以通過傳入一個自定義Executor,實作異步呼叫,回傳CompletableFuture,在此不再贅述,

通過CommandBus這種間接呼叫的方式可能有一定的理解成本,業務開發需要對自己的理解做評估,然后選擇采用CommandBus,如果不用CommandBus,直接呼叫Module方法也是一樣的,如下:

@Autowired
StoreSubmitInvestDataModule storeSubmitInvestDataModule;

// ...

FpcarSubmitDataCommand cmd = new FpcarSubmitDataCommand();
Boolean success = storeSubmitInvestDataModule.handleSubmitData(cmd);

(六)隔離第三方依賴 - 防腐層Facade

任何一個復雜業務都不可能不依賴第三方服務,在定價車業務里,我們依賴了UIC、IC、交易、網商、新零售、FP、匯金、訊息等第三方服務,擁有大量的第三方依賴帶來很多問題:

除錯對接成本很高:每個依賴方都有完全不同的接入方法,有些復雜依賴(如網商,需要加密解密,決議XML等)的從0開始的對接和除錯驗證成本可能要幾周,
第三方服務升級會導致不兼容或沖突:每年各種中間件、第三方依賴升級所需的作業量巨大,特別是當出現不兼容升級或沖突出現時,需要對代碼進行升級和測驗,產生大量的成本,
依賴了第三方資料格式:當你在業務邏輯里依賴了第三方的資料格式時,會導致你的代碼對第三方產生強依賴,讓你的代碼變得僵硬,特別是當第三方資料格式不可變時,
無法單測:直接依賴了第三方服務介面,就如同直接依賴DAO一樣,會導致業務邏輯無法單元測驗,只能集成測驗,而在很多情況下集成測驗的成本極高,比如絕大部分的第三方缺少穩定的日常環境,而在預發聯調的風險又極高,
為了解決這些問題,讓我們自己的代碼邏輯可單測且不依賴第三方,我們對一些常用的第三方依賴相關的邏輯做了一次封裝,并且利用了Spring Boot Starter的特性,通過AutoConfiguration解決了依賴配置的問題,呼叫方只需要引入一個Starter POM包即可,這個封裝的Starter就是我們的防腐層,

一個標準Starter的組成

Facade:新的介面和出參入參的DTO
Core:具體和第三方依賴的對接實作,以及AutoConfiguration相關的配置
在一個正常的代碼里,業務邏輯應該只依賴新的Facade,然后在應用的最外層依賴相關的Starter POM即可,starter自帶的spring.factories和AutoConfiguration會自動把對應的實作在運行時注入,

舉個上面分傭的例子,原有的代碼里需要直接呼叫匯金的介面,需要直接依賴匯金的服務和DTO:

// 構建入參
BizRequestDTO bizRequestDTO = new BizRequestDTO();
// 省略各種引數拼裝和校驗邏輯
ReturnWrapper<BizResponseDTO> returnWrapper = iBizGwService.submit(bizRequestDTO);
通過封裝了一層Facade層,我們可以隔離匯金的介面:

@BizFlowModule(flow = FpcarTradeFlow.class, name = "定價車分傭A模塊", parents = {UseEarnestOrderModule.class}, role = "用戶")
public class CommissionAModule {

    @Autowired
    private HuijinFacade huijinFacade;

    @EventHandler(name = "定價車分傭A事件")
    public void handleEarnestOrderSuccess(@NotNull FpcarCommissionAEvent event) {
        // ...
        HuijinRequestDTO request = new FpcarSourceHuijinRequestBuilder()
                .fpcarConfig(fpcarConfig)
                .fpcarOrder(fpcarOrder)
                .user(user)
                .build();

        final Result<HuijinBizResponseDTO> commissionResult = huijinFacade.submit(request);
    }

}

在上面的代碼中,業務代碼不再直接依賴匯金的介面和出入參,而是依賴我們自定義的、可控的Facade和DTO,并且由于該Facade只是一個介面類,可以隨意的Mock和Stab,讓該方法可控可測驗,

除了提供Mock之外,一個防腐層能提供更多的功能如:快取、兜底、限流、多業務身份隔離等功能,具體的實作規范會在我后面的一篇文章里詳細解釋,

老業務遷移方案

最后,我們在遷移代碼之后,需要考慮如何做老業務的遷移,考慮到修改前端的成本比較高也暫時沒有人力去完成,整體的遷移方案希望能做一個成本最低的方案,我們對定價車整體的遷移方案設計為以下兩個階段:

第一階段 - 新應用,介面簽名不變,HSF V2.0

在第一個階段,遷移成本最低的方案是確保所有的對外HSF介面都保持簽名不變,僅僅是HSF版本變化,如下:

image-20200507203148790.png

定價車業務的前臺介面都是由carcenter-front應用來提供,然后通過HSF呼叫到carcenter上,其HSF介面定義在carcenter-client二方包里,

在遷移后的carcenter2上,fpcar-interface-old模塊重新實作了carcenter-client的介面,但是其介面實作只相當于引數拼裝后轉發到application層,然后把結果重新轉化為原有的資料格式,案例如下:

@HSFProvider(serviceVersion = "${old.hsf.version}") // old.hsf.version = 2.0.0
public class FpcarPartnerServiceImpl implements FpcarPartnerService {

    @Autowired
    private QueryBus queryBus;

    private FpcarConverters converters = FpcarConverters.INSTANCE;

    @Override
    public Result<List<FpcarPartnerDO>> queryList(Map<String, Object> paraMap) {
        PartnerPagedQuery query = PartnerPagedQuery.fromMap(paraMap);
        Page<PartnerDTO> queryResult = queryBus.dispatch(query);

        Result<List<FpcarPartnerDO>> result = new Result<>();
        List<FpcarPartnerDO> list = queryResult.getContent().stream().map(converters::fromDTO).collect(Collectors.toList());
        result.setObject(list);
        return result;
    }
}

在前臺應用carcenter-front中,逐漸切流的方法有很多,最簡單的就是配置一個開關通過白名單或用戶ID或商家ID,通過配置兩個不同版本的HSFConsumer來切流,

第二階段 - 切換到新介面、新實作

第二個階段需要一定的前臺改造,但是能有效的減少代碼轉換邏輯和維護老介面的成本,邏輯如下:

在這里插入圖片描述

能看出來,新老介面的底層都呼叫了application層的業務邏輯,所以整體遷移的風險不大,只是前臺適配的作業量,這一步需要每個業務按需執行,

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/232676.html

標籤:其他

上一篇:Faster RCNN原理介紹

下一篇:iOS自定義彈窗之翻牌影片—Swift

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more